first commit
This commit is contained in:
331
modules/edocument_peppol/edocument.py
Normal file
331
modules/edocument_peppol/edocument.py
Normal file
@@ -0,0 +1,331 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from sql.conditionals import NullIf
|
||||
from sql.operators import Equal
|
||||
|
||||
import trytond.config as config
|
||||
from trytond.model import (
|
||||
Exclude, MatchMixin, ModelSQL, ModelView, Workflow, dualmethod, fields,
|
||||
sequence_ordered)
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval, If
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .exceptions import PeppolServiceError
|
||||
|
||||
if config.getboolean('edocument_peppol', 'filestore', default=True):
|
||||
file_id = 'file_id'
|
||||
store_prefix = config.get(
|
||||
'edocument_peppol', 'store_prefix', default=None)
|
||||
else:
|
||||
file_id = store_prefix = None
|
||||
|
||||
|
||||
class Peppol(Workflow, ModelSQL, ModelView):
|
||||
__name__ = 'edocument.peppol'
|
||||
|
||||
_states = {
|
||||
'readonly': Eval('state') != 'draft',
|
||||
}
|
||||
|
||||
direction = fields.Selection([
|
||||
('in', "IN"),
|
||||
('out', "OUT"),
|
||||
], "Direction", required=True, states=_states)
|
||||
company = fields.Many2One(
|
||||
'company.company', "Company", required=True, states=_states)
|
||||
type = fields.Selection([
|
||||
(None, ""),
|
||||
('bis-billing-3', "BIS Billing V3"),
|
||||
], "Type", translate=False,
|
||||
states={
|
||||
'readonly': _states['readonly'],
|
||||
'required': Eval('state') != 'draft',
|
||||
})
|
||||
service = fields.Many2One(
|
||||
'edocument.peppol.service', "Service",
|
||||
domain=[
|
||||
('company', '=', Eval('company', -1)),
|
||||
If(Eval('state') == 'draft',
|
||||
('types', 'in', Eval('type')),
|
||||
()),
|
||||
],
|
||||
states={
|
||||
'readonly': _states['readonly'],
|
||||
'required': Eval('state') != 'draft',
|
||||
})
|
||||
invoice = fields.Many2One(
|
||||
'account.invoice', "Invoice", ondelete='RESTRICT',
|
||||
domain=[
|
||||
('company', '=', Eval('company', -1)),
|
||||
If(Eval('direction') == 'out', [
|
||||
('type', '=', 'out'),
|
||||
('state', 'in', ['posted', 'paid']),
|
||||
], [
|
||||
('type', '=', 'in'),
|
||||
]),
|
||||
],
|
||||
states={
|
||||
'readonly': _states['readonly'],
|
||||
'invisible': (
|
||||
~Eval('type').in_([
|
||||
'bis-billing-3',
|
||||
])
|
||||
| ((Eval('state') == 'draft')
|
||||
& (Eval('direction') == 'in'))),
|
||||
'required': (
|
||||
(Eval('direction') == 'out')
|
||||
& Eval('type').in_([
|
||||
'bis-billing-3',
|
||||
])),
|
||||
})
|
||||
data = fields.Binary(
|
||||
"Data",
|
||||
file_id=file_id, store_prefix=store_prefix,
|
||||
states={
|
||||
'invisible': (
|
||||
(Eval('state') == 'draft')
|
||||
& (Eval('direction') == 'out')),
|
||||
})
|
||||
file_id = fields.Char("File ID", readonly=False)
|
||||
transmission_id = fields.Char("Transmission ID", readonly=True)
|
||||
document_retried = fields.Many2One(
|
||||
'edocument.peppol', "Retry", readonly=True,
|
||||
states={
|
||||
'invisible': ~Eval('document_retried'),
|
||||
'required': Eval('state') == 'retried',
|
||||
})
|
||||
status = fields.Char(
|
||||
"Status", readonly=True,
|
||||
states={
|
||||
'invisible': ~Eval('status'),
|
||||
})
|
||||
state = fields.Selection([
|
||||
('draft', "Draft"),
|
||||
('submitted', "Submitted"),
|
||||
('processing', "Processing"),
|
||||
('succeeded', "Succeeded"),
|
||||
('failed', "Failed"),
|
||||
('retried', "Retried"),
|
||||
('cancelled', "Cancelled"),
|
||||
], "State", readonly=True, required=True, sort=False)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints += [
|
||||
('service_transmission_id_unique',
|
||||
Exclude(t,
|
||||
(t.service, Equal),
|
||||
(NullIf(t.transmission_id, ''), Equal)),
|
||||
'edocument_peppol.msg_service_transmission_id_unique'),
|
||||
]
|
||||
|
||||
cls._transitions |= {
|
||||
('draft', 'submitted'),
|
||||
('submitted', 'processing'),
|
||||
('processing', 'processing'),
|
||||
('processing', 'succeeded'),
|
||||
('processing', 'failed'),
|
||||
('failed', 'retried'),
|
||||
('failed', 'cancelled'),
|
||||
}
|
||||
cls._buttons.update(
|
||||
draft={
|
||||
'invisible': Eval('state') != 'submitted',
|
||||
'depends': ['state'],
|
||||
},
|
||||
submit={
|
||||
'invisible': Eval('state') != 'draft',
|
||||
'depends': ['state'],
|
||||
},
|
||||
process={
|
||||
'invisible': ~Eval('state').in_(['submitted', 'processing']),
|
||||
'depends': ['state'],
|
||||
},
|
||||
retry={
|
||||
'invisible': Eval('state') != 'failed',
|
||||
'depends': ['state'],
|
||||
},
|
||||
cancel={
|
||||
'invisible': Eval('state') != 'failed',
|
||||
'depends': ['state'],
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def default_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@classmethod
|
||||
def default_state(cls):
|
||||
return 'draft'
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('draft')
|
||||
def draft(cls, documents):
|
||||
pass
|
||||
|
||||
@dualmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('submitted')
|
||||
def submit(cls, documents):
|
||||
pool = Pool()
|
||||
Service = pool.get('edocument.peppol.service')
|
||||
for document in documents:
|
||||
if not document.service:
|
||||
document.service = Service.get_service(document)
|
||||
if document.direction == 'out':
|
||||
document.data = document.render()
|
||||
cls.save(documents)
|
||||
cls.__queue__.process(documents)
|
||||
|
||||
def get_service_pattern(self):
|
||||
return {
|
||||
'type': self.type,
|
||||
}
|
||||
|
||||
def render(self):
|
||||
pool = Pool()
|
||||
Invoice = pool.get('edocument.ubl.invoice')
|
||||
assert self.direction == 'out'
|
||||
if self.type == 'bis-billing-3':
|
||||
return Invoice(self.invoice).render(
|
||||
'2', specification='peppol-bis-3')
|
||||
|
||||
@dualmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('processing')
|
||||
def process(cls, documents):
|
||||
cls.lock(documents)
|
||||
# write state before calling _process
|
||||
cls.write(documents, {'state': 'processing'})
|
||||
for document in documents:
|
||||
if document.direction == 'out':
|
||||
cls.__queue__._process(document)
|
||||
else:
|
||||
document._process()
|
||||
|
||||
def _process(self):
|
||||
pool = Pool()
|
||||
Invoice = pool.get('edocument.ubl.invoice')
|
||||
if self.state != 'processing':
|
||||
return
|
||||
self.lock()
|
||||
if self.direction == 'out':
|
||||
if self.transmission_id:
|
||||
return
|
||||
try:
|
||||
self.transmission_id = self.service.post(self)
|
||||
self.save()
|
||||
except PeppolServiceError as e:
|
||||
self.fail(status=str(e))
|
||||
elif self.direction == 'in':
|
||||
if self.type == 'bis-billing-3':
|
||||
if self.invoice:
|
||||
return
|
||||
self.invoice = Invoice.parse(self.data)
|
||||
self.save()
|
||||
self.succeed()
|
||||
|
||||
@classmethod
|
||||
def update_status(cls, documents=None):
|
||||
if documents is None:
|
||||
documents = cls.search([
|
||||
('direction', '=', 'out'),
|
||||
('state', '=', 'processing'),
|
||||
])
|
||||
for document in documents:
|
||||
document._update_status()
|
||||
cls.save(documents)
|
||||
|
||||
def _update_status(self):
|
||||
assert self.direction == 'out'
|
||||
self.service.update_status(self)
|
||||
|
||||
@dualmethod
|
||||
@Workflow.transition('succeeded')
|
||||
def succeed(cls, documents, status=None):
|
||||
cls.write(documents, {'status': status})
|
||||
|
||||
@dualmethod
|
||||
@Workflow.transition('failed')
|
||||
def fail(cls, documents, status=None):
|
||||
cls.write(documents, {'status': status})
|
||||
|
||||
@classmethod
|
||||
@Workflow.transition('retried')
|
||||
def retry(cls, documents):
|
||||
retries = cls.copy(documents)
|
||||
for document, retry in zip(documents, retries):
|
||||
document.document_retried = retry
|
||||
cls.save(documents)
|
||||
cls.submit(retries)
|
||||
|
||||
@classmethod
|
||||
@Workflow.transition('cancelled')
|
||||
def cancel(cls, documents):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def copy(cls, documents, default=None):
|
||||
default = default.copy() if default is not None else {}
|
||||
default.setdefault('service')
|
||||
default.setdefault('data')
|
||||
default.setdefault('transmission_id')
|
||||
default.setdefault('document_retried')
|
||||
default.setdefault('status')
|
||||
return super().copy(documents, default=default)
|
||||
|
||||
|
||||
class PeppolService(sequence_ordered(), MatchMixin, ModelSQL, ModelView):
|
||||
__name__ = 'edocument.peppol.service'
|
||||
|
||||
company = fields.Many2One(
|
||||
'company.company', "Company", required=True)
|
||||
service = fields.Selection([
|
||||
], "Service")
|
||||
types = fields.MultiSelection(
|
||||
'get_peppol_types', "Types",
|
||||
help="The types of document supported by the service provider.")
|
||||
|
||||
@classmethod
|
||||
def default_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@classmethod
|
||||
def get_service(cls, document):
|
||||
pattern = document.get_service_pattern()
|
||||
for service in cls.search([('company', '=', document.company)]):
|
||||
if service.match(pattern):
|
||||
return service
|
||||
|
||||
def match(self, pattern, match_none=False):
|
||||
if 'type' in pattern:
|
||||
pattern = pattern.copy()
|
||||
if pattern.pop('type') not in self.types:
|
||||
return False
|
||||
return super().match(pattern, match_none=match_none)
|
||||
|
||||
@classmethod
|
||||
def get_peppol_types(cls):
|
||||
pool = Pool()
|
||||
Peppol = pool.get('edocument.peppol')
|
||||
return [
|
||||
(v, l) for v, l in Peppol.fields_get(['type'])['type']['selection']
|
||||
if v is not None]
|
||||
|
||||
@classmethod
|
||||
def default_types(cls):
|
||||
return ['bis-billing-3']
|
||||
|
||||
def post(self, document):
|
||||
if meth := getattr(self, f'_post_{self.service}', None):
|
||||
return meth(document)
|
||||
|
||||
def update_status(self, document):
|
||||
if meth := getattr(self, f'_update_status_{self.service}', None):
|
||||
return meth(document)
|
||||
Reference in New Issue
Block a user