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 @@ +