Add workflow on chorus invoice
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -34,6 +34,34 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="view" ref="invoice_chorus_view_form"/>
|
||||
<field name="act_window" ref="act_invoice_chorus_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_invoice_chorus_form_domain_draft">
|
||||
<field name="name">Draft</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="domain" eval="[('state', '=', 'draft')]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_invoice_chorus_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_invoice_chorus_form_domain_sent">
|
||||
<field name="name">Sent</field>
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="domain" eval="[('state', '=', 'sent')]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_invoice_chorus_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_invoice_chorus_form_domain_exception">
|
||||
<field name="name">Exception</field>
|
||||
<field name="sequence" eval="30"/>
|
||||
<field name="domain" eval="[('state', '=', 'exception')]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_invoice_chorus_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_invoice_chorus_form_domain_all">
|
||||
<field name="name">All</field>
|
||||
<field name="sequence" eval="9999"/>
|
||||
<field name="domain"></field>
|
||||
<field name="act_window" ref="act_invoice_chorus_form"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="account_invoice.menu_invoice_out_form"
|
||||
action="act_invoice_chorus_form"
|
||||
@@ -56,10 +84,28 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.button" id="invoice_chorus_send_button">
|
||||
<field name="name">send</field>
|
||||
<field name="string">Send</field>
|
||||
<field name="model" search="[('model', '=', 'account.invoice.chorus')]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.button" id="invoice_chorus_update_button">
|
||||
<field name="name">update</field>
|
||||
<field name="string">Update</field>
|
||||
<field name="model" search="[('model', '=', 'account.invoice.chorus')]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="cron_invoice_send">
|
||||
<field name="method">account.invoice.chorus|send</field>
|
||||
<field name="interval_number" eval="15"/>
|
||||
<field name="interval_type">minutes</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="cron_invoice_update">
|
||||
<field name="method">account.invoice.chorus|update</field>
|
||||
<field name="interval_number" eval="1"/>
|
||||
<field name="interval_type">hours</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
@@ -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"),
|
||||
])
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -12,5 +12,12 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="date"/>
|
||||
|
||||
<label name="data"/>
|
||||
<field name="data"/>
|
||||
<field name="data" colspan="3"/>
|
||||
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<group col="-1" colspan="2" id="buttons">
|
||||
<button name="send"/>
|
||||
<button name="update"/>
|
||||
</group>
|
||||
</form>
|
||||
|
||||
@@ -7,4 +7,5 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="number"/>
|
||||
<field name="date"/>
|
||||
<field name="data"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
|
||||
Reference in New Issue
Block a user