From 61689ee2859cbfc2e38ee5bdca71a3b4d49e404e Mon Sep 17 00:00:00 2001 From: Sergio Morillo Date: Mon, 11 Jul 2022 16:55:12 +0200 Subject: [PATCH] Add core taxes. Add reagyp functionality. --- account.py | 2 + aeat.py | 37 ++++- invoice.py | 36 +++++ locale/es.po | 16 ++- message.xml | 16 ++- sii_core.xml | 222 +++++++++++++++++++++++++++++ tests/scenario_aeat_sii_reagyp.rst | 209 +++++++++++++++++++++++++++ tests/test_aeat_sii.py | 5 + tryton.cfg | 5 +- view/configuration_form.xml | 2 + 10 files changed, 540 insertions(+), 10 deletions(-) create mode 100644 sii_core.xml create mode 100644 tests/scenario_aeat_sii_reagyp.rst diff --git a/account.py b/account.py index 6f6c3c9..6a56971 100644 --- a/account.py +++ b/account.py @@ -33,6 +33,8 @@ class Configuration(metaclass=PoolMeta): domain=['OR', [('max_sii_lines', '=', None)], [('max_sii_lines', '>', 0)]]) + sii_reagyp_invoice_paid = fields.Boolean('Load SII REAGYP when paid', + help='Load REAGYP invoices on SII book only when they are paid') @staticmethod def default_aeat_pending_sii(): diff --git a/aeat.py b/aeat.py index 0da5d5b..d51f0ea 100644 --- a/aeat.py +++ b/aeat.py @@ -16,7 +16,7 @@ from trytond.pool import Pool from trytond.transaction import Transaction from trytond.config import config from trytond.i18n import gettext -from trytond.exceptions import UserError +from trytond.exceptions import UserError, UserWarning from trytond.tools import grouped_slice, reduce_ids from . import tools from . import service @@ -423,6 +423,7 @@ class SIIReport(Workflow, ModelSQL, ModelView): for report in reports: report.check_invoice_state() report.check_duplicate_invoices() + report.check_reagyp_invoices() @classmethod @ModelView.button @@ -481,12 +482,38 @@ class SIIReport(Workflow, ModelSQL, ModelView): 'aeat_sii.msg_report_wrong_invoice_state', invoice=line.invoice.rec_name)) + def check_reagyp_invoices(self): + pool = Pool() + Invoice = pool.get('account.invoice') + Warning = pool.get('res.user.warning') + + to_warn = [] + invoices = [ + l.invoice for l in self.lines if l.invoice] + invoices = [invoice_id for invoice_id, value + in Invoice.get_is_reagyp(invoices).items() if value] + + invoices = Invoice.browse(invoices) + for invoice in invoices: + if invoice.state != 'paid': + to_warn.append(invoice) + if to_warn: + key = 'aeat_sii.msg_report_unpaid_reagyp_invoice_%s' % '_'.join([ + str(invoice.id) for invoice in to_warn]) + if Warning.check(key): + raise UserWarning(key, gettext( + 'aeat_sii.msg_report_unpaid_reagyp_invoice', + invoices=', '.join([ + invoice.rec_name for invoice in to_warn]))) + @classmethod @ModelView.button def load_invoices(cls, reports): pool = Pool() Invoice = pool.get('account.invoice') ReportLine = pool.get('aeat.sii.report.lines') + Conf = pool.get('account.configuration') + conf = Conf(1) to_create = [] for report in reports: @@ -513,9 +540,15 @@ class SIIReport(Workflow, ModelSQL, ModelView): _logger.debug('Searching invoices for SII report: %s', domain) - for invoice in Invoice.search(domain): + invoices = Invoice.search(domain) + reagyp = Invoice.get_is_reagyp(invoices) + for invoice in invoices: if not all(x.report != report for x in invoice.sii_records): continue + if (conf.sii_reagyp_invoice_paid and reagyp[invoice.id] + and invoice.state != 'paid'): + continue + to_create.append({ 'report': report, 'invoice': invoice, diff --git a/invoice.py b/invoice.py index 7341642..504dc66 100644 --- a/invoice.py +++ b/invoice.py @@ -47,6 +47,7 @@ class Invoice(metaclass=PoolMeta): sii_pending_sending = fields.Boolean('SII Pending Sending Pending', readonly=True) sii_header = fields.Text('Header') + is_reagyp = fields.Function(fields.Boolean('Is REAGYP'), 'get_is_reagyp') @classmethod def __setup__(cls): @@ -256,6 +257,41 @@ class Invoice(metaclass=PoolMeta): header = mapper.build_delete_request(invoice) return header + @classmethod + def set_number(cls, invoices): + super().set_number(invoices) + + is_reagyp = cls.get_is_reagyp(invoices) + for invoice in invoices: + if not invoice.reference and is_reagyp[invoice.id]: + invoice.reference = invoice.number + cls.save(invoices) + + @classmethod + def get_is_reagyp(cls, invoices, name=None): + pool = Pool() + Modeldata = pool.get('ir.model.data') + + res = {} + try: + reagyp_ids = [ + Modeldata.get_id('account_es', 'iva_reagp_12_normal'), + Modeldata.get_id('account_es', 'iva_reagp_12_pyme'), + ] + except AttributeError: + reagyp_ids = [ + Modeldata.get_id('account_es', 'iva_reagp_compras_12_1') + ] + for invoice in invoices: + res[invoice.id] = False + if not invoice.taxes or invoice.type != 'in': + continue + for tax in invoice.taxes: + if tax.tax.template and tax.tax.template.id in reagyp_ids: + res[invoice.id] = True + break + return res + class ResetSIIKeysStart(ModelView): """ diff --git a/locale/es.po b/locale/es.po index 7f2439b..c5ba20f 100644 --- a/locale/es.po +++ b/locale/es.po @@ -1874,4 +1874,18 @@ msgstr "Máximo líneas SII" msgctxt "help:account.configuration,max_sii_lines:" msgid "Indicates the maximum number of invoices to be included in each SII book." -msgstr "Indica el número máximo de facturas a incluir por libro del SII." \ No newline at end of file +msgstr "Indica el número máximo de facturas a incluir por libro del SII." + +msgctxt "model:ir.message,text:msg_report_unpaid_reagyp_invoice" +msgid "" +"There are unpaid invoices with REAGYP taxes in SII Book to confirm:\n%(invoices)s." +msgstr "" +"Existen facturas sin pagar con impuestos REAGYP en el libro SII a confirmar:\n%(invoices)s." + +msgctxt "field:account.configuration,sii_reagyp_invoice_paid:" +msgid "Load SII REAGYP when paid" +msgstr "Cargar REAGYP en SII cuando pagadas" + +msgctxt "help:account.configuration,sii_reagyp_invoice_paid:" +msgid "Load REAGYP invoices on SII book only when they are paid" +msgstr "Carga facturas REAGYP en libro SII solo cuando estan pagadas." \ No newline at end of file diff --git a/message.xml b/message.xml index 073dfde..8d989fe 100644 --- a/message.xml +++ b/message.xml @@ -23,12 +23,12 @@ If you edit them take care if you need to update again to SII Unable to load PKCS12: %(message)s - - There are some reports in draft and/or confirmed. Any report won't be created until this reports will be processed. - - - %(message)s - + + There are some reports in draft and/or confirmed. Any report won't be created until this reports will be processed. + + + %(message)s + The party of the invoice %(invoice)s must has a valid Tax Identifier. @@ -53,5 +53,9 @@ If you edit them take care if you need to update again to SII Invoice "%(invoice)s" is duplicated on SII Report "%(report)s". + + There are unpaid invoices with REAGYP taxes in SII Book to confirm: +%(invoices)s. + diff --git a/sii_core.xml b/sii_core.xml new file mode 100644 index 0000000..05eadb3 --- /dev/null +++ b/sii_core.xml @@ -0,0 +1,222 @@ + + + + + + R + 01 + S1 + + + + + R + 01 + S1 + + + + + R + 01 + S1 + + + + + R + 01 + S1 + + + + + R + 01 + S1 + + + + + R + 13 + E6 + + + + + R + 13 + E6 + + + + + R + 13 + E6 + + + + + E + 02 + E2 + + + + + E + 02 + E2 + + + + + E + 01 + E1 + + + + + E + 01 + E1 + + + + + + E + 01 + E5 + + + + + E + 01 + E5 + + + + + + R + 09 + S1 + + + + + R + 09 + S1 + + + + + + R + 09 + E6 + + + + + R + 09 + S1 + + + + + R + 09 + S1 + + + + + R + 09 + S1 + + + + + + R + 01 + E1 + + + + + R + 01 + E1 + + + + + R + 01 + E6 + + + + + E + 01 + S1 + + + + + E + 01 + S1 + + + + + E + 01 + S1 + + + + + E + 01 + S1 + + + + + E + 01 + S1 + + + + + + R + 02 + S1 + + + + + R + 02 + S1 + + + + + diff --git a/tests/scenario_aeat_sii_reagyp.rst b/tests/scenario_aeat_sii_reagyp.rst new file mode 100644 index 0000000..15a583b --- /dev/null +++ b/tests/scenario_aeat_sii_reagyp.rst @@ -0,0 +1,209 @@ +=================================== +Account Es Reagyp AEAT SII Scenario +=================================== + +Imports:: + >>> import datetime + >>> from dateutil.relativedelta import relativedelta + >>> from decimal import Decimal + >>> from operator import attrgetter + >>> from proteus import config, Model, Wizard + >>> from trytond.tests.tools import activate_modules + >>> from trytond.modules.company.tests.tools import create_company, \ + ... get_company + >>> from trytond.modules.account.tests.tools import create_fiscalyear, \ + ... create_chart, get_accounts, create_tax, create_tax_code + >>> from trytond.modules.account_invoice.tests.tools import \ + ... set_fiscalyear_invoice_sequences + >>> today = datetime.date.today() + +Install modules:: + + >>> config = activate_modules(['aeat_sii']) + + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + + +Create fiscal year:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences( + ... create_fiscalyear(company)) + >>> fiscalyear.click('create_period') + >>> Period = Model.get('account.period') + >>> period, = Period.find([ + ... ('start_date', '>=', today.replace(day=1)), + ... ('end_date', '<=', today.replace(day=1) + relativedelta(months=+1)), + ... ], limit=1) + + +Create chart of accounts:: + + >>> AccountTemplate = Model.get('account.account.template') + >>> Account = Model.get('account.account') + >>> ModelData = Model.get('ir.model.data') + >>> data, = ModelData.find([ + ... ('module', '=', 'account_es'), + ... ('fs_id', '=', 'pgc_0_normal'), + ... ], limit=1) + >>> account_template = AccountTemplate(data.db_id) + >>> create_chart = Wizard('account.create_chart') + >>> create_chart.execute('account') + >>> create_chart.form.account_template = account_template + >>> create_chart.form.company = company + >>> create_chart.execute('create_account') + >>> accounts = {} + >>> for kind in ['receivable', 'payable', 'revenue', 'expense']: + ... accounts[kind], = Account.find([ + ... ('type.%s' % kind, '=', True), + ... ('company', '=', company.id), + ... ], limit=1) + >>> expense = accounts['expense'] + >>> revenue = accounts['revenue'] + >>> create_chart.form.account_receivable = accounts['receivable'] + >>> create_chart.form.account_payable = accounts['payable'] + >>> create_chart.execute('create_properties') + + +Create party:: + + >>> Party = Model.get('party.party') + >>> party = Party(name='Party') + >>> party.save() + + >>> TaxRule = Model.get('account.tax.rule') + >>> tax_rule, = TaxRule.find([ + ... ('company', '=', company.id), + ... ('kind', '=', 'purchase'), + ... ('name', 'ilike', '%especial agr%'), + ... ]) + >>> party_reagyp = Party(name='Party REAGYP') + >>> party_reagyp.supplier_tax_rule = tax_rule + >>> party_reagyp.save() + + +Create account category:: + + >>> Tax = Model.get('account.tax') + >>> customer_tax, = Tax.find([ + ... ('company', '=', company.id), + ... ('description', 'like', '%IVA 21%'), + ... ('group.kind', '=', 'sale'), + ... ], limit=1) + >>> supplier_tax, = Tax.find([ + ... ('company', '=', company.id), + ... ('description', 'like', '%IVA 21%'), + ... ('group.kind', '=', 'purchase'), + ... ], limit=1) + >>> ProductCategory = Model.get('product.category') + >>> account_category = ProductCategory(name="Account Category") + >>> account_category.accounting = True + >>> account_category.account_expense = expense + >>> account_category.account_revenue = revenue + >>> account_category.customer_taxes.append(customer_tax) + >>> account_category.supplier_taxes.append(supplier_tax) + >>> account_category.save() + + +Create product:: + + >>> ProductUom = Model.get('product.uom') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> ProductTemplate = Model.get('product.template') + >>> Product = Model.get('product.product') + >>> product = Product() + >>> template = ProductTemplate() + >>> template.name = 'product' + >>> template.default_uom = unit + >>> template.type = 'service' + >>> template.list_price = Decimal('40') + >>> template.cost_price = Decimal('25') + >>> template.account_category = account_category + >>> template.save() + >>> product, = template.products + + +Create invoices:: + + >>> Invoice = Model.get('account.invoice') + >>> invoice = Invoice(type='in') + >>> invoice.party = party + >>> invoice.invoice_date = period.start_date + >>> invoice.accounting_date = period.start_date + >>> line = invoice.lines.new() + >>> line.product = product + >>> line.quantity = 5 + >>> line.unit_price = Decimal('40') + >>> invoice.sii_book_key = 'R' + >>> invoice.save() + >>> invoice.click('post') + >>> invoice.state + 'posted' + >>> bool(invoice.is_reagyp) + False + + >>> invoice2 = Invoice(type='in') + >>> invoice2.party = party_reagyp + >>> invoice2.invoice_date = period.start_date + >>> invoice2.accounting_date = period.start_date + >>> line = invoice2.lines.new() + >>> line.product = product + >>> line.quantity = 5 + >>> line.unit_price = Decimal('40') + >>> invoice2.sii_book_key = 'R' + >>> invoice2.save() + >>> invoice2.click('post') + >>> invoice2.state + 'posted' + >>> bool(invoice2.is_reagyp) + True + +Check invoices references:: + + >>> invoice.click('post') + >>> invoice.reference == invoice.number + False + >>> invoice2.click('post') + >>> invoice2.reference == invoice2.number + True + +Create AEAT Report:: + + >>> AEATReport = Model.get('aeat.sii.report') + >>> Conf = Model.get('account.configuration') + >>> conf = Conf(1) + >>> report = AEATReport() + >>> report.fiscalyear = fiscalyear + >>> report.period = period + >>> report.operation_type = 'A0' + >>> report.book = 'R' + >>> report.company_vat = '123456789' + >>> report.save() + >>> report.state + 'draft' + >>> report.click('load_invoices') + >>> len(report.lines) + 2 + >>> line1, line2 = report.lines + >>> report.lines.remove(line1) + >>> report.lines.remove(line2) + >>> report.save() + >>> conf.sii_reagyp_invoice_paid = True + >>> conf.save() + >>> report.click('load_invoices') + >>> len(report.lines) + 1 + >>> line = report.lines.new() + >>> line.invoice = invoice2 + >>> report.save() + >>> report.reload() + >>> len(report.lines) + 2 + >>> report.click('confirm') + Traceback (most recent call last): + ... + trytond.exceptions.UserWarning: There are unpaid invoices with REAGYP taxes in SII Book to confirm: + 2 [2]. - \ No newline at end of file diff --git a/tests/test_aeat_sii.py b/tests/test_aeat_sii.py index bbb3cc8..e29a340 100644 --- a/tests/test_aeat_sii.py +++ b/tests/test_aeat_sii.py @@ -32,4 +32,9 @@ def suite(): tearDown=doctest_teardown, encoding='utf-8', checker=doctest_checker, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(doctest.DocFileSuite( + 'scenario_aeat_sii_reagyp.rst', + tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite diff --git a/tryton.cfg b/tryton.cfg index a7d3a7b..9aa87ed 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -19,5 +19,8 @@ xml: party.xml company.xml load_pkcs12.xml - sii.xml message.xml + # uncomment to use 'account_es' from trytonspain + sii.xml + # uncomment to use 'account_es' from foundation + #sii_core.xml diff --git a/view/configuration_form.xml b/view/configuration_form.xml index 904bdf4..b082800 100644 --- a/view/configuration_form.xml +++ b/view/configuration_form.xml @@ -15,5 +15,7 @@ copyright notices and license types. -->