diff --git a/modules/account_fr_chorus/account.py b/modules/account_fr_chorus/account.py index 8a32bb5149..b8db96489c 100644 --- a/modules/account_fr_chorus/account.py +++ b/modules/account_fr_chorus/account.py @@ -12,7 +12,7 @@ from sql.functions import CharLength from trytond.config import config from trytond.i18n import gettext -from trytond.model import ModelSQL, ModelView, Unique, fields +from trytond.model import ModelSQL, ModelView, Unique, Workflow, fields from trytond.model.exceptions import AccessError from trytond.modules.company.model import CompanyValueMixin from trytond.pool import Pool, PoolMeta @@ -47,6 +47,11 @@ else: logger = logging.getLogger(__name__) +SUCCEEDED = {'IN_INTEGRE', 'IN_RECU', 'IN_TRAITE_SE_CPP'} +FAILED = { + 'IN_INCIDENTE', 'QP_IRRECEVABLE', 'QP_RECEVABLE_AVEC_ERREUR', 'IN_REJETE'} + + class _SyntaxMixin(object): __slots__ = () @@ -176,9 +181,11 @@ class Invoice(metaclass=PoolMeta): InvoiceChorus.save(invoices_chorus) -class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): +class InvoiceChorus( + Workflow, ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): "Invoice Chorus" __name__ = 'account.invoice.chorus' + _history = True invoice = fields.Many2One( 'account.invoice', "Invoice", required=True, @@ -190,12 +197,26 @@ class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): ]) syntax = fields.Selection('get_syntaxes', "Syntax", required=True) filename = fields.Function(fields.Char("Filename"), 'get_filename') - number = fields.Char("Number", readonly=True, strip=False) - date = fields.Date("Date", readonly=True) + number = fields.Char( + "Number", readonly=True, strip=False, + states={ + 'required': Eval('state') == 'sent', + }) + date = fields.Date( + "Date", readonly=True, + states={ + 'required': Eval('state') == 'sent', + }) data = fields.Binary( "Data", filename='filename', file_id=file_id, store_prefix=store_prefix, readonly=True) data_file_id = fields.Char("Data File ID", readonly=True) + state = fields.Selection([ + ('draft', "Draft"), + ('sent', "Sent"), + ('done', "Done"), + ('exception', "Exception"), + ], "State", readonly=True, required=True, sort=False) @classmethod def __setup__(cls): @@ -208,6 +229,37 @@ class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): 'account_fr_chorus.msg_invoice_unique'), ] + cls._transitions |= { + ('draft', 'sent'), + ('sent', 'done'), + ('sent', 'exception'), + ('exception', 'sent'), + } + cls._buttons.update( + send={ + 'invisible': ~Eval('state').in_(['draft', 'exception']), + 'depends': ['state'], + }, + update={ + 'invisible': Eval('state') != 'sent', + 'depends': ['state'], + }, + ) + + @classmethod + def __register__(cls, module): + cursor = Transaction().connection.cursor() + table = cls.__table__() + table_h = cls.__table_handler__(module) + + update_state = not table_h.column_exist('state') + + super().__register__(module) + + # Migration from 6.8: fill state + if update_state: + cursor.execute(*table.update([table.state], ['done'])) + @classmethod def default_syntax(cls): pool = Pool() @@ -219,6 +271,10 @@ class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): filename = EDOC2FILENAME[self.syntax] % self.invoice.number return filename.replace('/', '-') + @classmethod + def default_state(cls): + return 'draft' + @classmethod def order_number(cls, tables): table, _ = tables[None] @@ -254,6 +310,8 @@ class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): } @classmethod + @ModelView.button + @Workflow.transition('sent') def send(cls, records=None): """Send invoice to Chorus @@ -264,11 +322,10 @@ class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): transaction = Transaction() if not records: - records = cls.search(['OR', - ('number', '=', None), - ('number', '=', ''), + records = cls.search([ ('invoice.company', '=', transaction.context.get('company')), + ('state', '=', 'draft'), ]) sessions = defaultdict(Credential.get_session) @@ -291,6 +348,7 @@ class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): record.number = resp['numeroFluxDepot'] record.date = datetime.datetime.strptime( resp['dateDepot'], '%Y-%m-%d').date() + record.state = 'sent' record.save() Transaction().commit() @@ -305,3 +363,54 @@ class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta): 'syntaxeFlux': EDOC2SYNTAX[self.syntax], 'avecSignature': False, } + + @classmethod + @ModelView.button + def update(cls, records=None): + "Update state from Chorus" + pool = Pool() + Credential = pool.get('account.credential.chorus') + transaction = Transaction() + + if not records: + records = cls.search([ + ('invoice.company', '=', + transaction.context.get('company')), + ('state', '=', 'sent'), + ]) + + sessions = defaultdict(Credential.get_session) + succeeded, failed = [], [] + for record in records: + if not record.number: + continue + context = record._send_context() + with transaction.set_context(**context): + payload = { + 'numeroFluxDepot': record.number, + } + resp = Credential.post( + 'cpro/transverses/v1/consulterCR', payload, + session=sessions[tuple(context.items())]) + if resp['codeRetour']: + logger.info( + "Error when retrieve information about %d: %s", + record.id, resp['libelle']) + elif resp['etatCourantFlux'] in SUCCEEDED: + succeeded.append(record) + elif resp['etatCourantFlux'] in FAILED: + failed.append(record) + if failed: + cls.fail(failed) + if succeeded: + cls.succeed(succeeded) + + @classmethod + @Workflow.transition('done') + def succeed(cls, records): + pass + + @classmethod + @Workflow.transition('exception') + def fail(cls, records): + pass diff --git a/modules/account_fr_chorus/account.xml b/modules/account_fr_chorus/account.xml index d8c5cf042f..17d4282cbe 100644 --- a/modules/account_fr_chorus/account.xml +++ b/modules/account_fr_chorus/account.xml @@ -34,6 +34,34 @@ this repository contains the full copyright notices and license terms. --> + + Draft + + + + + + + Sent + + + + + + + Exception + + + + + + + All + + + + + + + send + Send + + + + + update + Update + + + account.invoice.chorus|send minutes + + + account.invoice.chorus|update + + hours + diff --git a/modules/account_fr_chorus/ir.py b/modules/account_fr_chorus/ir.py index 24d3e7e775..c7ae1c8550 100644 --- a/modules/account_fr_chorus/ir.py +++ b/modules/account_fr_chorus/ir.py @@ -9,5 +9,8 @@ class Cron(metaclass=PoolMeta): @classmethod def __setup__(cls): super().__setup__() - cls.method.selection.append( - ('account.invoice.chorus|send', "Send invoices to Chorus")) + cls.method.selection.extend([ + ('account.invoice.chorus|send', "Send invoices to Chorus"), + ('account.invoice.chorus|update', + "Update invoices from Chorus"), + ]) diff --git a/modules/account_fr_chorus/tests/scenario_account_fr_chorus_invoice.rst b/modules/account_fr_chorus/tests/scenario_account_fr_chorus_invoice.rst index ea3a0a992f..c42d9d385a 100644 --- a/modules/account_fr_chorus/tests/scenario_account_fr_chorus_invoice.rst +++ b/modules/account_fr_chorus/tests/scenario_account_fr_chorus_invoice.rst @@ -4,7 +4,10 @@ Account FR Chorus Invoice Imports:: + >>> import datetime as dt >>> import os + >>> import time + >>> import uuid >>> from decimal import Decimal >>> from proteus import Model @@ -17,6 +20,8 @@ Imports:: >>> from trytond.modules.account_invoice.tests.tools import ( ... set_fiscalyear_invoice_sequences) + >>> today = dt.date.today() + Patch SIRET validation:: >>> from unittest.mock import patch @@ -65,14 +70,6 @@ Create chart of accounts:: ... ('group.kind', '=', 'sale'), ... ('name', 'ilike', '%normal%'), ... ], limit=1) - >>> tax.template_override = True - >>> tax.unece_code = 'VAT' - >>> tax.unece_category_code = 'S' - >>> for child in tax.childs: - ... child.template_override = True - ... child.unece_code = tax.unece_code - ... child.unece_category_code = tax.unece_category_code - >>> tax.save() Configure Chorus:: @@ -109,6 +106,11 @@ Create invoice:: >>> line.quantity = 5 >>> line.unit_price = Decimal('50.0000') >>> line.taxes.append(tax) + >>> invoice.save() + >>> Invoice.write([invoice], { + ... 'number': str(uuid.uuid4())[:20], + ... 'invoice_date': today, + ... }, config._context) >>> invoice.click('post') >>> invoice.state 'posted' @@ -123,13 +125,48 @@ Check Chorus invoice:: Send to Chorus:: - >>> cron, = Cron.find([('method', '=', 'account.invoice.chorus|send')]) - >>> cron.click('run_once') - - >>> invoice_chorus.reload() + >>> invoice_chorus.click('send') + >>> invoice_chorus.state + 'sent' >>> bool(invoice_chorus.number) True >>> bool(invoice_chorus.date) True >>> bool(invoice_chorus.data) True + >>> number = invoice_chorus.number + +Update from Chorus:: + + >>> while invoice_chorus.state == 'sent': + ... invoice_chorus.click('update') + ... time.sleep(1) + >>> invoice_chorus.state + 'exception' + +Add code to tax:: + + >>> tax.template_override = True + >>> tax.unece_code = 'VAT' + >>> tax.unece_category_code = 'S' + >>> for child in tax.childs: + ... child.template_override = True + ... child.unece_code = tax.unece_code + ... child.unece_category_code = tax.unece_category_code + >>> tax.save() + +Resend to Chorus:: + + >>> invoice_chorus.click('send') + >>> invoice_chorus.state + 'sent' + >>> invoice_chorus.number != number + True + +Update from Chorus:: + + >>> while invoice_chorus.state == 'sent': + ... invoice_chorus.click('update') + ... time.sleep(1) + >>> invoice_chorus.state + 'done' diff --git a/modules/account_fr_chorus/view/invoice_chorus_form.xml b/modules/account_fr_chorus/view/invoice_chorus_form.xml index 0714a563e6..8865817a49 100644 --- a/modules/account_fr_chorus/view/invoice_chorus_form.xml +++ b/modules/account_fr_chorus/view/invoice_chorus_form.xml @@ -12,5 +12,12 @@ this repository contains the full copyright notices and license terms. -->