diff --git a/account.py b/account.py index 1e047df..2156c32 100644 --- a/account.py +++ b/account.py @@ -1,10 +1,9 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. from trytond.model import fields -from trytond.pool import PoolMeta +from trytond.pool import PoolMeta, Pool from trytond.pyson import Eval from trytond.transaction import Transaction -from trytond.tools import cached_property from .aeat import (BOOK_KEY, OPERATION_KEY, SEND_SPECIAL_REGIME_KEY, RECEIVE_SPECIAL_REGIME_KEY, IVA_SUBJECTED, EXEMPTION_CAUSE, IVA_NOT_SUBJECTED) @@ -66,6 +65,13 @@ class TemplateTax(metaclass=PoolMeta): sii_exemption_cause = fields.Selection(EXEMPTION_CAUSE, 'Exemption Cause') sii_not_subjected_key = fields.Selection(IVA_NOT_SUBJECTED, 'Not Subjected Key') + sii_product_type = fields.Selection([ + (None, ''), + ('goods', 'Goods'), + ('services', 'Services')], 'SII Product Type', + states={ + 'invisible': Eval('_parent_group', {}).get('kind') == 'purchase' + }, depends=['group']) tax_used = fields.Boolean('Used in Tax') invoice_used = fields.Boolean('Used in invoice Total') @@ -100,6 +106,10 @@ class TemplateTax(metaclass=PoolMeta): return res + @staticmethod + def default_sii_product_type(): + return 'goods' + class Tax(metaclass=PoolMeta): __name__ = 'account.tax' @@ -113,6 +123,12 @@ class Tax(metaclass=PoolMeta): sii_not_subjected_key = fields.Selection(IVA_NOT_SUBJECTED, 'Not Subjected Key') sii_exemption_cause = fields.Selection(EXEMPTION_CAUSE, 'Exemption Cause') + sii_product_type = fields.Selection([ + ('goods', 'Goods'), + ('services', 'Services')], 'SII Product Type', + states={ + 'invisible': Eval('_parent_group', {}).get('kind') == 'purchase' + }, depends=['group']) tax_used = fields.Boolean('Used in Tax') invoice_used = fields.Boolean('Used in invoice Total') recargo_equivalencia = fields.Boolean('Recargo Equivalencia', @@ -150,3 +166,18 @@ class Tax(metaclass=PoolMeta): @staticmethod def default_deducible(): return True + + @staticmethod + def default_sii_product_type(): + return 'goods' + + @property + def sii_product_type_used(self): + ModelData = Pool().get('ir.model.data') + try: + if self.group.id == ModelData.get_id( + 'account_es', 'tax_group_sale_service'): + return 'services' + return 'goods' + except KeyError: + return self.sii_product_type diff --git a/aeat_mapping.py b/aeat_mapping.py index d7ca39b..046e3ef 100644 --- a/aeat_mapping.py +++ b/aeat_mapping.py @@ -2,6 +2,7 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. from decimal import Decimal +from itertools import groupby from logging import getLogger from operator import attrgetter from datetime import date @@ -317,89 +318,101 @@ class IssuedInvoiceMapper(BaseInvoiceMapper): 'IDOtro' in ret['Contraparte'] or ('NIF' in ret['Contraparte'] and ret['Contraparte']['NIF'].startswith('N'))) ) - detail = { - 'Sujeta': {}, - 'NoSujeta': {} - } - if must_detail_op: - ret['TipoDesglose'].update({ - 'DesgloseTipoOperacion': { - 'Entrega': detail, - # 'PrestacionDeServicios': {}, - } - }) - else: - ret['TipoDesglose'].update({ - 'DesgloseFactura': detail - }) - for tax in self.taxes(invoice): - exempt_kind = self.exempt_kind(tax.tax) - not_exempt_kind = self.not_exempt_kind(tax.tax) - not_subject_kind = self.not_subject_kind(tax.tax) - if (not_exempt_kind in ('S2', 'S3') and - 'NIF' not in ret.get('Contraparte', {})): - raise UserError(gettext('aeat_sii.msg_missing_nif', - invoice=invoice)) + def get_tax_grouping(tax): + # TODO: add all fields to group taxes once + if not must_detail_op: + return '' + return tax.tax.sii_product_type_used - if not_exempt_kind: - if not_exempt_kind == 'S2': - # inv. subj. pass. - tax_detail = { - 'TipoImpositivo': 0, - 'BaseImponible': self.get_tax_base(tax), - 'CuotaRepercutida': 0 - } - else: - tax_detail = self.build_taxes(tax) - if tax_detail: - if not detail['Sujeta']: - detail['Sujeta'].update({ - 'NoExenta': { - 'TipoNoExenta': not_exempt_kind, - 'DesgloseIVA': { - 'DetalleIVA': [tax_detail] - } - } - }) - else: - detail['Sujeta']['NoExenta']['DesgloseIVA'][ - 'DetalleIVA'].append(tax_detail) - elif exempt_kind: - baseimponible = self.get_tax_base(tax) - if detail['Sujeta'].get('Exenta', {}).get( - 'DetalleExenta', {}).get( - 'CausaExencion', None) == exempt_kind: - baseimponible += detail['Sujeta'].get('Exenta').get( - 'DetalleExenta').get('BaseImponible', 0) - detail['Sujeta'].update({ - 'Exenta': { - 'DetalleExenta': { - 'CausaExencion': exempt_kind, - 'BaseImponible': baseimponible + taxes = sorted(self.taxes(invoice), key=get_tax_grouping) + for product_type, grouped_taxes in groupby(taxes, + key=get_tax_grouping): + grouped_taxes = list(grouped_taxes) + detail = { + 'Sujeta': {}, + 'NoSujeta': {} + } + + for tax in grouped_taxes: + exempt_kind = self.exempt_kind(tax.tax) + not_exempt_kind = self.not_exempt_kind(tax.tax) + not_subject_kind = self.not_subject_kind(tax.tax) + if (not_exempt_kind in ('S2', 'S3') and + 'NIF' not in ret.get('Contraparte', {})): + raise UserError(gettext('aeat_sii.msg_missing_nif', + invoice=invoice)) + + if not_exempt_kind: + if not_exempt_kind == 'S2': + # inv. subj. pass. + tax_detail = { + 'TipoImpositivo': 0, + 'BaseImponible': self.get_tax_base(tax), + 'CuotaRepercutida': 0 } - } - }) - elif not_subject_kind: - if self.art_7_14(tax.tax): - detail['NoSujeta'].setdefault( - 'ImportePorArticulos7_14_Otros', 0) - detail['NoSujeta']['ImportePorArticulos7_14_Otros' - ] += self.get_tax_base(tax) - elif self.location_rules(tax.tax, must_detail_op): - detail['NoSujeta'].setdefault( - 'ImporteTAIReglasLocalizacion', 0) - detail['NoSujeta']['ImporteTAIReglasLocalizacion' - ] += self.get_tax_base(tax) + else: + tax_detail = self.build_taxes(tax) + if tax_detail: + if not detail['Sujeta']: + detail['Sujeta'].update({ + 'NoExenta': { + 'TipoNoExenta': not_exempt_kind, + 'DesgloseIVA': { + 'DetalleIVA': [tax_detail] + } + } + }) + else: + detail['Sujeta']['NoExenta']['DesgloseIVA'][ + 'DetalleIVA'].append(tax_detail) + elif exempt_kind: + baseimponible = self.get_tax_base(tax) + if detail['Sujeta'].get('Exenta', {}).get( + 'DetalleExenta', {}).get( + 'CausaExencion', None) == exempt_kind: + baseimponible += detail['Sujeta'].get('Exenta').get( + 'DetalleExenta').get('BaseImponible', 0) + detail['Sujeta'].update({ + 'Exenta': { + 'DetalleExenta': { + 'CausaExencion': exempt_kind, + 'BaseImponible': baseimponible + } + } + }) + elif not_subject_kind: + if self.art_7_14(tax.tax): + detail['NoSujeta'].setdefault( + 'ImportePorArticulos7_14_Otros', 0) + detail['NoSujeta']['ImportePorArticulos7_14_Otros' + ] += self.get_tax_base(tax) + elif self.location_rules(tax.tax, must_detail_op): + detail['NoSujeta'].setdefault( + 'ImporteTAIReglasLocalizacion', 0) + detail['NoSujeta']['ImporteTAIReglasLocalizacion' + ] += self.get_tax_base(tax) + else: + raise NotImplementedError() else: raise NotImplementedError() - else: - raise NotImplementedError() - # remove unused key - for key in ('Sujeta', 'NoSujeta'): - if not detail[key]: - detail.pop(key) + # remove unused key + for key in ('Sujeta', 'NoSujeta'): + if not detail[key]: + detail.pop(key) + + detail_type = 'PrestacionServicios' \ + if product_type == 'services' else 'Entrega' + if must_detail_op: + ret['TipoDesglose'].setdefault('DesgloseTipoOperacion', {}) + ret['TipoDesglose']['DesgloseTipoOperacion'].update({ + detail_type: detail.copy(), + }) + else: + ret['TipoDesglose'].update({ + 'DesgloseFactura': detail.copy() + }) self._update_total_amount(ret, invoice) self._update_rectified_invoice(ret, invoice) diff --git a/invoice.py b/invoice.py index 504dc66..16c51ab 100644 --- a/invoice.py +++ b/invoice.py @@ -278,7 +278,7 @@ class Invoice(metaclass=PoolMeta): Modeldata.get_id('account_es', 'iva_reagp_12_normal'), Modeldata.get_id('account_es', 'iva_reagp_12_pyme'), ] - except AttributeError: + except KeyError: reagyp_ids = [ Modeldata.get_id('account_es', 'iva_reagp_compras_12_1') ] diff --git a/locale/es.po b/locale/es.po index c5ba20f..3056950 100644 --- a/locale/es.po +++ b/locale/es.po @@ -90,6 +90,18 @@ msgctxt "field:account.tax,sii_not_subjected_key:" msgid "Not Subjected Key" msgstr "Clave no sujeto" +msgctxt "field:account.tax,sii_product_type:" +msgid "SII Product Type" +msgstr "Tipo operación SII" + +msgctxt "selection:account.tax,sii_product_type:" +msgid "Goods" +msgstr "Entrega de bienes" + +msgctxt "selection:account.tax,sii_product_type:" +msgid "Services" +msgstr "Prestación de servicios" + msgctxt "field:account.tax.template,invoice_used:" msgid "Used in invoice Total" msgstr "Usado en total de factura" @@ -126,6 +138,18 @@ msgctxt "field:account.tax.template,tax_used:" msgid "Used in Tax" msgstr "Usado en impuestos" +msgctxt "field:account.tax.template,sii_product_type:" +msgid "SII Product Type" +msgstr "Tipo operación SII" + +msgctxt "selection:account.tax.template,sii_product_type:" +msgid "Goods" +msgstr "Entrega de bienes" + +msgctxt "selection:account.tax.template,sii_product_type:" +msgid "Services" +msgstr "Prestación de servicios" + msgctxt "field:aeat.sii.load_pkcs12.start,password:" msgid "Password" msgstr "Contraseña" diff --git a/sii.xml b/sii.xml index f38c06c..b09c63c 100644 --- a/sii.xml +++ b/sii.xml @@ -111,6 +111,7 @@ this repository contains the full copyright notices and license terms. --> E6 + services E @@ -459,6 +460,7 @@ this repository contains the full copyright notices and license terms. --> E1 + services E diff --git a/view/tax_form.xml b/view/tax_form.xml index c6f0ccc..4b1197c 100644 --- a/view/tax_form.xml +++ b/view/tax_form.xml @@ -22,6 +22,8 @@ contains the full copyright notices and license terms. -->