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. -->
+
+
diff --git a/view/template_tax_form.xml b/view/template_tax_form.xml
index c6f0ccc..4b1197c 100644
--- a/view/template_tax_form.xml
+++ b/view/template_tax_form.xml
@@ -22,6 +22,8 @@ contains the full copyright notices and license terms. -->
+
+