diff --git a/l10n_ar_txt_sire/README.rst b/l10n_ar_txt_sire/README.rst new file mode 100644 index 00000000..74b8182f --- /dev/null +++ b/l10n_ar_txt_sire/README.rst @@ -0,0 +1,69 @@ +.. |company| replace:: ADHOC SA + +.. |company_logo| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-logo.png + :alt: ADHOC SA + :target: https://www.adhoc.com.ar + +.. |icon| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-icon.png + +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +======================== +Tax Settlements For Sire +======================== + +Este módulo imlementa: + +* Archivos para declaración de impuesto sire de retenciones aplicadas a sujetos domiciliados en el exterior. + +Archivos para declaración de impuestos +====================================== + +* SIRE: https://www.afip.gob.ar/sire/documentos/SIRE-especificacion-para-emision-por-lote.pdf apartado 3. F2003 CERTIFICADOS SUJETOS DOMICILIADOS EN EL EXTERIOR. +* CERTIFICADOS DE RETENCIÓN IMPOSITIVA: https://www.afip.gob.ar/sire/documentos/SIRE-especificacion-para-emision-por-lote.pdf apartado 5. F2005 CERTIFICADOS DE RETENCIÓN IMPOSITIVA (beta: nunca fue testeado). + +Installation +============ + +To install this module, you need to: + +#. Only need to install the module + +Configuration +============= + +To configure this module, you need to: + + 1. Crear diario de liquidación con la etiqueta para liquidación "Sire" y elegir "TXT Retenciones SIRE" en el campo "Impuesto de liquidación". + 2. Crear impuesto de sire y agregar la etiqueta "Sire" en cuadrículas de impuesto en vista formulario del impuesto y agregar codigo de regimen en el campo "Codigo de regimen IVA" en la solapa "Opciones avanzadas" del impuesto. + 3. El contacto debe tener país y si es tipo 'individual' entonces se deberá establecer el país de nacimiento y la fecha de nacimiento en la solapa "Datos Fiscales" de la vista formulario del contacto. + 4. Establecer "Sire Codigo Alicuota" en la solapa "Datos Fiscales" de la vista formulario del contacto. Si no lo establece entonces en el pago será requerido que lo establezca. + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: http://runbot.adhoc.com.ar/ + +Credits +======= + +Images +------ + +* |company| |icon| + +Contributors +------------ + +Maintainer +---------- + +|company_logo| + +This module is maintained by the |company|. + +To contribute to this module, please visit https://www.adhoc.com.ar. diff --git a/l10n_ar_txt_sire/__init__.py b/l10n_ar_txt_sire/__init__.py new file mode 100644 index 00000000..d0337769 --- /dev/null +++ b/l10n_ar_txt_sire/__init__.py @@ -0,0 +1,5 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import models diff --git a/l10n_ar_txt_sire/__manifest__.py b/l10n_ar_txt_sire/__manifest__.py new file mode 100644 index 00000000..dd1e849b --- /dev/null +++ b/l10n_ar_txt_sire/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': 'Txt SIRE', + 'version': "16.0.1.0.0", + 'category': 'Accounting', + 'website': 'www.adhoc.com.ar', + 'license': 'LGPL-3', + 'depends': [ + 'l10n_ar_account_tax_settlement', + ], + 'data': [ + 'data/account_account_tag_data.xml', + 'views/res_partner_view.xml', + 'views/account_payment_view.xml', + ], + 'demo': [ + ], + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/l10n_ar_txt_sire/data/account_account_tag_data.xml b/l10n_ar_txt_sire/data/account_account_tag_data.xml new file mode 100644 index 00000000..4ce86e06 --- /dev/null +++ b/l10n_ar_txt_sire/data/account_account_tag_data.xml @@ -0,0 +1,8 @@ + + + + Sire + taxes + + + diff --git a/l10n_ar_txt_sire/doc/sire/SIRE-especificacion-para-emision-por-lote.pdf b/l10n_ar_txt_sire/doc/sire/SIRE-especificacion-para-emision-por-lote.pdf new file mode 100644 index 00000000..46c582f1 Binary files /dev/null and b/l10n_ar_txt_sire/doc/sire/SIRE-especificacion-para-emision-por-lote.pdf differ diff --git a/l10n_ar_txt_sire/models/__init__.py b/l10n_ar_txt_sire/models/__init__.py new file mode 100644 index 00000000..5a6e0c9b --- /dev/null +++ b/l10n_ar_txt_sire/models/__init__.py @@ -0,0 +1,3 @@ +from . import account_journal +from . import res_partner +from . import account_payment diff --git a/l10n_ar_txt_sire/models/account_journal.py b/l10n_ar_txt_sire/models/account_journal.py new file mode 100644 index 00000000..ac60b53e --- /dev/null +++ b/l10n_ar_txt_sire/models/account_journal.py @@ -0,0 +1,324 @@ +from odoo import models, fields, _ +from odoo.exceptions import RedirectWarning, UserError +import re + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + settlement_tax = fields.Selection(selection_add=[ + ('sire', 'TXT Retenciones SIRE'), + ('certificado_retencion_impositiva', 'TXT Certificado Retención Impositiva'), + ]) + + def sire_files_values(self, move_lines): + """ Devuelve contenido del archivo Retenciones_sire.txt . Implementado según especificación de tarea 40906. + https://www.afip.gob.ar/sire/documentos/SIRE-especificacion-para-emision-por-lote.pdf apartado 3. + F2003 CERTIFICADOS SUJETOS DOMICILIADOS EN EL EXTERIOR. + También se puede ver la especificación en doc/SIRE-especificacion-para-emision-por-lote.pdf """ + self.ensure_one() + content = '' + for line in move_lines.sorted(key=lambda r: (r.date, r.id)): + payment = line.payment_id + # VALIDACIONES + self._sire_validations(move_lines) + pais = line.partner_id.country_id + es_persona = line.partner_id.company_type == 'person' + partner_vat = payment.partner_id.vat + + fecha_impuesto = fields.Date.from_string(line.date).strftime('%d/%m/%Y') + # 1 Formulario (integer long 4, 1-4, obligatorio) --> '2003' comentado por agi en tarea 40050, en nota del + # 23/10/2024 a las 10:00hs + # Además en la sección "3.2. F2003 - Validaciones", pàgina 5 del pdf de la especificación se indica que + # debe ser fijo "2003" + content += '2003' + # 2 Versión (integer long 4, 5-8, obligatorio) --> '0100' comentado por agi en tarea 40050, en nota del + # 23/10/2024 a las 10:00hs + # Además en la sección "3.2. F2003 - Validaciones", pàgina 5 del pdf de la especificación se indica que debe + # ser fijo "0100" + content += '0100' + # 3 Código de trazabilidad (string long 10, 9-18, no obligatorio) --> texto libre o en blanco --> + # comentado por agi en tarea 40050, en nota del 23/10/2024 a las 10:00hs + content += ' ' * 10 + # 4 Cuit agente (integer, long 11, 19-29, obligatorio) + content += partner_vat + # 5 Impuesto (integer long 3, 30-32, no obligatorio) --> + # sección "3.2. F2003 - Validaciones", pàgina 5 del pdf de la especificación se indica que debe ser + # fijo "218" + content += '218' + # 6 Régimen (integer long 3, 33-35, obligatorio) + content += payment.tax_withholding_id.codigo_regimen[:3].zfill(3) + # 7 Cuit ordenante (integer 11, 36-46, obligatorio) + content += partner_vat + # 8 Fecha retención (date long 10, 59-68, obligatorio) + content += fecha_impuesto + # 9 Tipo comprobante (integer 2, 57-58, obligatorio) + # por el momento lo dejamos fijo '06' que es el tipo de comprobante para retenciones + # pero en un futuro para percepciones puede tomar otros valrores (tomar como referencia lo desarrollado + # para sicore) + # --> ver espeficiación vieja tarea 40906 + content += '06' + # 10 Fecha comprobante (date 10, 59-68, obligatorio) + content += fecha_impuesto + # 11 Nro comprobante (string 16, 69-84, obligatorio) + # Número de Orden de Pago sin guiones ni tipo de identificación (ej: 000100008220) + # (prefijo sin - + número de documento) + # (no es obligatorio) --> ver espeficiación vieja tarea 40906 + content += re.sub('[^0-9]', '', payment.name).ljust(16) + # 12 Importe comprobante (decimal 14, 85-98, obligatorio) + content += '%14.2f' % payment.payment_group_id.payments_amount + # 13 Filler (filler 14, 99-112, obligatorio) + content += ' ' * 14 + # 14 Certificado original nro (string 25, 113-137, no obligatorio) + content += ' ' * 25 + # 15 Certificado original fecha reten (date 10, 138-147, no obligatorio) + content += ' ' * 10 + # 16 Certificado original importe (decimal 14, 148-161, no obligatorio) + content += ' ' * 14 + # 17 Motivo emisión nota de créditon(string 30, 162-191, no obligatorio) + content += ' ' * 30 + # 18 No retención (boolean 1, 192-192) --> ver especificación vieja tarea 40906 + content += '0' + # 19 No retención motivo (string 30, 193-222, no obligatorio) + content += '0' * 30 + # 20 Aplica CDI (boolean 1, 223-223, obligatorio) --> especificación 40906, mt 11/12/24 + content += '1' if payment.sire_aplica_cdi else '0' + # 21 Código de alícuota (integer, 4, 224-227, obligatorio) + content += payment.sire_codigo_alicuota.zfill(4) + # 22 Aplica acrecentamiento (boolean, 1, 228-228) + content += '1' if payment.sire_aplica_acrecentamiento else '0' + # 23 Retenido clave nif (string 50, 229-278, obligatorio) + # Cuit del pais del sujeto retenido s/ especificación tarea 40906, mt 11/12/24 + content += pais.l10n_ar_natural_vat if es_persona else pais.l10n_ar_legal_entity_vat + content += ' ' * 39 + # 24 Retenido Apellido Nombre Denominacion (string, 60, 279-338, obligatorio) + content += line.partner_id.name[:60].ljust(60) + # 25 Retenido domicilio actual en exterior (string, 60, 339-398, obligatorio) + # domicilio completo --> comentado por agi en tarea 40050, en nota del 23/10/2024 a las 10:00hs + domicilio = '' + if payment.partner_id.street: + domicilio += payment.partner_id.street + if payment.partner_id.street2: + domicilio += ' ' + payment.partner_id.street2 + if payment.partner_id.city: + domicilio += ' ' + payment.partner_id.city + if payment.partner_id.state_id: + domicilio += ' ' + payment.partner_id.state_id.name + content += domicilio[:60].ljust(60) if domicilio else ' ' * 60 + # 26 Retenido domicilio actual en exterior pais (integer, 3, 399-401, obligatorio) + content += line.partner_id.country_id.l10n_ar_afip_code or ' ' * 3 + # 27 Retenido tipo de persona (string, 1, 402-402, obligatorio) + content += 'F' if es_persona else 'J' + # 28 Retenido nacimiento constitucion pais (integer, 3, 403-405, no obligatorio) + content += line.partner_id.born_country_id.l10n_ar_afip_code if es_persona else ' ' * 3 + # 29 Retenido nacimiento constitucion fecha (date 10, 406-415, no obligatorio) + content += line.partner_id.birthdate.strftime('%d/%m/%Y') if es_persona else ' ' * 10 + content += '\r\n' + return [{ + 'txt_filename': ('Retenciones') + '_sire.txt', + 'txt_content': content, + }] + + def _sire_validations(self, move_lines): + """ Validaciones para el archivo TXT Retenciones SIRE. 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. """ + # Validamos que el impuesto SIRE tenga código de régimen establecido + for line in move_lines.sorted(key=lambda r: (r.date, r.id)): + payment = line.payment_id + if not payment.tax_withholding_id.codigo_regimen: + raise RedirectWarning( + message=_("El impuesto '%s' no tiene código de régimen establecido. Es obligatorio para generar el" + " archivo txt Sire. Editar campo 'Codigo de regimen IVA' en solapa 'Opciones avanzadas'" + " en la vista formulario", payment.tax_withholding_id.name), + action={ + 'type': 'ir.actions.act_window', + 'res_model': 'account.tax', + 'views': [(False, 'form')], + 'res_id': payment.tax_withholding_id.id, + 'name': _('Tax'), + 'view_mode': 'form', + }, + button_text=_('Editar impuesto'), + ) + + # Validamos que el partner sea Cliente / Proveedor del Exterior + if line.partner_id.l10n_ar_afip_responsibility_type_id.id != self.env.ref('l10n_ar.res_EXT').id: + raise UserError(_("Solo puede generar el archivo de retenciones SIRE para contactos con responsabilidad" + " AFIP: 'Cliente / Proveedor del Exterior'. Contacto: %s (id: %s)" + % (line.partner_id.name, line.partner_id.id))) + + # Validamos que el contacto tenga país establecido + if not line.partner_id.country_id: + raise RedirectWarning( + message=_("El contacto '%s' debe tener país establecido", payment.partner_id.name), + action={ + 'type': 'ir.actions.act_window', + 'res_model': 'res.partner', + 'views': [(False, 'form')], + 'res_id': payment.partner_id.id, + 'name': _('Tax'), + 'view_mode': 'form', + }, + button_text=_('Editar contacto'), + ) + + # Validamos que el país del contacto tenga el cuit correspondiente + pais = line.partner_id.country_id + es_persona = line.partner_id.company_type == 'person' + if es_persona: + if not pais.l10n_ar_natural_vat: + raise RedirectWarning( + message=_("El país '%s' no tiene IVA Persona Física establecido.", pais.name), + action={ + 'type': 'ir.actions.act_window', + 'res_model': 'res.country', + 'views': [(False, 'form')], + 'res_id': pais.id, + 'name': _('País'), + 'view_mode': 'form', + }, + button_text=_('Editar País'), + ) + if not es_persona: + if not pais.l10n_ar_legal_entity_vat: + raise RedirectWarning( + message=_("El país '%s' no tiene cuit persona jurídica establecido.", pais.name), + action={ + 'type': 'ir.actions.act_window', + 'res_model': 'res.country', + 'views': [(False, 'form')], + 'res_id': pais.id, + 'name': _('País'), + 'view_mode': 'form', + }, + button_text=_('Editar País'), + ) + + # Validamos que el cuit del agente sea de 11 dígitos + partner_vat = payment.partner_id.vat + if not partner_vat or (partner_vat and len(partner_vat) != 11): + raise RedirectWarning( + message=_("El cuit del agente '%s' debe ser de 11 dígitos de longitud.", payment.partner_id.name), + action={ + 'type': 'ir.actions.act_window', + 'res_model': 'res.partner', + 'views': [(False, 'form')], + 'res_id': payment.partner_id.id, + 'name': _('Tax'), + 'view_mode': 'form', + }, + button_text=_('Editar contacto'), + ) + + # Validamos que el código de alícuota se encuentre entre 1 y 83 si no aplica CDI + if not payment.sire_aplica_cdi: + if int(payment.sire_codigo_alicuota) > 83: + raise UserError(_("El pago %s (id: %s) debe tener código de alícuota" + " menor a 83 ya que no aplica CDI" % (payment.name, payment.id))) + + def certificado_retencion_impositiva_files_values(self, move_lines): + """ Devuelve contenido del archivo Retenciones_sire.txt .Implementado según especificación de tarea 40906. + https://www.afip.gob.ar/sire/documentos/SIRE-especificacion-para-emision-por-lote.pdf apartado 5. + F2005 CERTIFICADOS DE RETENCIÓN IMPOSITIVA. También se puede ver la especificación en + doc/SIRE-especificacion-para-emision-por-lote.pdf . (beta: nunca fue testeado) .""" + self.ensure_one() + content = '' + for line in move_lines.sorted(key=lambda r: (r.date, r.id)): + payment = line.payment_id + if not payment.tax_withholding_id.codigo_regimen: + raise RedirectWarning( + message=_("El impuesto '%s' no tiene código de régimen establecido." + " Editar campo 'Codigo de regimen IVA' en solapa 'Opciones avanzadas'" + "en la vista formulario", + payment.tax_withholding_id.name), + action={ + 'type': 'ir.actions.act_window', + 'res_model': 'account.tax', + 'views': [(False, 'form')], + 'res_id': payment.tax_withholding_id.id, + 'name': _('Tax'), + 'view_mode': 'form', + }, + button_text=_('Editar impuesto'), + ) + fecha_impuesto = fields.Date.from_string(line.date).strftime('%d/%m/%Y') + # 1 Versión (integer long 4, 1-4, obligatorio) --> 0100 + content += '0100' + # 2 Código de trazabilidad (string long 36, 5-40, no obligatorio) + content += ' '*36 + # 3 Impuesto (integer long 3, 41-43, obligatorio) + content += '216' + # 4 Régimen (integer long 3, 44-46, obligatorio) + content += payment.tax_withholding_id.codigo_regimen + # 5 Fecha retención (date long 10, 47-56, obligatorio) + content += fecha_impuesto + # 6 Condición (integer 2, 57-58, no obligatorio) + content += ' '*2 + # 7 Imposibilidad de retención (boolean long 1, 59-59, obligatorio) + content += '0' + # 8 No retención motivo (string 30, 60-89, no obligatorio) + content += ' '*30 + # 9 Importe retención (decimal 14, 90-103, obligatorio) + content += '%014.2f' % abs(line.balance) + # 10 Importe de la base de cálculo/cantidad (decimal 14, 104-117, obligatorio) + content += '%014.2f' % abs(payment.withholding_base_amount) + # 11 Régimen de exclusión (boolean 1, 118-118, obligatorio) + content += '0' + # 12 Porcentaje de exclusión (decimal 6, 119-124, no obligatorio) + content += '%06.2f' % payment.tax_withholding_id.porcentaje_exclusion\ + if payment.tax_withholding_id.porcentaje_exclusion != '0.0' else '000.00' + # 13 Fecha publicación o finalización de la vigencia (date 10, 125-134, no obligatorio) + content += ' '*10 + # 14 Tipo comprobante (integer 2, 135-136, obligatorio) + # por el momento lo dejamos fijo '06' que es el tipo de comprobante para retenciones + # pero en un futuro para percepciones puede tomar otros valrores (tomar como referencia lo desarrollado + # para sicore) + content += '06' + # 15 Fecha comprobante (date 10, 137-146, obligatorio) + content += fecha_impuesto + # 16 Nro comprobante (string 16, 147-162, no obligatorio) + content += re.sub('[^0-9]', '', payment.name).ljust(16) + # 17 COE (string 12, 163-174, no obligatorio) + content += ' '*12 + # 18 COE ORIGINAL (string 12, 175-186, no obligatorio) + content += ' '*12 + # 19 CAE (string 14, 187-200, no obligatorio) + content += ' '*14 + # 20 Importe comprobante (decimal 14, 201-214, obligatorio) + content += '%14.2f' % payment.payment_group_id.payments_amount + # 21 Motivo emisión de nota de crédito/ajuste (string 30, 215-244, no obligatorio) + content += ' '*30 + # 22 Retenido clave (integer 11, 245-255, obligatorio) + # Si es cliente del exterior establecemos cuit del país del exterior, sino establecemos l10n_ar_vat + if line.partner_id.l10n_ar_afip_responsibility_type_id.id == self.env.ref('l10n_ar.res_EXT').id: + pais = line.partner_id.country_id + if not pais.l10n_ar_legal_entity_vat: + raise RedirectWarning( + message=_("El país '%s' no tiene cuit persona jurídica establecido.", pais.name), + action={ + 'type': 'ir.actions.act_window', + 'res_model': 'res.country', + 'views': [(False, 'form')], + 'res_id': pais.id, + 'name': _('País'), + 'view_mode': 'form', + }, + button_text=_('Editar País'), + ) + content += pais.l10n_ar_legal_entity_vat + else: + content += line.partner_id.l10n_ar_vat + # 23 Certificado original nro (string 25, 256-280, no obligatorio) + content += ' '*25 + # 24 Certificado original fecha reten (date 10, 281-290, no obligatorio) + content += ' '*10 + # 25 Certificado original importe (decimal 14, 291-304, no obligatorio) + content += ' '*14 + # 26 Motivo de la anulación (integer 1, 305-305, no obligatorio) + content += ' '*1 + content += '\r\n' + return [{ + 'txt_filename': ('Retenciones') + '_sire.txt', + 'txt_content': content, + }] diff --git a/l10n_ar_txt_sire/models/account_payment.py b/l10n_ar_txt_sire/models/account_payment.py new file mode 100644 index 00000000..4c827b20 --- /dev/null +++ b/l10n_ar_txt_sire/models/account_payment.py @@ -0,0 +1,25 @@ +from odoo import models, fields, api + + +class AccountPayment(models.Model): + + _inherit = "account.payment" + + sire_aplica_cdi = fields.Boolean(related='partner_id.sire_aplica_cdi', + readonly=False, + help="Campo para archivo txt Ganancias SIRE. Marcar si aplica CDI") + sire_aplica_acrecentamiento = fields.Boolean(related='partner_id.sire_aplica_acrecentamiento', + readonly=False, + help="Campo para archivo txt Ganancias SIRE. Marcar si aplica CDI") + sire_codigo_alicuota = fields.Char(related='partner_id.sire_codigo_alicuota', readonly=False, size=4) + # Este campo se usa para hacer invisibles los campos anteriores en el pago si no se trata de una retención + # de sire + es_sire = fields.Boolean(compute='_compute_es_sire') + + @api.onchange('tax_withholding_id') + def _compute_es_sire(self): + tag_tax_sire = self.env.ref('l10n_ar_txt_sire.tag_tax_sire') + payments_with_sire = self.filtered(lambda pay: tag_tax_sire in + pay.tax_withholding_id.invoice_repartition_line_ids.tag_ids) + payments_with_sire.es_sire = True + (self - payments_with_sire).es_sire = False diff --git a/l10n_ar_txt_sire/models/res_partner.py b/l10n_ar_txt_sire/models/res_partner.py new file mode 100644 index 00000000..72939a7a --- /dev/null +++ b/l10n_ar_txt_sire/models/res_partner.py @@ -0,0 +1,13 @@ +from odoo import models, fields + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + sire_aplica_cdi = fields.Boolean(string='Aplica CDI', + help="Campo para archivo txt Ganancias SIRE. Marcar si aplica CDI") + sire_aplica_acrecentamiento = fields.Boolean(string='Aplica acrecentamiento', + help="Campo para archivo txt Ganancias SIRE. Marcar si aplica CDI") + sire_codigo_alicuota = fields.Char(size=4) + born_country_id = fields.Many2one('res.country', string='País de Nacimiento', ondelete='restrict') + birthdate = fields.Date(string='Fecha de Nacimiento') diff --git a/l10n_ar_txt_sire/views/account_payment_view.xml b/l10n_ar_txt_sire/views/account_payment_view.xml new file mode 100644 index 00000000..d5a64108 --- /dev/null +++ b/l10n_ar_txt_sire/views/account_payment_view.xml @@ -0,0 +1,16 @@ + + + + account.payment.form.inherited + account.payment + + + + + + + + + + + diff --git a/l10n_ar_txt_sire/views/res_partner_view.xml b/l10n_ar_txt_sire/views/res_partner_view.xml new file mode 100644 index 00000000..877700a4 --- /dev/null +++ b/l10n_ar_txt_sire/views/res_partner_view.xml @@ -0,0 +1,17 @@ + + + + res.partner.form + res.partner + + + + + + + + + + + +