diff --git a/aeat.py b/aeat.py
index 2093cce..03596e4 100644
--- a/aeat.py
+++ b/aeat.py
@@ -1,16 +1,26 @@
# -*- coding: utf-8 -*-
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
+
+__all__ = [
+ 'SIIReport',
+ 'SIIReportLine',
+]
+
import unicodedata
from logging import getLogger
from decimal import Decimal
+
from trytond.model import ModelSQL, ModelView, fields, Workflow
from trytond.pyson import Eval
-from trytond.pool import Pool, PoolMeta
+from trytond.pool import Pool
from trytond.transaction import Transaction
-from . import aeat_errors
-__all__ = ['SIIReport', 'SIIReportLine']
+from . import aeat_errors
+from .pyAEATsii import service
+from .pyAEATsii import mapping
+
+
_logger = getLogger(__name__)
_ZERO = Decimal('0.0')
@@ -55,7 +65,8 @@ PARTY_IDENTIFIER_TYPE = [
('04', 'Official Document Emmited by the Country of Residence'),
('05', 'Certificate of fiscal resident'),
('06', 'Other proving document'),
- ]
+ ('07', 'Not on the Census'),
+]
SEND_SPECIAL_REGIME_KEY = [ # L3.1
@@ -105,16 +116,16 @@ RECEIVE_SPECIAL_REGIME_KEY = [
AEAT_COMMUNICATION_STATE = [
(None, ''),
- ('CORRECTO', 'Accepted'),
- ('PARCIALMENTECORRECTO', 'Partial Accepted'),
- ('INCORRECTO', 'Rejected')
+ ('Correcto', 'Accepted'),
+ ('ParcialmenteCorrecto', 'Partially Accepted'),
+ ('Incorrecto', 'Rejected')
]
AEAT_INVOICE_STATE = [
(None, ''),
- ('CORRECTO', 'Accepted'),
- ('ACEPTADOCONERRORES', 'Accepted with Errors'),
- ('INCORRECTO', 'Rejected')
+ ('Correcto', 'Accepted'),
+ ('AceptadoConErrores', 'Accepted with Errors'),
+ ('Incorrecto', 'Rejected')
]
@@ -237,8 +248,9 @@ class SIIReport(Workflow, ModelSQL, ModelView):
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('done', 'Done'),
- ('cancelled', 'Cancelled')
- ], 'State', readonly=True)
+ ('cancelled', 'Cancelled'),
+ ('sent', 'Sent'),
+ ], 'State', readonly=True)
communication_state = fields.Selection( AEAT_COMMUNICATION_STATE,
'Communication State', readonly=True)
@@ -273,7 +285,7 @@ class SIIReport(Workflow, ModelSQL, ModelView):
'icon': 'tryton-ok',
},
'cancel': {
- 'invisible': Eval('state').in_(['cancelled']),
+ 'invisible': Eval('state').in_(['cancelled', 'sent']),
'icon': 'tryton-cancel',
},
'load_invoices': {
@@ -336,12 +348,39 @@ class SIIReport(Workflow, ModelSQL, ModelView):
@ModelView.button
@Workflow.transition('sent')
def send(cls, reports):
- _logger.info(
- 'Sending reports (%s) to AEAT SII',
- ','.join(str(r.id) for r in reports))
- for report in reports:
+
+ def call_aeat(headers, invoices):
with report.company.tmp_ssl_credentials() as (crt, key):
- raise NotImplementedError
+ _logger.debug('Service invoices request: %s', invoices)
+ srv = service.bind_SuministroFactEmitidas(crt, key, test=True)
+ res = srv.SuministroLRFacturasEmitidas(headers, invoices)
+ _logger.debug('Service response: %s', res)
+ return res
+
+ pool = Pool()
+ Company = pool.get('company.company')
+ Invoice = pool.get('account.invoice')
+ company = Company(Transaction().context.get('company'))
+ headers = mapping.get_headers(
+ name=company.party.name, vat=company.party.vat_number,
+ comm_kind='A0')
+ for report in reports:
+ _logger.info('Sending report %s to AEAT SII', report.id)
+ invoices = Invoice.map_to_aeat_sii(
+ line.invoice for line in report.lines)
+ res = call_aeat(headers, invoices)
+ # TODO: assert response lines order matches report line order
+ for (report_line, response_line) in zip(
+ report.lines, res.RespuestaLinea):
+ report_line.state = response_line.EstadoRegistro
+ report_line.communication_code = \
+ response_line.CodigoErrorRegistro
+ report_line.communication_msg = \
+ response_line.DescripcionErrorRegistro
+ report_line.save()
+ report.communication_state = res.EstadoEnvio
+ report.save()
+ _logger.debug('Done sending reports to AEAT SII')
@classmethod
@ModelView.button
@@ -388,8 +427,10 @@ class SIIReportLine(ModelSQL, ModelView):
'aeat.sii.report', 'Issued Report', ondelete='CASCADE')
invoice = fields.Many2One('account.invoice', 'Invoice')
state = fields.Selection(AEAT_INVOICE_STATE, 'State')
- communication_msg = fields.Selection(
- aeat_errors.AEAT_ERRORS, 'Communication Message', readonly=True)
+ communication_code = fields.Selection(
+ aeat_errors.AEAT_ERRORS, 'Communication Code', readonly=True)
+ communication_msg = fields.Char(
+ 'Communication Message', readonly=True)
company = fields.Many2One(
'company.company', 'Company', required=True, select=True)
diff --git a/aeat_errors.py b/aeat_errors.py
index d913bd2..6ab25a9 100644
--- a/aeat_errors.py
+++ b/aeat_errors.py
@@ -1,116 +1,119 @@
AEAT_ERRORS = [
(None, ''),
- (3501, 'Technical error of BBDD'),
- (3500, 'Technical error of BBDD. Error in the Integrity of the Information'),
- (3502, 'Technical error. Failed to get invoice data'),
- (3503, 'Invoice consulted for the provision of Payments / Collections does not exist'),
- (3504, 'Technical error. Failed to get Metall Collection data'),
- (3505, 'Technical error. Error obtaining the data of the Insurance Operation'),
- (4100, 'Error in header. The contents of the IDVersionSii field are invalid.'),
- (4101, 'Error in header. The content of theCommunication field is invalid.'),
- (4102, 'XML does not meet the schema. Required field is missing: XXXX'),
- (4103, 'Unexpected error parsing the XML'),
- (4104, 'Error in header. The value of the NIF field of the Holder block is not identified'),
- (4105, 'Error in the header. The value of the NIFRepresentator field of the Holder block is not identified'),
- (4106, 'Error in date format'),
- (4107, 'Technical error when obtaining the CSV.'),
- (4108, 'The XML root tag does not match the defined schema'),
- (4109, 'NIF is not identified. NIF: XXXX'),
- (4110, 'Failed to get the certificate.'),
- (4111, 'The NIF is in the wrong format.'),
- (4112, 'Technical error checking powers.'),
- (4113, 'Technical error when creating the process.'),
- (4114, 'The holder of the certificate must be the holder of the book of registry, social worker or proxy'),
- (4115, 'Technical error checking Social Collaboration.'),
- (4116, 'The allowed limit of registrations for the block DatosInmueble / DetalleIVA has been exceeded'),
- (4117, 'XML does not meet the schema. The maximum allowable invoice threshold has been exceeded to register.'),
- (4118, "The holder's NIF is not authorized to send information to the system. You must register previously"),
- (4122, "Error in the header. The holder's NIF has an incorrect format."),
- (4123, 'Error in header. The NIFRepresentant has an incorrect format.'),
- (4124, 'Error The address does not correspond to the input file.'),
- (4125, 'XML does not meet the schema. The maximum allowable amount of operations has been exceeded to register.'),
- (1100, "Incorrect value or field type: XXXX"),
- (1101, "Incorrect field code value"),
- (1102, "Field Value Incorrect Period"),
- (1103, "Incorrect IDType field value"),
- (1104, "Incorrect ID field value"),
- (1105, "Field Value NumberFactory Incorrect Enumerator"),
- (1106, "Field Value DateExpeditionFactorMissor Incorrect"),
- (1107, "Field Value Type Invoice Incorrect"),
- (1108, "Field Value Location Property Incorrect"),
- (1109, "Field Value Key Special Regime or Incorrect Transcendence"),
- (1110, "Field Value Payment Medium or Incorrect Collection"),
- (1111, "Field Value Type Incorrect Amending"),
- (1112, "The invoice NIF must be the same as the NIF of the record holder"),
- (1113, "Field Value CauseExcluded Incorrect"),
- (1114, "Incorrect type field value"),
- (1115, "If the invoice has part No Subject must report at least one of the two amounts not subject"),
- (1116, "NIF is not identified. NIF: XXXXX"),
- (1117, "NIF is not identified. NIF: XXXXX. NAME_RACE: YYYYY"),
- (1118, "The Issuer's Country Code and the Counterparty's Country Code do not match"),
- (1119, "The IdType of the Issuer and the Counterpart do not match"),
- (1120, "Issuer and Counterparty ID do not match"),
- (1121, "The Issuer's and Counterpart's NIF do not overlap"),
- (1122, "In the case of a minor, the NIF of the representative must contain value"),
- (1123, "In case of a minor, the NIF of the representative can not coindidir with the NIF of the holder"),
- (1124, "Country Code is mandatory when Type Identification is different from VAT NIF"),
- (1125, "The Expedition Date is greater than the current date"),
- (1126, "Field value Incorrect exercise. This should be the current or previous year"),
- (1127, "Invoice Type of Seats Summary, NumberStockFactorEmisterSummaryFin is undeclared"),
- (1128, "Invoice type is not Seats Summary and has NumSeriesFactorSamplingResumedFin declared"),
- (1129, "The IssuedForColors field only accepts N or S values"),
- (1130, "Field Value TypeCommunication Incorrect."),
- (1131, "Field value IncorrectKeyword."),
- (1132, "Field value StateMember is incorrect."),
- (1133, "Incorrect IDType field value. Must have value 02"),
- (1134, "If the invoice is of the rectifying type, the TypeRectificative field must have value."),
- (1135, "If the invoice is not an amendment type, the TypeRectificative field must have no value."),
- (1136, "The field Invoices should be reported only if the invoice is invoice type issued in Replacement of invoiced and declared simplified invoices."),
- (1137, "If the invoice is not of the Amending or Summary Invoice type, the field Refunded Bills can not be informed."),
- (1138, "If the invoice is an Amending amendment, the rectification amount is mandatory"),
- (1139, "If the invoice is not an Amending Amendment or an Invoice Summary ReCotification block must have no value"),
- (1140, "The transactions may have within the subject part, exempt part and / or non exempt part. So, Only one block or both may appear, but at least one (Exempt and / or Exempt)"),
- (1141, "The operations may have part subject and part not subject. Therefore, only one Block or both, but at least one must appear (Subject and / or No subject)"),
- (1142, "Field Value NumberNumberFactorEmisterRemand Incorrect"),
- (1143, "Field value NIF of block IDFacture with incorrect type"),
- (1144, "The invoice ID and CounterSource fields of the invoice are different"),
- (1145, "Field value Incorrect period. This must be less than or equal to the current period"),
- (1146, "The CodigoPais field indicated for the identification of NIF-IVA does not coincide with the two First characters of ID"),
- (1147, "Error in the IDFactura block. IncorrectNameName field value."),
- (1148, "BreakdownTypeOperation needs at least ProvisionServices or Delivery or both"),
- (1149, "The field ID is not identified"),
- (1150, "The field CodePais indicated does not match the first two digits of the identifier"),
- (1151, "The value 03 can only be entered in the Medium field when the Date of payment / payment '31 -12 'for the year"),
- (1152, "If the field Average has 03 value the field Account_O_Medium can not have value"),
- (1153, "NIF is formatted incorrectly"),
- (1154, "The field MiscellaneousDisitors only accepts N or S values"),
- (1155, "The Cadastral Reference field must be informed whenever the FieldInput field does not Has value 3"),
- (1156, "Field value KeyOperation is not included in the list of allowed values"),
- (1157, "The BreakdownFactory block must have reported at least one of the two blocks InvestmentPayment or Deferred Transfer"),
- (1158, "The Counterpart field must be informed as long as the TypeFactor field has no value F2 or F4"),
- (1159, "The Coupon field only accepts N or S values"),
- (1160, "If the invoice is not of the type Invoice Amending in simplified invoices or summary entry, the Coupon field should have no value"),
- (3000, "Duplicate Invoice"),
- (3001, "Registration is already unsubscribed"),
- (3002, "There is no record"),
- (3003, "Can not Include Charges for Unsubscribed Bills"),
- (3004, "Maximum field size exceeded"),
- (3005, "Collections can not be included if the field KeyRegimenSpecialTotal of the invoice Has a value other than 07"),
- (3006, "Payment of unsubscribed invoices can not be included"),
- (3007, "You can not include payments if the fieldTypeRegistryType of the invoice field Has a value other than 07"),
- (3008, "There is already a Metallic Collection with this Counterpart"),
- (3009, "Duplicate intra-Community operation"),
- (3010, "The Presenter does not have the necessary permissions to update this invoice"),
- (3011, "It is not allowed to modify the Special Regime Key in invoices that contain Collections or Payments"),
- (3012, "There is already an Insurance Operation with this Counterparty"),
- (2000, "If the fieldTypeRegistryType has a value of 12 or 13 the block of DatosInmueble must be informed"),
- (2001, "The base fieldAvailableAvailable on invoices issued should not be reported if the field ClaveRegimenEspecialTrascendencia has a value other than 06"),
- (2002, "Error if SpecialRegimenKeyTransfer different from 12, 13 and the block of Estate"),
- (2003, "Some of the rectified invoices do not exist in the system"),
- (2004, "Technical Error when consulting the list of rectified invoices"),
- (2005, "The BaseAvailableAccount field should not be reported on received invoices if the field ClaveRegimenEspecialTrascendencia has a value other than 06"),
- (2006, "The invoice contains a breakdown at the invoice level when it corresponds to a breakdown at the Transaction, since it is a non-simplified invoice or summary statement and the counterpart contains a IdOtro or a NIF beginning with N"),
- (2007, "The Total Amount field is not more than 6,000"),
- (2008, "The field PercentDeliveryREAGYP should not be reported if the field KeyRegimenSpecialTrading on invoices received has a value other than 02"),
- (2009, "Do not enter the field PaymentCompensationREAGYP if the field KeyRegimenSpecialTrading on invoices received has a value other than 02"),
+ ('3501', 'Technical error of BBDD'),
+ ('3500', 'Technical error of BBDD. Error in the Integrity of the Information'),
+ ('3502', 'Technical error. Failed to get invoice data'),
+ ('3503', 'Invoice consulted for the provision of Payments / Collections does not exist'),
+ ('3504', 'Technical error. Failed to get Metall Collection data'),
+ ('3505', 'Technical error. Error obtaining the data of the Insurance Operation'),
+ ('4100', 'Error in header. The contents of the IDVersionSii field are invalid.'),
+ ('4101', 'Error in header. The content of theCommunication field is invalid.'),
+ ('4102', 'XML does not meet the schema. Required field is missing: XXXX'),
+ ('4103', 'Unexpected error parsing the XML'),
+ ('4104', 'Error in header. The value of the NIF field of the Holder block is not identified'),
+ ('4105', 'Error in the header. The value of the NIFRepresentator field of the Holder block is not identified'),
+ ('4106', 'Error in date format'),
+ ('4107', 'Technical error when obtaining the CSV.'),
+ ('4108', 'The XML root tag does not match the defined schema'),
+ ('4109', 'NIF is not identified. NIF: XXXX'),
+ ('4110', 'Failed to get the certificate.'),
+ ('4111', 'The NIF is in the wrong format.'),
+ ('4112', 'Technical error checking powers.'),
+ ('4113', 'Technical error when creating the process.'),
+ ('4114', 'The holder of the certificate must be the holder of the book of registry, social worker or proxy'),
+ ('4115', 'Technical error checking Social Collaboration.'),
+ ('4116', 'The allowed limit of registrations for the block DatosInmueble / DetalleIVA has been exceeded'),
+ ('4117', 'XML does not meet the schema. The maximum allowable invoice threshold has been exceeded to register.'),
+ ('4118', "The holder's NIF is not authorized to send information to the system. You must register previously"),
+ ('4122', "Error in the header. The holder's NIF has an incorrect format."),
+ ('4123', 'Error in header. The NIFRepresentant has an incorrect format.'),
+ ('4124', 'Error The address does not correspond to the input file.'),
+ ('4125', 'XML does not meet the schema. The maximum allowable amount of operations has been exceeded to register.'),
+ ('1100', "Incorrect value or field type: XXXX"),
+ ('1101', "Incorrect field code value"),
+ ('1102', "Field Value Incorrect Period"),
+ ('1103', "Incorrect IDType field value"),
+ ('1104', "Incorrect ID field value"),
+ ('1105', "Field Value NumberFactory Incorrect Enumerator"),
+ ('1106', "Field Value DateExpeditionInvoiceIssuer Incorrect"),
+ ('1107', "Field Value Type Invoice Incorrect"),
+ ('1108', "Field Value Location Property Incorrect"),
+ ('1109', "Field Value Key Special Regime or Incorrect Transcendence"),
+ ('1110', "Field Value Payment Medium or Incorrect Collection"),
+ ('1111', "Field Value Type Incorrect Amending"),
+ ('1112', "The invoice NIF must be the same as the NIF of the record holder"),
+ ('1113', "Field Value CauseExcluded Incorrect"),
+ ('1114', "Incorrect type field value"),
+ ('1115', "If the invoice has part No Subject must report at least one of the two amounts not subject"),
+ ('1116', "NIF is not identified. NIF: XXXXX"),
+ ('1117', "NIF is not identified. NIF: XXXXX. NAME_RACE: YYYYY"),
+ ('1118', "The Issuer's Country Code and the Counterparty's Country Code do not match"),
+ ('1119', "The IdType of the Issuer and the Counterpart do not match"),
+ ('1120', "Issuer and Counterparty ID do not match"),
+ ('1121', "The Issuer's and Counterpart's NIF do not overlap"),
+ ('1122', "In the case of a minor, the NIF of the representative must contain value"),
+ ('1123', "In case of a minor, the NIF of the representative can not coindidir with the NIF of the holder"),
+ ('1124', "Country Code is mandatory when Type Identification is different from VAT NIF"),
+ ('1125', "The Expedition Date is greater than the current date"),
+ ('1126', "Field value Incorrect exercise. This should be the current or previous year"),
+ ('1127', "Invoice Type of Seats Summary, NumberStockFactorEmisterSummaryFin is undeclared"),
+ ('1128', "Invoice type is not Seats Summary and has NumSeriesFactorSamplingResumedFin declared"),
+ ('1129', "The IssuedForColors field only accepts N or S values"),
+ ('1130', "Field Value TypeCommunication Incorrect."),
+ ('1131', "Field value IncorrectKeyword."),
+ ('1132', "Field value StateMember is incorrect."),
+ ('1133', "Incorrect IDType field value. Must have value 02"),
+ ('1134', "If the invoice is of the rectifying type, the TypeRectificative field must have value."),
+ ('1135', "If the invoice is not an amendment type, the TypeRectificative field must have no value."),
+ ('1136', "The field Invoices should be reported only if the invoice is invoice type issued in Replacement of invoiced and declared simplified invoices."),
+ ('1137', "If the invoice is not of the Amending or Summary Invoice type, the field Refunded Bills can not be informed."),
+ ('1138', "If the invoice is an Amending amendment, the rectification amount is mandatory"),
+ ('1139', "If the invoice is not an Amending Amendment or an Invoice Summary ReCotification block must have no value"),
+ ('1140', "The transactions may have within the subject part, exempt part and / or non exempt part. So, Only one block or both may appear, but at least one (Exempt and / or Exempt)"),
+ ('1141', "The operations may have part subject and part not subject. Therefore, only one Block or both, but at least one must appear (Subject and / or No subject)"),
+ ('1142', "Field Value NumberNumberFactorEmisterRemand Incorrect"),
+ ('1143', "Field value NIF of block IDFacture with incorrect type"),
+ ('1144', "The invoice ID and CounterSource fields of the invoice are different"),
+ ('1145', "Field value Incorrect period. This must be less than or equal to the current period"),
+ ('1146', "The CodigoPais field indicated for the identification of NIF-IVA does not coincide with the two First characters of ID"),
+ ('1147', "Error in the IDFactura block. IncorrectNameName field value."),
+ ('1148', "BreakdownTypeOperation needs at least ProvisionServices or Delivery or both"),
+ ('1149', "The field ID is not identified"),
+ ('1150', "The field CodePais indicated does not match the first two digits of the identifier"),
+ ('1151', "The value 03 can only be entered in the Medium field when the Date of payment / payment '31 -12 'for the year"),
+ ('1152', "If the field Average has 03 value the field Account_O_Medium can not have value"),
+ ('1153', "NIF is formatted incorrectly"),
+ ('1154', "The field MiscellaneousDisitors only accepts N or S values"),
+ ('1155', "The Cadastral Reference field must be informed whenever the FieldInput field does not Has value 3"),
+ ('1156', "Field value KeyOperation is not included in the list of allowed values"),
+ ('1157', "The BreakdownFactory block must have reported at least one of the two blocks InvestmentPayment or Deferred Transfer"),
+ ('1158', "The Counterpart field must be informed as long as the TypeFactor field has no value F2 or F4"),
+ ('1159', "The Coupon field only accepts N or S values"),
+ ('1160', "If the invoice is not of the type Invoice Amending in simplified invoices or summary entry, the Coupon field should have no value"),
+ ('1166', ""),
+ ('1177', "The value of the field XXX is not among the allowed values"),
+ ('3000', "Duplicate Invoice"),
+ ('3001', "Registration is already unsubscribed"),
+ ('3002', "There is no record"),
+ ('3003', "Can not Include Charges for Unsubscribed Bills"),
+ ('3004', "Maximum field size exceeded"),
+ ('3005', "Collections can not be included if the field KeyRegimenSpecialTotal of the invoice Has a value other than 07"),
+ ('3006', "Payment of unsubscribed invoices can not be included"),
+ ('3007', "You can not include payments if the fieldTypeRegistryType of the invoice field Has a value other than 07"),
+ ('3008', "There is already a Metallic Collection with this Counterpart"),
+ ('3009', "Duplicate intra-Community operation"),
+ ('3010', "The Presenter does not have the necessary permissions to update this invoice"),
+ ('3011', "It is not allowed to modify the Special Regime Key in invoices that contain Collections or Payments"),
+ ('3012', "There is already an Insurance Operation with this Counterparty"),
+ ('2000', "If the fieldTypeRegistryType has a value of 12 or 13 the block of DatosInmueble must be informed"),
+ ('2001', "The base fieldAvailableAvailable on invoices issued should not be reported if the field ClaveRegimenEspecialTrascendencia has a value other than 06"),
+ ('2002', "Error if SpecialRegimenKeyTransfer different from 12, 13 and the block of Estate"),
+ ('2003', "Some of the rectified invoices do not exist in the system"),
+ ('2004', "Technical Error when consulting the list of rectified invoices"),
+ ('2005', "The BaseAvailableAccount field should not be reported on received invoices if the field ClaveRegimenEspecialTrascendencia has a value other than 06"),
+ ('2006', "The invoice contains a breakdown at the invoice level when it corresponds to a breakdown at the Transaction, since it is a non-simplified invoice or summary statement and the counterpart contains a IdOtro or a NIF beginning with N"),
+ ('2007', "The Total Amount field is not more than 6,000"),
+ ('2008', "The field PercentDeliveryREAGYP should not be reported if the field KeyRegimenSpecialTrading on invoices received has a value other than 02"),
+ ('2009', "Do not enter the field PaymentCompensationREAGYP if the field KeyRegimenSpecialTrading on invoices received has a value other than 02"),
+ ('2011', "Counterpart NIF is not in the Census"),
]
diff --git a/invoice.py b/invoice.py
index 7207e46..be091fc 100644
--- a/invoice.py
+++ b/invoice.py
@@ -1,5 +1,8 @@
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
+
+from operator import attrgetter
+
from trytond import backend
from trytond.model import ModelSQL, ModelView, fields
from trytond.wizard import Wizard, StateView, StateTransition, Button
@@ -12,6 +15,8 @@ from .aeat import (OPERATION_KEY, BOOK_KEY, SEND_SPECIAL_REGIME_KEY,
RECEIVE_SPECIAL_REGIME_KEY, AEAT_INVOICE_STATE, IVA_SUBJECTED,
EXCEMPTION_CAUSE, INTRACOMUNITARY_TYPE)
+from .pyAEATsii import mapping
+
__all__ = ['Invoice', 'ReasignSIIRecord', 'ReasignSIIRecordStart',
'ReasignSIIRecordEnd']
@@ -107,6 +112,30 @@ class Invoice:
return result
+ @classmethod
+ def map_to_aeat_sii(cls, invoices):
+ mapper = IssuedTrytonInvoiceMapper()
+ return map(mapper.build_request, invoices)
+
+
+class IssuedTrytonInvoiceMapper(mapping.OutInvoiceMapper):
+ year = attrgetter('move.period.fiscalyear.name')
+ period = attrgetter('move.period.start_date.month')
+ nif = attrgetter('company.party.vat_number')
+ serial_number = attrgetter('number')
+ issue_date = attrgetter('invoice_date')
+ invoice_kind = attrgetter('sii_operation_key')
+ specialkey_or_trascendence = attrgetter('sii_issued_key')
+ description = attrgetter('description')
+ not_exempt_kind = attrgetter('sii_subjected')
+ counterpart_name = attrgetter('party.name')
+ counterpart_nif = attrgetter('party.vat_number')
+ counterpart_id_type = attrgetter('party.identifier_type')
+ counterpart_country = attrgetter('party.vat_country')
+ taxes = attrgetter('taxes')
+ tax_rate = attrgetter('tax.rate')
+ tax_base = attrgetter('base')
+ tax_amount = attrgetter('amount')
class ReasignSIIRecordStart(ModelView):
diff --git a/pyAEATsii/__init__.py b/pyAEATsii/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pyAEATsii/mapping.py b/pyAEATsii/mapping.py
new file mode 100644
index 0000000..4c7456d
--- /dev/null
+++ b/pyAEATsii/mapping.py
@@ -0,0 +1,91 @@
+
+__all__ = [
+ 'get_headers',
+ 'OutInvoiceMapper',
+]
+
+
+def get_headers(name=None, vat=None, comm_kind=None, version='0.6'):
+ return {
+ 'IDVersionSii': version,
+ 'Titular': {
+ 'NombreRazon': name,
+ 'NIF': vat,
+ },
+ 'TipoComunicacion': comm_kind,
+ }
+
+
+class OutInvoiceMapper(object):
+ def __init__(self):
+ pass
+
+ def build_request(self, invoice):
+ return {
+ 'PeriodoImpositivo': self.build_period(invoice),
+ 'IDFactura': self.build_invoice_id(invoice),
+ 'FacturaExpedida': self.build_issued_invoice(invoice),
+ }
+
+ def build_period(self, invoice):
+ return {
+ 'Ejercicio': self.year(invoice),
+ 'Periodo': str(self.period(invoice)).zfill(2),
+ }
+
+ def build_invoice_id(self, invoice):
+ return {
+ 'IDEmisorFactura': {
+ 'NIF': self.nif(invoice),
+ },
+ 'NumSerieFacturaEmisor': self.serial_number(invoice),
+ 'FechaExpedicionFacturaEmisor':
+ self.issue_date(invoice).strftime('%d/%m/%Y'),
+ }
+
+ def build_issued_invoice(self, invoice):
+ ret = {
+ 'TipoFactura': self.invoice_kind(invoice),
+ 'ClaveRegimenEspecialOTrascendencia':
+ self.specialkey_or_trascendence(invoice),
+ 'DescripcionOperacion': self.description(invoice),
+ 'TipoDesglose': {
+ 'DesgloseFactura': {
+ 'Sujeta': {
+ # 'Exenta': {
+ # 'BaseImponible': '0.00',
+ # },
+ 'NoExenta': {
+ 'TipoNoExenta': self.not_exempt_kind(invoice),
+ 'DesgloseIVA': {
+ 'DetalleIVA':
+ map(self.build_taxes, self.taxes(invoice)),
+ }
+ },
+ },
+ # 'NoSujeta': {
+ # },
+ },
+ },
+ }
+ if ret['TipoFactura'] not in {'F2', 'F4', 'R5'}:
+ ret['Contraparte'] = self.build_counterpart(invoice)
+ return ret
+
+ def build_counterpart(self, invoice):
+ return {
+ 'NombreRazon': self.counterpart_name(invoice),
+ # 'NIF': self.counterpart_nif(invoice),
+ 'IDOtro': {
+ 'IDType': self.counterpart_id_type(invoice),
+ 'CodigoPais': self.counterpart_country(invoice),
+ 'ID': self.counterpart_nif(invoice),
+ },
+ }
+
+ def build_taxes(self, tax):
+ return {
+ 'TipoImpositivo': int(100 * self.tax_rate(tax)),
+ 'BaseImponible': self.tax_base(tax),
+ 'CuotaRepercutida': self.tax_amount(tax),
+ }
diff --git a/pyAEATsii/plugins.py b/pyAEATsii/plugins.py
new file mode 100644
index 0000000..6792a8e
--- /dev/null
+++ b/pyAEATsii/plugins.py
@@ -0,0 +1,27 @@
+
+__all__ = [
+ 'LoggingPlugin',
+]
+
+from logging import getLogger
+from lxml import etree
+from zeep import Plugin
+
+_logger = getLogger(__name__)
+
+
+class LoggingPlugin(Plugin):
+
+ def ingress(self, envelope, http_headers, operation):
+ _logger.debug('http_headers: %s', http_headers)
+ _logger.debug('operation: %s', operation)
+ _logger.debug('envelope: %s', etree.tostring(
+ envelope, pretty_print=True))
+ return envelope, http_headers
+
+ def egress(self, envelope, http_headers, operation, binding_options):
+ _logger.debug('http_headers: %s', http_headers)
+ _logger.debug('operation: %s', operation)
+ _logger.debug('envelope: %s', etree.tostring(
+ envelope, pretty_print=True))
+ return envelope, http_headers
diff --git a/pyAEATsii/service.py b/pyAEATsii/service.py
new file mode 100644
index 0000000..bc0bc5f
--- /dev/null
+++ b/pyAEATsii/service.py
@@ -0,0 +1,47 @@
+
+__all__ = [
+ 'bind_SuministroFactEmitidas',
+]
+
+from requests import Session
+
+from zeep import Client
+from zeep.transports import Transport
+from zeep.plugins import HistoryPlugin
+
+from .plugins import LoggingPlugin
+
+
+def _get_client(wsdl, public_crt, private_key, test=False):
+ session = Session()
+ session.cert = (public_crt, private_key)
+ transport = Transport(session=session)
+ plugins = [HistoryPlugin()]
+ # TODO: manually handle sessionId? Not mandatory yet recommended...
+ # http://www.agenciatributaria.es/AEAT.internet/Inicio/Ayuda/Modelos__Procedimientos_y_Servicios/Ayuda_P_G417____IVA__Llevanza_de_libros_registro__SII_/Ayuda_tecnica/Informacion_tecnica_SII/Preguntas_tecnicas_frecuentes/1__Cuestiones_Generales/16___Como_se_debe_utilizar_el_dato_sesionId__.shtml
+ if test:
+ plugins.append(LoggingPlugin())
+ client = Client(wsdl=wsdl, transport=transport, plugins=plugins)
+ return client
+
+
+def bind_SuministroFactEmitidas(crt, pkey, test=False):
+ wsdl = (
+ 'http://www.agenciatributaria.es/static_files/AEAT/'
+ 'Contenidos_Comunes/La_Agencia_Tributaria/Modelos_y_formularios/'
+ 'Suministro_inmediato_informacion/FicherosSuministros/V_06/'
+ 'SuministroFactEmitidas.wsdl'
+ )
+ port_name = 'SuministroFactEmitidas'
+ if test:
+ port_name += 'Pruebas'
+ cli = _get_client(wsdl, crt, pkey, test)
+ service = cli.bind('siiService', port_name)
+ return service
+
+ # wsdl_in = fields.Char(
+ # string='WSDL Invoice In', required=True,
+ # default='http://www.agenciatributaria.es/static_files/AEAT/'
+ # 'Contenidos_Comunes/La_Agencia_Tributaria/Modelos_y_formularios/'
+ # 'Suministro_inmediato_informacion/FicherosSuministros/V_06/'
+ # 'SuministroFactRecibidas.wsdl')
diff --git a/setup.py b/setup.py
index a7dfc57..aa20509 100644
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@ major_version, minor_version, _ = version.split('.', 2)
major_version = int(major_version)
minor_version = int(minor_version)
-requires = ['cryptography', 'pyOpenSSL']
+requires = ['cryptography', 'pyOpenSSL', 'zeep', 'vatnumber']
for dep in info.get('depends', []):
if not re.match(r'(ir|res|webdav)(\W|$)', dep):
prefix = MODULE2PREFIX.get(dep, 'trytond')
@@ -59,6 +59,7 @@ setup(name='%s_%s' % (PREFIX, MODULE),
packages=[
'trytond.modules.%s' % MODULE,
'trytond.modules.%s.tests' % MODULE,
+ 'trytond.modules.%s.pyAEATsii' % MODULE,
],
package_data={
'trytond.modules.%s' % MODULE: (info.get('xml', [])
diff --git a/view/sii_report_lines_list.xml b/view/sii_report_lines_list.xml
index a299d5b..7ecd840 100644
--- a/view/sii_report_lines_list.xml
+++ b/view/sii_report_lines_list.xml
@@ -5,5 +5,6 @@
+