diff --git a/l10n_ar_account_tax_settlement/models/account_journal.py b/l10n_ar_account_tax_settlement/models/account_journal.py index 325f908f..999b23fb 100644 --- a/l10n_ar_account_tax_settlement/models/account_journal.py +++ b/l10n_ar_account_tax_settlement/models/account_journal.py @@ -1,5 +1,5 @@ from odoo import models, fields, api, _ -from odoo.exceptions import ValidationError, RedirectWarning +from odoo.exceptions import ValidationError from odoo.tools.float_utils import float_round # from odoo.tools.misc import formatLang # from odoo.tools import DEFAULT_SERVER_DATE_FORMAT @@ -853,9 +853,8 @@ def iibb_aplicado_sircar_files_values(self, move_lines): line_nbr = 1 for line in move_lines.filtered('payment_id'): alicuot_line = line.tax_line_id.get_partner_alicuot( - line.partner_id, line.date) if not line.tax_line_id.withholding_type == 'code' else line.payment_id.alicuota_mendoza * 100 - - if not type(alicuot_line, float) and not alicuot_line: + line.partner_id, line.date) + if not alicuot_line: raise ValidationError(_( 'No hay alicuota configurada en el partner ' '"%s" (id: %s)') % ( @@ -894,31 +893,18 @@ def iibb_aplicado_sircar_files_values(self, move_lines): # 8 alicuota de la retencion content.append(format_amount( - alicuot_line.alicuota_retencion if not type(alicuot_line, float) else alicuot_line, 6, 2, '.')) + alicuot_line.alicuota_retencion, 6, 2, '.')) # 9 Monto retenido content.append(format_amount(-line.balance, 12, 2, '.')) # 10 Tipo de Régimen de Percepción # (código correspondiente según tabla definida por la jurisdicción) - if not type(alicuot_line, float) and not alicuot_line.regimen_retencion: + if not alicuot_line.regimen_retencion: raise ValidationError(_( 'No hay regimen de retencion configurado para la alícuota' ' del partner %s') % line.partner_id.name) - elif not line.tax_line_id.codigo_regimen: - raise RedirectWarning( - message=_("El impuesto '%s' not tiene código de regimen en solapa 'Opciones avanzadas' campo 'Codigo de regimen'.", line.tax_line_id.name), - action={ - 'type': 'ir.actions.act_window', - 'res_model': 'account.tax', - 'views': [(False, 'form')], - 'res_id': line.tax_line_id.id, - 'name': _('Tax'), - 'view_mode': 'form', - }, - button_text=_('Editar Impuesto'), - ) - content.append(alicuot_line.regimen_retencion if not type(alicuot_line, float) else line.tax_line_id.codigo_regimen) + content.append(alicuot_line.regimen_retencion) # 11 Jurisdicción: código en Convenio Multilateral de la # jurisdicción a la cual está presentando la DDJJ diff --git a/l10n_ar_txt_mendoza/__init__.py b/l10n_ar_txt_mendoza/__init__.py new file mode 100644 index 00000000..9b429614 --- /dev/null +++ b/l10n_ar_txt_mendoza/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/l10n_ar_txt_mendoza/__manifest__.py b/l10n_ar_txt_mendoza/__manifest__.py new file mode 100644 index 00000000..80289292 --- /dev/null +++ b/l10n_ar_txt_mendoza/__manifest__.py @@ -0,0 +1,19 @@ +{ + 'name': 'TXT Mendoza', + 'version': "16.0.1.0.0", + 'category': 'Accounting', + 'author': 'ADHOC SA', + 'depends': [ + 'l10n_ar_account_tax_settlement', + 'l10n_ar_account_withholding', + ], + 'data': [ + 'views/account_move_views.xml', + 'views/afip_activity_view.xml', + 'views/account_payment_view.xml', + 'wizard/res_config_settings_views.xml', + ], + 'installable': True, + 'auto_install': False, + 'application': True, +} diff --git a/l10n_ar_txt_mendoza/models/__init__.py b/l10n_ar_txt_mendoza/models/__init__.py new file mode 100644 index 00000000..fbf93f5c --- /dev/null +++ b/l10n_ar_txt_mendoza/models/__init__.py @@ -0,0 +1,6 @@ +from . import account_move +from . import afip_activity +from . import res_company +from . import account_payment_group +from . import account_payment +from . import account_journal diff --git a/l10n_ar_txt_mendoza/models/account_journal.py b/l10n_ar_txt_mendoza/models/account_journal.py new file mode 100644 index 00000000..f79078d7 --- /dev/null +++ b/l10n_ar_txt_mendoza/models/account_journal.py @@ -0,0 +1,252 @@ +from odoo import models, fields, _ +from odoo.exceptions import ValidationError, RedirectWarning +import re + +######### +# helpers +######### + + +def format_amount(amount, padding=15, decimals=2, sep=""): + if amount < 0: + template = "-{:0>%dd}" % (padding - 1 - len(sep)) + else: + template = "{:0>%dd}" % (padding - len(sep)) + res = template.format( + int(round(abs(amount) * 10**decimals, decimals))) + if sep: + res = "{0}{1}{2}".format(res[:-decimals], sep, res[-decimals:]) + return res + + +def get_line_tax_base(move_line): + return sum(move_line.move_id.line_ids.filtered( + lambda x: move_line.tax_line_id in x.tax_ids).mapped( + 'balance')) + + +def get_pos_and_number(full_number): + """ + Para un numero nos fijamos si hay '-', si hay: + * mas de 1, entonces devolvemos error + * 1, entonces devolvemos las partes (solo parte númerica) + * 0, entonces devolvemos '0' y parte númerica del número que se pasó + """ + args = full_number.split('-') + if len(args) == 1: + # si no hay '-' tomamos punto de venta 0 + return ('0', re.sub('[^0-9]', '', args[0])) + else: + return re.sub('[^0-9]', '', args[0]), re.sub('[^0-9]', '', ''.join(args[1:])) + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + def iibb_aplicado_sircar_files_values(self, move_lines): + """ Especificacion en /doc/sircar, solicitado en ticket 62526. Método heredado de https://github.com/ingadhoc/odoo-argentina-ee/blob/16.0/l10n_ar_account_tax_settlement/models/account_journal.py#L840 + """ + self.ensure_one() + ret = '' + perc = '' + + for line in move_lines.filtered( + lambda x: not x.payment_id and not x.move_id): + raise ValidationError(_( + 'Hay lineas a liquidar que no estan enlazadas a pagos ni ' + 'facturas lo cual es requerido para generar el TXT')) + + line_nbr = 1 + for line in move_lines.filtered('payment_id'): + alicuot_line = line.tax_line_id.get_partner_alicuot( + line.partner_id, line.date) if not line.tax_line_id.withholding_type == 'code' else line.payment_id.alicuota_mendoza * 100 + + if not isinstance(alicuot_line, float) and not alicuot_line: + raise ValidationError(_( + 'No hay alicuota configurada en el partner ' + '"%s" (id: %s)') % ( + line.partner_id.name, line.partner_id.id)) + + payment = line.payment_id + internal_type = line.l10n_latam_document_type_id.internal_type + + # 1 Número de Renglón (único por archivo) + content = [] + content.append('%05d' % line_nbr) + + # 2 Origen del Comprobante + content.append('1') + + # 3 Tipo del Comprobante + if payment.payment_type == 'outbound': + content.append('1') + else: + content.append('2') + + # 4 Número del comprobante + content.append('%012d' % int( + re.sub('[^0-9]', '', line.payment_id.withholding_number or ''))) + + # 5 Cuit del contribuyene + content.append(line.partner_id.ensure_vat()) + + # 6 Fecha de la percepción + content.append( + fields.Date.from_string(line.date).strftime('%d/%m/%Y')) + + # 7 Monto sujeto a percepción + content.append(format_amount( + payment.withholdable_base_amount, 12, 2, '.')) + + # 8 alicuota de la retencion + content.append(format_amount( + alicuot_line.alicuota_retencion if not isinstance(alicuot_line, float) else alicuot_line, 6, 2, '.')) + + # 9 Monto retenido + content.append(format_amount(-line.balance, 12, 2, '.')) + + # 10 Tipo de Régimen de Percepción + # (código correspondiente según tabla definida por la jurisdicción) + if not isinstance(alicuot_line, float) and not alicuot_line.regimen_retencion: + raise ValidationError(_( + 'No hay regimen de retencion configurado para la alícuota' + ' del partner %s') % line.partner_id.name) + elif not line.tax_line_id.codigo_regimen: + raise RedirectWarning( + message=_("El impuesto '%s' not tiene código de regimen en solapa 'Opciones avanzadas' campo 'Codigo de regimen'.", line.tax_line_id.name), + action={ + 'type': 'ir.actions.act_window', + 'res_model': 'account.tax', + 'views': [(False, 'form')], + 'res_id': line.tax_line_id.id, + 'name': _('Tax'), + 'view_mode': 'form', + }, + button_text=_('Editar Impuesto'), + ) + content.append(alicuot_line.regimen_retencion if not isinstance(alicuot_line, float) else line.tax_line_id.codigo_regimen) + + # 11 Jurisdicción: código en Convenio Multilateral de la + # jurisdicción a la cual está presentando la DDJJ + if not line.tax_line_id.jurisdiction_code: + raise ValidationError(_( + 'No hay etiqueta de jurisdicción configurada!')) + + content.append(line.tax_line_id.jurisdiction_code) + + # Tipo registro 2. Provincia Cordoba + if line.tax_line_id.jurisdiction_code in ['904', '914']: + + # 12 Tipo de Operación (1-Efectuada, 2-Anulada, 3-Omitida) + content.append('2' if internal_type == 'credit_note' else '1') + + # 13 Fecha de Emisión de Constancia (en formato dd/mm/aaaa) + content.append(fields.Date.from_string(line.date).strftime('%d/%m/%Y')) + + # 14 Número de Constancia - Numeric(14) + content.append('%014s' % int(re.sub('[^0-9]', '', payment.withholding_number or '0')[:14])) + + # 15 Número de Constancia original (sólo para las Anulaciones –ver códigos por jur-) - Numeric(14) + original_invoice = line.move_id._found_related_invoice() or line.move_id + content.append('%014d' % int(re.sub('[^0-9]', '', original_invoice.document_number or '')) + if internal_type == 'credit_note' else '%014d' % 0) + + ret += ','.join(content) + '\r\n' + line_nbr += 1 + + line_nbr = 1 + for line in move_lines.filtered(lambda x: x.move_id.is_invoice()): + alicuot_line = line.tax_line_id.get_partner_alicuot( + line.partner_id, line.date) + if not alicuot_line: + raise ValidationError(_( + 'No hay alicuota configurada en el partner ' + '"%s" (id: %s)') % ( + line.partner_id.name, line.partner_id.id)) + + + # 1 Número de Renglón (único por archivo) + content = [] + content.append('%05d' % line_nbr) + + letter = line.l10n_latam_document_type_id.l10n_ar_letter + + # 2 Tipo de comprobante + internal_type = line.l10n_latam_document_type_id.internal_type + if internal_type == 'invoice': + tipo_comprobante = letter == 'E' and 5 or 1 + elif internal_type == 'credit_note': + tipo_comprobante = letter == 'E' and 106 or 102 + elif internal_type == 'debit_note': + tipo_comprobante = letter == 'E' and 6 or 2 + elif line.move_id.type == 'out_invoice': + tipo_comprobante = 20 + elif line.move_id.type == 'out_refund': + tipo_comprobante = 120 + else: + raise ValidationError(_('Tipo de comprobante no reconocido')) + content.append('%03d' % tipo_comprobante) + + # 3 Letra del comprobante + content.append(line.l10n_latam_document_type_id.l10n_ar_letter) + + # 4 Número del comprobante + content.append('%012d' % int( + re.sub('[^0-9]', '', line.move_id.l10n_latam_document_number or ''))) + + # 5 Cuit del contribuyene + content.append(line.partner_id.ensure_vat()) + + # 6 Fecha de la percepción + content.append( + fields.Date.from_string(line.date).strftime('%d/%m/%Y')) + + # 7 Monto sujeto a percepción + content.append(format_amount(-get_line_tax_base(line), 12, 2, '.')) + + # 8 alicuota de la percepcion + content.append(format_amount( + alicuot_line.alicuota_percepcion, 6, 2, '.')) + + # 9 Monto percibido + content.append(format_amount(-line.balance, 12, 2, '.')) + + # 10 Tipo de Régimen de Percepción + # (código correspondiente según tabla definida por la jurisdicción) + if not alicuot_line.regimen_percepcion: + raise ValidationError(_( + 'No hay regimen de percepcion configurado para la alícuota' + ' del partner %s') % line.partner_id.name) + content.append(alicuot_line.regimen_percepcion) + + # 11 Jurisdicción: código en Convenio Multilateral de la + # jurisdicción a la cual está presentando la DDJJ + if not line.tax_line_id.jurisdiction_code: + raise ValidationError(_( + 'No hay etiqueta de jurisdicción configurada!')) + + content.append(line.tax_line_id.jurisdiction_code) + + # Tipo registro 2. Provincia Cordoba + if line.tax_line_id.jurisdiction_code in ['904', '914']: + + # 12 Tipo de Operación (1-Efectuada, 2-Anulada, 3-Omitida, 4-Informativa) + content.append('2' if internal_type == 'credit_note' else '1') + + # 13 Número de Constancia original (sólo para 2-Anulaciones) Alfanumérico (14) - ejemplo 1A002311312221 + content.append(self._get_perception_original_invoice_number(line) + if internal_type == 'credit_note' else '%014d' % 0) + + perc += ','.join(content) + '\r\n' + line_nbr += 1 + + return [ + { + 'txt_filename': 'Perc IIBB Aplicadas para SIRCAR.txt', + 'txt_content': perc, + }, + { + 'txt_filename': 'Ret IIBB Aplicadas para SIRCAR.txt', + 'txt_content': ret, + }] + diff --git a/l10n_ar_txt_mendoza/models/account_move.py b/l10n_ar_txt_mendoza/models/account_move.py new file mode 100644 index 00000000..f46db416 --- /dev/null +++ b/l10n_ar_txt_mendoza/models/account_move.py @@ -0,0 +1,15 @@ +from odoo import models, fields, api + + +class AccountMove(models.Model): + _inherit = 'account.move' + + actividades_padron = fields.Many2many( + 'afip.activity', + related='partner_id.actividades_padron', + ) + activities_mendoza_ids = fields.Many2many( + comodel_name='afip.activity', + string="Activities in Mendoza", + domain="[('id', 'in', actividades_padron)]" + ) diff --git a/l10n_ar_txt_mendoza/models/account_payment.py b/l10n_ar_txt_mendoza/models/account_payment.py new file mode 100644 index 00000000..2c44d4aa --- /dev/null +++ b/l10n_ar_txt_mendoza/models/account_payment.py @@ -0,0 +1,8 @@ +from odoo import models, fields, api + + +class AccountPayment(models.Model): + + _inherit = "account.payment" + + alicuota_mendoza = fields.Float(store=True, readonly=True) diff --git a/l10n_ar_txt_mendoza/models/account_payment_group.py b/l10n_ar_txt_mendoza/models/account_payment_group.py new file mode 100644 index 00000000..890d5cbd --- /dev/null +++ b/l10n_ar_txt_mendoza/models/account_payment_group.py @@ -0,0 +1,32 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models, api, fields, _ +from odoo.exceptions import ValidationError + + +class AccountPaymentGroup(models.Model): + + _inherit = "account.payment.group" + + # IMPORTANTE: alicuota_mendoza se guarda al momento de correr el código python del impuesto 'Retención IIBB Mendoza Aplicada' --> payment.write({'alicuota_mendoza': alicuota}). Ver por interfaz. + alicuota_mendoza = fields.Float(help="Guardamos la alícuota para el txt de mendoza.", readonly=True) + + def compute_withholdings(self): + """Para el cálculo de retenciones automáticas de aplicadas de Mendoza siempre tiene que haber una factura vinculada al payment group.""" + tax_group_mendoza_id = self.env.ref('l10n_ar_ux.tax_group_retencion_iibb_za').id + retencion_mdza_aplicada = self.env['account.tax'].with_context(type=None).search([ + ('type_tax_use', '=', self.partner_type), + ('company_id', '=', self.company_id.id), + ('tax_group_id', '=', tax_group_mendoza_id), + ], limit=1) + if retencion_mdza_aplicada and not self.to_pay_move_line_ids: + raise ValidationError('No puede calcular retenciones automáticas de aplicadas de Mendoza si no seleccionó una factura para pagar') + else: + super().compute_withholdings() + + # Agregamos la alícuota de mendoza al payment (es necesario para generar el txt iibb_aplicado_sircar_files_values) + payment_mendoza = self.payment_ids.filtered(lambda x: x.tax_withholding_id.tax_group_id.id == tax_group_mendoza_id and x.tax_withholding_id.withholding_type == 'code' and x.state == 'draft') + if payment_mendoza: + payment_mendoza.alicuota_mendoza = self.alicuota_mendoza diff --git a/l10n_ar_txt_mendoza/models/afip_activity.py b/l10n_ar_txt_mendoza/models/afip_activity.py new file mode 100644 index 00000000..16de27a3 --- /dev/null +++ b/l10n_ar_txt_mendoza/models/afip_activity.py @@ -0,0 +1,35 @@ +from odoo import models, fields +from odoo.exceptions import UserError + + +class AfipActivity(models.Model): + _inherit = "afip.activity" + + alicuota_general = fields.Char() + posee_tasa_cero = fields.Char() + no_posee_certificado_tasa_cero = fields.Char() + + def menor_alicuota(self, actividades_con_alicuota_cero): + """Método utilizado para el cálculo de la menor alícuota a aplicar para 'Retención IIBB Mendoza Aplicada'.""" + aliquots = [] + activity_codes = self.mapped('code') + activities = self.env['afip.activity'].search([('code', 'in', activity_codes)]) + activities_with_aliquots = activities.filtered(lambda x: x.alicuota_general or x.posee_tasa_cero or x.no_posee_certificado_tasa_cero) + if not activities_with_aliquots: + raise UserError('No hay actividades con alícuotas') + actividades_con_alic = activities_with_aliquots.mapped('code') + elementos_no_en_ambas = [activity for activity in activity_codes if activity not in actividades_con_alic] + if elementos_no_en_ambas: + raise UserError('Hay actividades en la factura que no tienen alícuota. Actividades: %s' % (','.join(elementos_no_en_ambas))) + for code in activities_with_aliquots: + if code.alicuota_general: + aliquots.append((code.code, float(code.alicuota_general))) + elif code.posee_tasa_cero and code.code in actividades_con_alicuota_cero: + aliquots.append((code.code, float(code.posee_tasa_cero))) + elif code.no_posee_certificado_tasa_cero: + aliquots.append((code.code, float(code.no_posee_certificado_tasa_cero))) + if not aliquots: + raise UserError('Las actividades incluidas en la factura no tienen alícuota') + # Busco la menor alícuota + min_aliquot = min(aliquots, key=lambda x: x[1]) + return min_aliquot diff --git a/l10n_ar_txt_mendoza/models/res_company.py b/l10n_ar_txt_mendoza/models/res_company.py new file mode 100644 index 00000000..816ba525 --- /dev/null +++ b/l10n_ar_txt_mendoza/models/res_company.py @@ -0,0 +1,39 @@ +from odoo import models, fields, api +import base64 +import csv +from io import StringIO +from odoo.exceptions import UserError + + +class ResCompany(models.Model): + _inherit = "res.company" + + riesgo_fiscal_csv_file = fields.Binary(string="CSV Riesgo Fiscal") + riesgo_fiscal_csv_file_last_update = fields.Datetime( + readonly=True, + ) + + @api.model + def write(self, vals): + if 'riesgo_fiscal_csv_file' in vals: + vals['riesgo_fiscal_csv_file_last_update'] = fields.Datetime.now() + return super(ResCompany, self).write(vals) + + def process_csv_file(self, partner_vat, activity_codes): + if self.riesgo_fiscal_csv_file: + # Decode the base64 file content and parse the CSV + csv_content = base64.b64decode(self.riesgo_fiscal_csv_file) + csv_data = StringIO(csv_content.decode('utf-8')) + reader = csv.reader(csv_data, delimiter=';') + actividades_con_riesgo = [] + actividades_con_alicuota_cero = [] + # Process each row in the CSV + for row in reader: + if partner_vat == row[0] and (row[3] in activity_codes): + if row[6] == 'A': + actividades_con_alicuota_cero.append(row[3]) + if row[7] == 'S': + actividades_con_riesgo.append(row[3]) + return actividades_con_riesgo, actividades_con_alicuota_cero + else: + raise UserError('Debe subir el archivo de riesgo fiscal en la sección de ajustes de contabilidad para calcular la retención automática de Mendoza.') diff --git a/l10n_ar_txt_mendoza/views/account_move_views.xml b/l10n_ar_txt_mendoza/views/account_move_views.xml new file mode 100644 index 00000000..73b0fc71 --- /dev/null +++ b/l10n_ar_txt_mendoza/views/account_move_views.xml @@ -0,0 +1,14 @@ + + + + account.move.form.personalizations + account.move + + + + + + + + + diff --git a/l10n_ar_txt_mendoza/views/account_payment_view.xml b/l10n_ar_txt_mendoza/views/account_payment_view.xml new file mode 100644 index 00000000..39410d51 --- /dev/null +++ b/l10n_ar_txt_mendoza/views/account_payment_view.xml @@ -0,0 +1,13 @@ + + + + account.payment.form + account.payment + + + + + + + + diff --git a/l10n_ar_txt_mendoza/views/afip_activity_view.xml b/l10n_ar_txt_mendoza/views/afip_activity_view.xml new file mode 100644 index 00000000..09d878b8 --- /dev/null +++ b/l10n_ar_txt_mendoza/views/afip_activity_view.xml @@ -0,0 +1,30 @@ + + + + + afip.activity.form + afip.activity + + + + + + + + + + + + afip.activity.tree + afip.activity + + + + + + + + + + + diff --git a/l10n_ar_txt_mendoza/wizard/__init__.py b/l10n_ar_txt_mendoza/wizard/__init__.py new file mode 100644 index 00000000..e5010bbc --- /dev/null +++ b/l10n_ar_txt_mendoza/wizard/__init__.py @@ -0,0 +1,5 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import res_config_settings diff --git a/l10n_ar_txt_mendoza/wizard/res_config_settings.py b/l10n_ar_txt_mendoza/wizard/res_config_settings.py new file mode 100644 index 00000000..99904586 --- /dev/null +++ b/l10n_ar_txt_mendoza/wizard/res_config_settings.py @@ -0,0 +1,13 @@ +from odoo import models, fields + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + riesgo_fiscal_csv_file = fields.Binary( + related='company_id.riesgo_fiscal_csv_file', + readonly=False, + ) + riesgo_fiscal_csv_file_last_update = fields.Datetime( + related='company_id.riesgo_fiscal_csv_file_last_update', + string="Última Modificación" + ) diff --git a/l10n_ar_txt_mendoza/wizard/res_config_settings_views.xml b/l10n_ar_txt_mendoza/wizard/res_config_settings_views.xml new file mode 100644 index 00000000..ad45644d --- /dev/null +++ b/l10n_ar_txt_mendoza/wizard/res_config_settings_views.xml @@ -0,0 +1,33 @@ + + + + + res.config.settings.view.form.inherit.l10n_ar_afip_ws + res.config.settings + + +
+
+
+
+
+
+
+ + + +