From 2b7335c1ffbc9896d54db1e186ffe81c0447bf21 Mon Sep 17 00:00:00 2001 From: Raimon Esteve Date: Mon, 16 Nov 2020 10:18:00 +0100 Subject: [PATCH] Calculate SII by cron --- __init__.py | 3 + account.py | 38 ++++- account.xml | 8 + aeat.py | 273 ++++++++++++++++++++++++++++++--- company.py | 2 +- cron.py | 15 ++ cron.xml | 12 ++ invoice.py | 291 +----------------------------------- tryton.cfg | 1 + view/configuration_form.xml | 17 +++ 10 files changed, 350 insertions(+), 310 deletions(-) create mode 100644 cron.py create mode 100644 cron.xml create mode 100644 view/configuration_form.xml diff --git a/__init__.py b/__init__.py index 37ae274..52bde81 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,7 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. from trytond.pool import Pool +from . import cron from . import invoice from . import aeat from . import party @@ -14,8 +15,10 @@ from . import purchase def register(): Pool.register( + account.Configuration, account.TemplateTax, account.Tax, + cron.Cron, party.Party, party.PartyIdentifier, company.Company, diff --git a/account.py b/account.py index 7b0c80f..b3c8733 100644 --- a/account.py +++ b/account.py @@ -2,12 +2,48 @@ # copyright notices and license terms. from trytond.model import fields from trytond.pool import PoolMeta +from trytond.pyson import Eval from .aeat import (BOOK_KEY, OPERATION_KEY, SEND_SPECIAL_REGIME_KEY, RECEIVE_SPECIAL_REGIME_KEY, IVA_SUBJECTED, EXCEMPTION_CAUSE, INTRACOMUNITARY_TYPE) -__all__ = ['TemplateTax', 'Tax'] +__all__ = ['Configuration', 'TemplateTax', 'Tax'] + + +class Configuration(metaclass=PoolMeta): + __name__ = 'account.configuration' + aeat_pending_sii = fields.Boolean('AEAT Pending SII', + help='Automatically generate AEAT Pending SII reports by cron') + aeat_pending_sii_send = fields.Boolean('AEAT Pending SII Send', + states={ + 'invisible': ~Eval('aeat_pending_sii', False), + }, depends=['aeat_pending_sii'], + help='Automatically send AEAT Pending SII reports by cron') + aeat_received_sii = fields.Boolean('AEAT Received SII', + help='Automatically generate AEAT Received SII reports by cron') + aeat_received_sii_send = fields.Boolean('AEAT Received SII Send', + states={ + 'invisible': ~Eval('aeat_received_sii', False), + }, depends=['aeat_received_sii'], + help='Automatically send AEAT Received SII reports by cron') + + @staticmethod + def default_aeat_pending_sii(): + return False + + @staticmethod + def default_aeat_received_sii(): + return False + + @staticmethod + def default_aeat_pending_sii_send(): + return False + + @staticmethod + def default_aeat_received_sii_send(): + return False + class TemplateTax(metaclass=PoolMeta): diff --git a/account.xml b/account.xml index f91587c..52a53f5 100644 --- a/account.xml +++ b/account.xml @@ -3,6 +3,14 @@ copyright notices and license terms. --> + + + account.configuration + + configuration_form + + + account.tax diff --git a/aeat.py b/aeat.py index a737dd1..6184915 100644 --- a/aeat.py +++ b/aeat.py @@ -8,6 +8,7 @@ from datetime import datetime from zeep import helpers import json from collections import namedtuple +from ast import literal_eval from pyAEATsii import service from pyAEATsii import mapping @@ -20,6 +21,7 @@ from trytond.transaction import Transaction from trytond.config import config from trytond.i18n import gettext from trytond.exceptions import UserError +from trytond.tools import grouped_slice from . import tools @@ -38,7 +40,7 @@ _ZERO = Decimal('0.0') # AEAT SII test SII_TEST = config.getboolean('aeat', 'sii_test', default=True) - +MAX_SII_LINES = config.getint('aeat', 'sii_lines', default=300) def _decimal(x): return Decimal(x) if x is not None else None @@ -940,6 +942,252 @@ class SIIReport(Workflow, ModelSQL, ModelView): if pagination == 'S': self.query_recieved_invoices(last_invoice=last_invoice) + @classmethod + def get_issued_sii_reports(cls): + pool = Pool() + Invoice = pool.get('account.invoice') + SIIReportLine = pool.get('aeat.sii.report.lines') + + issued_invoices = { + 'A0': {}, # 'A0', 'Registration of invoices/records' + 'A1': {}, # 'A1', 'Amendment of invoices/records (registration errors)' + 'D0': {}, # 'D0', 'Delete Invoices' + } + + issued_invs = Invoice.search([ + ('sii_pending_sending', '=', True), + ('sii_state', '=', 'Correcto'), + ('sii_header', '!=', None), + ('type', '=', 'out'), + ]) + + # search issued invoices [delete] + delete_issued_invoices = [] + # search issued invoices [modify] + modify_issued_invoices = [] + for issued_inv in issued_invs: + if not issued_inv.sii_records: + continue + sii_record_id = max([s.id for s in issued_inv.sii_records]) + sii_record = SIIReportLine(sii_record_id) + if issued_inv.sii_header: + if (literal_eval(issued_inv.sii_header) == + literal_eval(sii_record.sii_header)): + modify_issued_invoices.append(issued_inv) + else: + delete_issued_invoices.append(issued_inv) + + periods = {} + for invoice in delete_issued_invoices: + period = invoice.move.period + if period in periods: + periods[period].append(invoice,) + else: + periods[period] = [invoice] + issued_invoices['D0'] = periods + + periods2 = {} + for invoice in modify_issued_invoices: + period = invoice.move.period + if period in periods2: + periods2[period].append(invoice,) + else: + periods2[period] = [invoice] + issued_invoices['A1'] = periods2 + + # search issued invoices [new] + new_issued_invoices = Invoice.search([ + ('sii_state', 'in', (None, 'Incorrecto')), + ('sii_pending_sending', '=', True), + ('type', '=', 'out'), + ]) + + # search possible deleted invoices in SII and not uploaded again + new_issued_invoices += Invoice.search([ + ('sii_state', '=', 'Anulada'), + ('sii_pending_sending', '=', True), + ('type', '=', 'out'), + ('state', 'in', ['paid', 'posted']), + ]) + + new_issued_invoices += delete_issued_invoices + + periods1 = {} + for invoice in new_issued_invoices: + period = invoice.move.period + if period in periods1: + periods1[period].append(invoice,) + else: + periods1[period] = [invoice] + issued_invoices['A0'] = periods1 + + book_type = 'E' # Issued + return cls.create_sii_book(issued_invoices, book_type) + + @classmethod + def get_received_sii_reports(cls): + pool = Pool() + Invoice = pool.get('account.invoice') + SIIReportLine = pool.get('aeat.sii.report.lines') + + received_invoices = { + 'A0': {}, # 'A0', 'Registration of invoices/records' + 'A1': {}, # 'A1', 'Amendment of invoices/records (registration errors)' + 'D0': {}, # 'D0', 'Delete Invoices' + } + + received_invs = Invoice.search([ + ('sii_pending_sending', '=', True), + ('sii_state', '=', 'Correcto'), + ('sii_header', '!=', None), + ('type', '=', 'in'), + ]) + # search received invoices [delete] + delete_received_invoices = [] + # search received invoices [modify] + modify_received_invoices = [] + for received_inv in received_invs: + if not received_inv.sii_records: + continue + sii_record_id = max([s.id for s in received_inv.sii_records]) + sii_record = SIIReportLine(sii_record_id) + if received_inv.sii_header: + if (literal_eval(received_inv.sii_header) == + literal_eval(sii_record.sii_header)): + modify_received_invoices.append(received_inv) + else: + delete_received_invoices.append(received_inv) + + periods2 = {} + for invoice in modify_received_invoices: + period = invoice.move.period + if period in periods2: + periods2[period].append(invoice,) + else: + periods2[period] = [invoice] + received_invoices['A1'] = periods2 + + periods = {} + for invoice in delete_received_invoices: + period = invoice.move.period + if period in periods: + periods[period].append(invoice,) + else: + periods[period] = [invoice] + received_invoices['D0'] = periods + + # search received invoices [new] + new_received_invoices = Invoice.search([ + ('sii_state', 'in', (None, 'Incorrecto')), + ('sii_pending_sending', '=', True), + ('type', '=', 'in'), + ]) + + # search possible deleted invoices in SII and not uploaded again + new_received_invoices += Invoice.search([ + ('sii_state', '=', 'Anulada'), + ('sii_pending_sending', '=', True), + ('type', '=', 'in'), + ('state', 'in', ['paid', 'posted']), + ]) + + new_received_invoices += delete_received_invoices + + periods1 = {} + for invoice in new_received_invoices: + period = invoice.move.period + if period in periods1: + periods1[period].append(invoice,) + else: + periods1[period] = [invoice] + received_invoices['A0'] = periods1 + + book_type = 'R' # Received + return cls.create_sii_book(received_invoices, book_type) + + @classmethod + def create_sii_book(cls, book_invoices, book): + pool = Pool() + SIIReport = pool.get('aeat.sii.report') + SIIReportLine = pool.get('aeat.sii.report.lines') + Company = Pool().get('company.company') + + company = Transaction().context.get('company') + company = Company(company) + company_vat = company.party.sii_vat_code + + cursor = Transaction().connection.cursor() + report_line_table = SIIReportLine.__table__() + + reports = [] + for operation in ['D0', 'A1', 'A0']: + values = book_invoices[operation] + delete = True if operation == 'D0' else False + for period, invoices in values.items(): + for invs in grouped_slice(invoices, MAX_SII_LINES): + report = SIIReport() + report.company = company + report.company_vat = company_vat + report.fiscalyear = period.fiscalyear + report.period = period + report.operation_type = operation + report.book = book + report.save() + reports.append(report) + + values = [] + for inv in invs: + sii_header = str(inv.get_sii_header(inv, delete)) + values.append([report.id, inv.id, sii_header, company.id]) + + cursor.execute(*report_line_table.insert( + columns=[report_line_table.report, + report_line_table.invoice, + report_line_table.sii_header, + report_line_table.company], + values=values + )) + + return reports + + @classmethod + def find_reports(cls, book='E'): + return cls.search([ + ('state', 'in', ('draft', 'confirmed')), + ('book', '=', book), + ], count=True) + + @classmethod + def calculate_sii(cls): + Configuration = Pool().get('account.configuration') + + config = Configuration(1) + + if not config.aeat_pending_sii and not config.aeat_received_sii: + return + + if config.aeat_pending_sii: + reports = cls.find_reports(book='E') + if reports: + _logger.info('Not calculate pending SII report ' + 'because is other reports pending to sending') + return + issued_reports = cls.get_issued_sii_reports() + if config.aeat_pending_sii_send: + cls.confirm(issued_reports) + cls.send(issued_reports) + + if config.aeat_received_sii: + reports = cls.find_reports(book='R') + if reports: + _logger.info('Not calculate received SII report ' + 'because is other reports pending to sending') + return + received_reports = cls.get_received_sii_reports() + if config.aeat_received_sii_send: + cls.confirm(received_reports) + cls.send(received_reports) + class SIIReportLine(ModelSQL, ModelView): ''' @@ -1161,17 +1409,12 @@ class CreateSiiIssuedPending(Wizard): create_ = StateAction('aeat_sii.act_aeat_sii_issued_report') def do_create_(self, action): - pool = Pool() - Invoice = pool.get('account.invoice') - Report = pool.get('aeat.sii.report') + Report = Pool().get('aeat.sii.report') - reports = Report.search([ - ('state', 'in', ('draft', 'confirmed')), - ('book', '=', 'E'), - ]) + reports = Report.find_reports(book='E') if reports: raise UserError(gettext('aeat_sii.reports_exist')) - reports = Invoice.get_issued_sii_reports() + reports = Report.get_issued_sii_reports() reports = [x.id for x in reports] if reports else [] action['pyson_domain'] = PYSONEncoder().encode([ ('id', 'in', reports), @@ -1199,19 +1442,13 @@ class CreateSiiReceivedPending(Wizard): ]) create_ = StateAction('aeat_sii.act_aeat_sii_received_report') - def do_create_(self, action): - pool = Pool() - Invoice = pool.get('account.invoice') - Report = pool.get('aeat.sii.report') + Report = Pool().get('aeat.sii.report') - reports = Report.search([ - ('state', 'in', ('draft', 'confirmed')), - ('book', '=', 'R'), - ]) + reports = Report.find_reports(book='R') if reports: raise UserError(gettext('aeat_sii.reports_exist')) - reports = Invoice.get_received_sii_reports() + reports = Report.get_received_sii_reports() reports = [x.id for x in reports] if reports else [] action['pyson_domain'] = PYSONEncoder().encode([ ('id', 'in', reports), diff --git a/company.py b/company.py index cecab60..4654f8b 100644 --- a/company.py +++ b/company.py @@ -78,7 +78,7 @@ class Company(metaclass=PoolMeta): @contextmanager def tmp_ssl_credentials(self): - if not self.pem_certificate: + if not self.pem_certificate or not self.private_key: raise UserError(gettext('aeat_sii.msg_missing_pem_cert')) with NamedTemporaryFile(suffix='.crt') as crt: with NamedTemporaryFile(suffix='.pem') as key: diff --git a/cron.py b/cron.py new file mode 100644 index 0000000..930b775 --- /dev/null +++ b/cron.py @@ -0,0 +1,15 @@ +# The COPYRIGHT file at the top level of this repository contains the full +# copyright notices and license terms. +from trytond.pool import PoolMeta + +__all__ = ['Cron'] + +class Cron(metaclass=PoolMeta): + __name__ = 'ir.cron' + + @classmethod + def __setup__(cls): + super().__setup__() + cls.method.selection.extend([ + ('aeat.sii.report|calculate_sii', "AEAT SII"), + ]) diff --git a/cron.xml b/cron.xml new file mode 100644 index 0000000..8b6bea7 --- /dev/null +++ b/cron.xml @@ -0,0 +1,12 @@ + + + + + + + days + aeat.sii.report|calculate_sii + + + diff --git a/invoice.py b/invoice.py index 6a32279..c82c421 100644 --- a/invoice.py +++ b/invoice.py @@ -1,18 +1,14 @@ # The COPYRIGHT file at the top level of this repository contains the full # copyright notices and license terms. import hashlib -from ast import literal_eval from decimal import Decimal from trytond.model import ModelView, fields from trytond.pool import Pool, PoolMeta -from trytond.pyson import Eval, Bool +from trytond.pyson import Eval from trytond.transaction import Transaction from trytond.i18n import gettext from trytond.exceptions import UserError, UserWarning from trytond.wizard import Wizard, StateView, StateTransition, Button -from sql import Null -from sql.aggregate import Max -from trytond.tools import grouped_slice from .aeat import ( OPERATION_KEY, BOOK_KEY, SEND_SPECIAL_REGIME_KEY, RECEIVE_SPECIAL_REGIME_KEY, AEAT_INVOICE_STATE, IVA_SUBJECTED, @@ -25,8 +21,6 @@ _SII_INVOICE_KEYS = ['sii_book_key', 'sii_operation_key', 'sii_issued_key', 'sii_received_key', 'sii_subjected_key', 'sii_excemption_key', 'sii_intracomunity_key'] -MAX_SII_LINES = 300 - class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' @@ -77,289 +71,6 @@ class Invoice(metaclass=PoolMeta): def default_sii_pending_sending(): return False - @classmethod - def get_issued_sii_reports(cls): - pool = Pool() - Invoice = pool.get('account.invoice') - SIIReportLine = pool.get('aeat.sii.report.lines') - - issued_invoices = { - 'A0': {}, # 'A0', 'Registration of invoices/records' - 'A1': {}, # 'A1', 'Amendment of invoices/records (registration errors)' - 'D0': {}, # 'D0', 'Delete Invoices' - } - - issued_invs = Invoice.search([ - ('sii_pending_sending', '=', True), - ('sii_state', '=', 'Correcto'), - ('sii_header', '!=', None), - ('type', 'in', ['out']), - ]) - - # search issued invoices [delete] - delete_issued_invoices = [] - # search issued invoices [modify] - modify_issued_invoices = [] - for issued_inv in issued_invs: - if not issued_inv.sii_records: - continue - sii_record_id = max([s.id for s in issued_inv.sii_records]) - sii_record = SIIReportLine(sii_record_id) - if issued_inv.sii_header: - if (literal_eval(issued_inv.sii_header) == - literal_eval(sii_record.sii_header)): - modify_issued_invoices.append(issued_inv) - else: - delete_issued_invoices.append(issued_inv) - - periods = {} - for invoice in delete_issued_invoices: - period = invoice.move.period - if period in periods: - periods[period].append(invoice,) - else: - periods[period] = [invoice] - issued_invoices['D0'] = periods - - periods2 = {} - for invoice in modify_issued_invoices: - period = invoice.move.period - if period in periods2: - periods2[period].append(invoice,) - else: - periods2[period] = [invoice] - issued_invoices['A1'] = periods2 - - # search issued invoices [new] - new_issued_invoices = Invoice.search([ - ('sii_state', 'in', (None, 'Incorrecto')), - ('sii_pending_sending', '=', True), - ('type', '=', 'out'), - ]) - - # search possible deleted invoices in SII and not uploaded again - new_issued_invoices += Invoice.search([ - ('sii_state', '=', 'Anulada'), - ('sii_pending_sending', '=', True), - ('type', '=', 'out'), - ('state', 'in', ['paid', 'posted']), - ]) - - new_issued_invoices += delete_issued_invoices - - periods1 = {} - for invoice in new_issued_invoices: - period = invoice.move.period - if period in periods1: - periods1[period].append(invoice,) - else: - periods1[period] = [invoice] - issued_invoices['A0'] = periods1 - - book_type = 'E' # Issued - return cls.create_sii_book(issued_invoices, book_type) - - @classmethod - def get_received_sii_reports(cls): - pool = Pool() - Invoice = pool.get('account.invoice') - SIIReportLine = pool.get('aeat.sii.report.lines') - - received_invoices = { - 'A0': {}, # 'A0', 'Registration of invoices/records' - 'A1': {}, # 'A1', 'Amendment of invoices/records (registration errors)' - 'D0': {}, # 'D0', 'Delete Invoices' - } - - received_invs = Invoice.search([ - ('sii_pending_sending', '=', True), - ('sii_state', '=', 'Correcto'), - ('sii_header', '!=', None), - ('type', '=', 'in'), - ]) - # search received invoices [delete] - delete_received_invoices = [] - # search received invoices [modify] - modify_received_invoices = [] - for received_inv in received_invs: - if not received_inv.sii_records: - continue - sii_record_id = max([s.id for s in received_inv.sii_records]) - sii_record = SIIReportLine(sii_record_id) - if received_inv.sii_header: - if (literal_eval(received_inv.sii_header) == - literal_eval(sii_record.sii_header)): - modify_received_invoices.append(received_inv) - else: - delete_received_invoices.append(received_inv) - - periods2 = {} - for invoice in modify_received_invoices: - period = invoice.move.period - if period in periods2: - periods2[period].append(invoice,) - else: - periods2[period] = [invoice] - received_invoices['A1'] = periods2 - - periods = {} - for invoice in delete_received_invoices: - period = invoice.move.period - if period in periods: - periods[period].append(invoice,) - else: - periods[period] = [invoice] - received_invoices['D0'] = periods - - # search received invoices [new] - new_received_invoices = Invoice.search([ - ('sii_state', 'in', (None, 'Incorrecto')), - ('sii_pending_sending', '=', True), - ('type', '=', 'in'), - ]) - - # search possible deleted invoices in SII and not uploaded again - new_received_invoices += Invoice.search([ - ('sii_state', '=', 'Anulada'), - ('sii_pending_sending', '=', True), - ('type', '=', 'in'), - ('state', 'in', ['paid', 'posted']), - ]) - - new_received_invoices += delete_received_invoices - - periods1 = {} - for invoice in new_received_invoices: - period = invoice.move.period - if period in periods1: - periods1[period].append(invoice,) - else: - periods1[period] = [invoice] - received_invoices['A0'] = periods1 - - book_type = 'R' # Received - return cls.create_sii_book(received_invoices, book_type) - - @classmethod - def create_sii_book(cls, book_invoices, book): - pool = Pool() - SIIReport = pool.get('aeat.sii.report') - SIIReportLine = pool.get('aeat.sii.report.lines') - Company = Pool().get('company.company') - - company = Transaction().context.get('company') - company = Company(company) - company_vat = company.party.sii_vat_code - - cursor = Transaction().connection.cursor() - report_line_table = SIIReportLine.__table__() - - reports = [] - for operation in ['D0', 'A1', 'A0']: - values = book_invoices[operation] - delete = True if operation == 'D0' else False - for period, invoices in values.items(): - for invs in grouped_slice(invoices, MAX_SII_LINES): - report = SIIReport() - report.company = company - report.company_vat = company_vat - report.fiscalyear = period.fiscalyear - report.period = period - report.operation_type = operation - report.book = book - report.save() - reports.append(report) - - values = [] - for inv in invs: - sii_header = str(inv.get_sii_header(inv, delete)) - values.append([report.id, inv.id, sii_header, company.id]) - - cursor.execute(*report_line_table.insert( - columns=[report_line_table.report, - report_line_table.invoice, - report_line_table.sii_header, - report_line_table.company], - values=values - )) - - return reports - - @classmethod - def search_sii_state(cls, name, clause): - pool = Pool() - SIILines = pool.get('aeat.sii.report.lines') - - table = SIILines.__table__() - - cursor = Transaction().connection.cursor() - cursor.execute(*table.select(Max(table.id), table.invoice, - group_by=table.invoice)) - - invoices = [] - lines = [] - for id_, invoice in cursor.fetchall(): - invoices.append(invoice) - lines.append(id_) - - is_none = False - c = clause[-1] - if isinstance(clause[-1], list): - if None in clause[-1]: - is_none = True - c.remove(None) - - c0 = [] - if clause[-1] == None or is_none: - c0 = [('id', 'not in', invoices)] - - clause2 = [tuple(('state',)) + tuple(clause[1:])] + \ - [('id', 'in', lines)] - - res_lines = SIILines.search(clause2) - - if is_none: - return ['OR', c0, [('id', 'in', [x.invoice.id for x in res_lines])]] - else: - return [('id', 'in', [x.invoice.id for x in res_lines])] - - @classmethod - def get_sii_state(cls, invoices, names): - pool = Pool() - SIILines = pool.get('aeat.sii.report.lines') - SIIReport = pool.get('aeat.sii.report') - - result = {} - - for name in names: - result[name] = dict((i.id, None) for i in invoices) - - table = SIILines.__table__() - report = SIIReport.__table__() - cursor = Transaction().connection.cursor() - join = table.join(report, condition=table.report == report.id) - - cursor.execute(*table.select(Max(table.id), table.invoice, - where=(table.invoice.in_([x.id for x in invoices]) & - (table.state != Null)), - group_by=table.invoice)) - - lines = [a[0] for a in cursor.fetchall()] - - if lines: - cursor.execute(*join.select(table.state, report.operation_type, - table.invoice, - where=((table.id.in_(lines)) & (table.state != Null) & - (table.company == report.company)))) - - for state, op, inv in cursor.fetchall(): - if 'sii_state' in names: - result['sii_state'][inv] = state - if 'sii_communication_type' in names: - result['sii_communication_type'][inv] = op - - return result - def _credit(self, **values): credit = super(Invoice, self)._credit(**values) for field in _SII_INVOICE_KEYS: diff --git a/tryton.cfg b/tryton.cfg index e4772ea..16bb6d7 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -12,6 +12,7 @@ extras_depend: xml: account.xml aeat.xml + cron.xml invoice.xml party.xml company.xml diff --git a/view/configuration_form.xml b/view/configuration_form.xml new file mode 100644 index 0000000..9f7965c --- /dev/null +++ b/view/configuration_form.xml @@ -0,0 +1,17 @@ + + + + + + +