From 90208f0bf177e00bd3dce4d65090fad8d63bca2d Mon Sep 17 00:00:00 2001 From: Pablo Montenegro Date: Mon, 11 Nov 2024 17:30:39 -0300 Subject: [PATCH] nuevos cambios --- l10n_ar_txt_tucuman/models/account_journal.py | 184 +++++++++++------- 1 file changed, 109 insertions(+), 75 deletions(-) diff --git a/l10n_ar_txt_tucuman/models/account_journal.py b/l10n_ar_txt_tucuman/models/account_journal.py index cf5625d0..fc25702c 100644 --- a/l10n_ar_txt_tucuman/models/account_journal.py +++ b/l10n_ar_txt_tucuman/models/account_journal.py @@ -1,6 +1,5 @@ from odoo import models, fields, _ from odoo.exceptions import UserError -from odoo.addons.l10n_ar_account_tax_settlement.models.account_journal import get_pos_and_number class AccountJournal(models.Model): @@ -10,102 +9,137 @@ class AccountJournal(models.Model): ('iibb_tucuman', 'TXT Retenciones/Percepciones Tucuman') ]) - def validate_len_pos(self, pos, line_name): - """ Este método valida que el punto de venta no tenga más de 5 dígitos. """ - if pos > 9999: - raise UserError(_("El punto de venta del comprobante %s debe ser de 4 dígitos." % (line_name))) - def iibb_tucuman_files_values(self, move_lines): - """ Implementado segun especificación indicada en tarea 38200. Ver especificación también en l10n_ar_txt_tucuman/doc/MRETPER6R2.pdf a partir de la página 12. """ + """ Implementado segun especificación indicada en tarea 38200. + Ver especificación también en l10n_ar_txt_tucuman/doc/MRETPER6R2.pdf a partir de la página 12. """ self.ensure_one() - content_datos = '' - content_retper = '' - content_ncfact = '' + content_datos, content_retper, content_ncfact = '', '', '' # VALIDACIONES - if nc_without_reversed_entry_id := move_lines.filtered(lambda x: x.move_type == 'out_refund' and not x.move_id.reversed_entry_id): - raise UserError(_("Algunos comprobantes rectificativos no contienen información de que comprobante original están revirtiendo: %s") % (", ".join(nc_without_reversed_entry_id.mapped('move_id.name')))) - if moves_without_street_city_state := move_lines.filtered(lambda x: not x.partner_id.street or not x.partner_id.city or not x.partner_id.state_id or not x.partner_id.zip): - raise UserError(_("Algunos comprobantes no contienen información acerca de la calle/ciudad/provincia/cod postal del contacto: %s") % (", ".join(moves_without_street_city_state.mapped('move_id.name')))) - es_percepcion = move_lines.filtered(lambda x: x.l10n_latam_document_type_id.internal_type == 'invoice') - if es_percepcion and len(es_percepcion) != len(move_lines): - raise UserError(_("Debe generar archivos para TXT Tucuman por separado para retenciones por un lado y percepciones por otro.")) + percepciones = self._iibb_tucuman_validations(move_lines) # ELABORACIÓN DE ARCHIVOS TXT - for line in move_lines.sorted(key=lambda r: (r.date, r.id)): - # Archivo DATOS.TXT - # FECHA, longitud: 8. Formato AAAAMMDD + lines = move_lines.sorted(key=lambda r: (r.date, r.id)) + return [{ + 'txt_filename': 'DATOS.txt', + 'txt_content': self._iibb_tucuman_datos_txt_file(lines), + }, + {'txt_filename': 'RETPER.txt', + 'txt_content': self._iibb_tucuman_retper_txt_file(lines), + }, + {'txt_filename': 'NCFACT.TXT', + 'txt_content': self._iibb_tucuman_ncfact_txt_file(lines.filtered(lambda x: x.move_type == 'out_refund')), + }] + + def _iibb_tucuman_validations(self, move_lines): + """ Validaciones para el archivo TXT Retenciones/Percepciones Tucuman. Si no hay errores este método no + devuelve nada, de lo contrario se lanzará mensaje de error que corresponda indicando lo que el usuario debe + corregir para poder generar el archivo. """ + if nc_without_reversed_entry_id := move_lines.filtered(lambda x: x.move_type == 'out_refund' + and not x.move_id.reversed_entry_id): + raise UserError(_("Algunos comprobantes rectificativos no contienen información de que " + "comprobante original están revirtiendo: %s") % + (", ".join(nc_without_reversed_entry_id.mapped('move_id.name')))) + if moves_without_street_city_state := move_lines.filtered(lambda x: not x.partner_id.street or + not x.partner_id.city or + not x.partner_id.state_id or not x.partner_id.zip): + raise UserError(_("Algunos comprobantes no contienen información acerca de la calle/ciudad/provincia/cod " + "postal del contacto: %s") % + (", ".join(moves_without_street_city_state.mapped('move_id.name')))) + move_lines_with_five_digits_pos = move_lines.filtered( + lambda x: x.move_id._l10n_ar_get_document_number_parts( + x.move_id.l10n_latam_document_number, + x.l10n_latam_document_type_id.code + )['point_of_sale'] > 9999 # Verificar si el punto de venta es mayor a 9999 + ) + if move_lines_with_five_digits_pos: + raise UserError(_("Algunos comprobantes tienen punto de venta de 5 dígitos y deben tener de 4 dígitos para " + "poder generar el archivo txt de retenciones y percepciones de Tucuman: %s") % + (", ".join(move_lines_with_five_digits_pos.mapped('move_id.name')))) + percepciones = move_lines.filtered(lambda x: x.move_id.is_invoice()) + if percepciones and len(percepciones) != len(move_lines): + raise UserError(_("Debe generar archivos para TXT Tucuman por separado para retenciones por un lado y " + "percepciones por otro.")) + + def _iibb_tucuman_datos_txt_file(self, lines): + """ Devuelve contenido del archivo DATOS.TXT Tucuman. """ + content_datos = '' + for line in lines: + is_perception = line.move_id.is_invoice() + # 1, FECHA, longitud: 8. Formato AAAAMMDD content_datos += fields.Date.from_string(line.date).strftime('%Y%m%d') - # TIPODOC, longitud: 2 + # 2, TIPODOC, longitud: 2 content_datos += line.partner_id.l10n_latam_identification_type_id.l10n_ar_afip_code - # DOCUMENTO, longitud: 11 + # 3, DOCUMENTO, longitud: 11 content_datos += line.partner_id.l10n_ar_vat - # TIPO COMP, longitud: 2 + # 4, TIPO COMP, longitud: 2 # 99 para retenciones por el ejemplo que pasó en el archivo adjunto el cliente en la tarea 38200 - content_datos += line.move_id.l10n_latam_document_type_id.code.zfill(2) if line in es_percepcion else '99' - # LETRA, longitud: 1 - content_datos += line.move_id.l10n_latam_document_type_id.l10n_ar_letter if line in es_percepcion else ' ' - # COD. LUGAR EMISION, longitud: 4 - pos, number = get_pos_and_number(line.move_id.l10n_latam_document_number) - self.validate_len_pos(int(pos[-4:]), line.move_id.name) - content_datos += pos[-4:] - # NUMERO, longitud: 8 - content_datos += number - # BASE_CALCULO, longitud: 15,2 - content_datos += '%015.2f' % (line.tax_base_amount if es_percepcion else line.payment_id.withholding_base_amount) - # PORCENTAJE/ALICUOTA, longitud: 6,3 - content_datos += '%06.3f' % line.tax_line_id.get_partner_alicuot(line.partner_id, line.date).alicuota_percepcion if line in es_percepcion else '%06.3f' % line.tax_line_id.get_partner_alicuot(line.partner_id, line.date).alicuota_retencion - # MONTO_RET/PER, longitud: 15,2 + content_datos += line.move_id.l10n_latam_document_type_id.code.zfill(2) if is_perception else '99' + # 5, LETRA, longitud: 1 + content_datos += line.move_id.l10n_latam_document_type_id.l10n_ar_letter if is_perception else ' ' + # 6, COD. LUGAR EMISION, longitud: 4 + document_number_parts = line.move_id._l10n_ar_get_document_number_parts( + line.move_id.l10n_latam_document_number, line.l10n_latam_document_type_id.code) + content_datos += str(document_number_parts['point_of_sale']).zfill(4) + # 7, NUMERO, longitud: 8 + content_datos += str(document_number_parts['invoice_number']).zfill(8) + # 8, BASE_CALCULO, longitud: 15,2 + content_datos += '%015.2f' % (line.tax_base_amount if is_perception else line.payment_id.withholding_base_amount) + # 9, PORCENTAJE/ALICUOTA, longitud: 6,3 + partner_alicuot = line.tax_line_id.get_partner_alicuot(line.partner_id, line.date) + if is_perception: + content_datos += '%06.3f' % partner_alicuot.alicuota_percepcion + else: + content_datos += '%06.3f' % partner_alicuot.alicuota_retencion + # 10, MONTO_RET/PER, longitud: 15,2 content_datos += '%015.2f' % abs(line.balance) content_datos += '\r\n' + return content_datos - # Archivo RETPER.TXT - # TIPODOC, longitud: 2 + def _iibb_tucuman_retper_txt_file(self, lines): + """ Devuelve contenido del archivo RETPER.TXT Tucuman. """ + content_retper = '' + for line in lines: + # 1, TIPODOC, longitud: 2 content_retper += line.partner_id.l10n_latam_identification_type_id.l10n_ar_afip_code - # DOCUMENTO, longitud: 11 + # 2, DOCUMENTO, longitud: 11 content_retper += line.partner_id.l10n_ar_vat - # NOMBRE, longitud: 40 + # 3, NOMBRE, longitud: 40 content_retper += line.partner_id.name[:40].ljust(40) - # DOMICILIO, longitud: 40 + # 4, DOMICILIO, longitud: 40 content_retper += line.partner_id.street[:40].ljust(40) - # Nro, longitud: 5 + # 5, Nro, longitud: 5 # Hacemos '9' * 5 por el ejemplo que pasó en el archivo adjunto el cliente en la tarea content_retper += '9' * 5 - # LOCALIDAD, longitud: 15 + # 6, LOCALIDAD, longitud: 15 content_retper += line.partner_id.city[:15].ljust(15) - # PROVINCIA, longitud: 15 + # 7, PROVINCIA, longitud: 15 content_retper += line.partner_id.state_id.name[:15].ljust(15) - # NO USADO, longitud 11 + # 8, NO USADO, longitud 11 content_retper += ' ' * 11 - # C. POSTAL, longitud: 8 + # 9, C. POSTAL, longitud: 8 content_retper += '%8s' % line.partner_id.zip content_retper += '\r\n' + return content_retper - # Archivo NCFACT.TXT - # COD. LUGAR EMISION NC, longitud: 4 - if line.move_type == 'out_refund': - pos_nc, number_nc = get_pos_and_number(line.move_id.l10n_latam_document_number) - self.validate_len_pos(int(pos_nc[-4:]), line.move_id.name) - content_ncfact += pos_nc[-4:] - # NUMERO NV, longitud: 8 - content_ncfact += number_nc - # COD LUGAR EMISION FAC, longitud: 4 - pos, number = get_pos_and_number(line.move_id.reversed_entry_id.l10n_latam_document_number) - self.validate_len_pos(int(pos[-4:]), line.move_id.name) - content_ncfact += pos[-4:] - # NUMERO FAC, longitud: 8 - content_ncfact += number - # TIPO FAC, longitud: 2 - content_ncfact += line.move_id.reversed_entry_id.l10n_latam_document_type_id.code.zfill(2) - content_ncfact += '\r\n' - - return [{ - 'txt_filename': 'DATOS.txt', - 'txt_content': content_datos, - }, - {'txt_filename': 'RETPER.txt', - 'txt_content': content_retper, - }, - {'txt_filename': 'NCFACT.TXT', - 'txt_content': content_ncfact, - }] + def _iibb_tucuman_ncfact_txt_file(self, lines): + """ Devuelve contenido del archivo NCFACT.TXT Tucuman. """ + content_ncfact = '' + for line in lines: + nc_document_number_parts = line.move_id._l10n_ar_get_document_number_parts( + line.move_id.l10n_latam_document_number, line.l10n_latam_document_type_id.code) + # 1, COD. LUGAR EMISION NC, longitud: 4 + content_ncfact += str(nc_document_number_parts['point_of_sale']).zfill(4) + # 2, NUMERO NV, longitud: 8 + content_ncfact += str(nc_document_number_parts['invoice_number']).zfill(8) + # 3, COD LUGAR EMISION FAC, longitud: 4 + document_number_parts = line.move_id._l10n_ar_get_document_number_parts( + line.move_id.reversed_entry_id.l10n_latam_document_number, + line.move_id.reversed_entry_id.l10n_latam_document_type_id.code) + content_ncfact += str(document_number_parts['point_of_sale']).zfill(4) + # 4, NUMERO FAC, longitud: 8 + content_ncfact += str(document_number_parts['invoice_number']).zfill(8) + # 5, TIPO FAC, longitud: 2 + content_ncfact += line.move_id.reversed_entry_id.l10n_latam_document_type_id.code.zfill(2) + content_ncfact += '\r\n' + return content_ncfact