Files
tryton/account.py
T
2018-12-29 14:20:28 +01:00

230 lines
7.6 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import base64
import datetime
import logging
import requests
from trytond.i18n import gettext
from trytond.config import config
from trytond.model import ModelSQL, ModelView, Workflow, Unique, fields
from trytond.model.exceptions import AccessError
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval, Bool, If
from trytond.transaction import Transaction
from trytond.modules.company.model import CompanyValueMixin
from .exceptions import InvoiceChorusValidationError
CERT = (
config.get('account_fr_chorus', 'certificate'),
config.get('account_fr_chorus', 'privatekey'))
BASE_URL = config.get(
'account_fr_chorus', 'url', default='https://chorus-pro.gouv.fr:5443')
EDOC2SYNTAX = {
'edocument.uncefact.invoice': 'IN_DP_E1_CII_16B',
}
EDOC2FILENAME = {
'edocument.uncefact.invoice': 'UNCEFACT-%s.xml',
}
logger = logging.getLogger(__name__)
class _SyntaxMixin(object):
@classmethod
def get_syntaxes(cls):
pool = Pool()
syntaxes = [(None, "")]
try:
doc = pool.get('edocument.uncefact.invoice')
except KeyError:
pass
else:
syntaxes.append((doc.__name__, "CII"))
return syntaxes
class Configuration(_SyntaxMixin, metaclass=PoolMeta):
__name__ = 'account.configuration'
_states = {
'required': Bool(Eval('chorus_login')),
}
_depends = ['chorus_login']
chorus_login = fields.MultiValue(fields.Char("Chorus Login"))
chorus_password = fields.MultiValue(fields.Char(
"Chorus Password", states=_states, depends=_depends))
chorus_service = fields.MultiValue(fields.Selection([
(None, ""),
('service-qualif', "Qualification"),
('service', "Production"),
], "Chorus Service", states=_states, depends=_depends))
chorus_syntax = fields.Selection(
'get_syntaxes', "Chorus Syntax", states=_states, depends=_depends)
del _states, _depends
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field in {'chorus_login', 'chorus_password', 'chorus_service'}:
return pool.get('account.credential.chorus')
return super(Configuration, cls).multivalue_model(field)
class CredentialChorus(ModelSQL, CompanyValueMixin):
"Account Credential Chorus"
__name__ = 'account.credential.chorus'
chorus_login = fields.Char("Login")
chorus_password = fields.Char("Password")
chorus_service = fields.Selection([
(None, ""),
('service-qualif', "Qualification"),
('service', "Production"),
], "Service")
@classmethod
def post(cls, path, payload):
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
url = '/'.join((BASE_URL, config.chorus_service, path))
auth = (config.chorus_login, config.chorus_password)
resp = requests.post(
url, json=payload, cert=CERT, verify=True, auth=auth)
resp.raise_for_status()
return resp.json()
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
@classmethod
@ModelView.button
@Workflow.transition('posted')
def post(cls, invoices):
pool = Pool()
InvoiceChorus = pool.get('account.invoice.chorus')
super(Invoice, cls).post(invoices)
invoices_chorus = []
for invoice in invoices:
if invoice.type == 'out' and invoice.party.chorus:
invoices_chorus.append(InvoiceChorus(invoice=invoice))
InvoiceChorus.save(invoices_chorus)
class InvoiceChorus(ModelSQL, ModelView, _SyntaxMixin, metaclass=PoolMeta):
"Invoice Chorus"
__name__ = 'account.invoice.chorus'
invoice = fields.Many2One(
'account.invoice', "Invoice", required=True,
domain=[
('type', '=', 'out'),
('state', 'in', If(Bool(Eval('number')),
['posted', 'paid'],
['posted'])),
],
depends=['number'])
syntax = fields.Selection('get_syntaxes', "Syntax", required=True)
number = fields.Char("Number", readonly=True)
date = fields.Date("Date", readonly=True)
@classmethod
def __setup__(cls):
super(InvoiceChorus, cls).__setup__()
t = cls.__table__()
cls._sql_constraints = [
('invoice_unique', Unique(t, t.invoice),
'account_fr_chorus.msg_invoice_unique'),
]
@classmethod
def default_syntax(cls):
pool = Pool()
Configuration = pool.get('account.configuration')
config = Configuration(1)
return config.chorus_syntax
@classmethod
def validate(cls, records):
super(InvoiceChorus, cls).validate(records)
for record in records:
addresses = [
record.invoice.company.party.address_get('invoice'),
record.invoice.invoice_address]
for address in addresses:
if not address.siret:
raise InvoiceChorusValidationError(
gettext('account_fr_chorus'
'.msg_invoice_address_no_siret',
invoice=record.invoice.rec_name,
address=address.rec_name))
@classmethod
def delete(cls, records):
for record in records:
if record.number:
raise AccessError(
gettext('account_fr_chorus.msg_invoice_delete_sent',
invoice=record.invoice.rec_name))
super(InvoiceChorus, cls).delete(records)
def _send_context(self):
return {
'company': self.invoice.company.id,
}
@classmethod
def send(cls, records=None):
"""Send invoice to Chorus
The transaction is committed after each invoice.
"""
pool = Pool()
Credential = pool.get('account.credential.chorus')
transaction = Transaction()
database = transaction.database
connection = transaction.connection
database.lock(connection, cls._table)
if not records:
records = cls.search(['OR',
('number', '=', None),
('number', '=', ''),
])
for record in records:
with Transaction().set_context(**record._send_context()):
payload = record.get_payload()
resp = Credential.post('factures/deposer/flux', payload)
if resp['codeRetour']:
logger.error(
"Error when sending invoice %d to chorus: %s",
record.id, resp['libelle'])
else:
record.number = resp['numeroFluxDepot']
record.date = datetime.datetime.strptime(
resp['dateDepot'], '%Y-%m-%d').date()
record.save()
Transaction().commit()
def get_payload(self):
pool = Pool()
Doc = pool.get(self.syntax)
with Transaction().set_context(account_fr_chorus=True):
data = Doc(self.invoice).render(None)
filename = EDOC2FILENAME[self.syntax] % self.invoice.number
filename = filename.replace('/', '-')
return {
'fichierFlux': base64.b64encode(data),
'nomFichier': filename,
'syntaxeFlux': EDOC2SYNTAX[self.syntax],
'avecSignature': False,
}