Skip to content

Commit

Permalink
[MIG] l10n_fr_chorus_*: migrate to v18
Browse files Browse the repository at this point in the history
Switch to new native field invoice_sending_method
Add test button on config page to test API
Large code cleanup
  • Loading branch information
alexis-via committed Jan 6, 2025
1 parent 64e2eb8 commit 31e5620
Show file tree
Hide file tree
Showing 24 changed files with 400 additions and 378 deletions.
8 changes: 3 additions & 5 deletions l10n_fr_chorus_account/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,21 @@
"name": "L10n FR Chorus",
"summary": "Generate Chorus-compliant e-invoices and transmit them "
"via the Chorus API",
"version": "17.0.1.0.1",
"version": "18.0.1.0.0",
"category": "French Localization",
"author": "Akretion,Odoo Community Association (OCA)",
"maintainers": ["alexis-via"],
"website": "https://github.com/OCA/l10n-france",
"license": "AGPL-3",
"depends": [
"l10n_fr_siret",
"account_invoice_transmit_method",
"l10n_fr_siret_account",
"server_environment",
],
"external_dependencies": {"python": ["requests_oauthlib"]},
"data": [
"security/group.xml",
"security/ir.model.access.csv",
"data/transmit_method.xml",
"data/cron.xml",
"data/ir_cron.xml",
"data/mail_template.xml",
"wizard/account_invoice_chorus_send_view.xml",
"views/chorus_flow.xml",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<!-- don't limit the number of calls -->
<field name="model_id" ref="model_chorus_flow" />
<field name="state">code</field>
<field name="code">model.chorus_cron()</field>
Expand All @@ -23,8 +21,6 @@
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">weeks</field>
<field name="numbercall">-1</field>
<!-- don't limit the number of calls -->
<field name="model_id" ref="base.model_res_partner" />
<field name="state">code</field>
<field name="code">model.chorus_cron()</field>
Expand All @@ -35,8 +31,6 @@
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<!-- don't limit the number of calls -->
<field name="model_id" ref="base.model_res_company" />
<field name="state">code</field>
<field name="code">model.chorus_api_expiry_reminder_cron()</field>
Expand Down
13 changes: 0 additions & 13 deletions l10n_fr_chorus_account/data/transmit_method.xml

This file was deleted.

4 changes: 2 additions & 2 deletions l10n_fr_chorus_account/demo/demo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<field name="website">http://www.education.gouv.fr/</field>
<field name="siren">110043015</field>
<field name="nic">00012</field>
<field name="customer_invoice_transmit_method_id" ref="chorus" />
<field name="invoice_sending_method">fr_chorus</field>
<field name="fr_chorus_required">engagement</field>
</record>
<record id="saam_d5_service" model="chorus.partner.service">
Expand Down Expand Up @@ -56,7 +56,7 @@
<field name="website">http://www.aphp.fr/</field>
<field name="siren">267500452</field>
<field name="nic">00011</field>
<field name="customer_invoice_transmit_method_id" ref="chorus" />
<field name="invoice_sending_method">fr_chorus</field>
<field name="fr_chorus_required">engagement</field>
</record>
<record id="necker_061_service" model="chorus.partner.service">
Expand Down
108 changes: 54 additions & 54 deletions l10n_fr_chorus_account/models/account_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,18 @@
class AccountMove(models.Model):
_inherit = "account.move"

# The related field below should be native... I hope we won't have conflict issues
# if another module defines the same related field.
invoice_sending_method = fields.Selection(
related="commercial_partner_id.invoice_sending_method", store=True
)
chorus_flow_id = fields.Many2one(
"chorus.flow", string="Chorus Flow", readonly=True, copy=False, tracking=True
"chorus.flow",
string="Chorus Flow",
readonly=True,
copy=False,
tracking=True,
check_company=True,
)
chorus_identifier = fields.Integer(
string="Chorus Invoice Identifier", readonly=True, copy=False, tracking=True
Expand All @@ -79,15 +89,16 @@ class AccountMove(models.Model):
"account_move_chorus_ir_attachment_rel",
string="Chorus Attachments",
copy=False,
check_company=True,
)

@api.constrains("chorus_attachment_ids", "transmit_method_id")
@api.constrains("chorus_attachment_ids", "invoice_sending_method")
def _check_chorus_attachments(self):
# https://communaute.chorus-pro.gouv.fr/pieces-jointes-dans-chorus-pro-quelques-regles-a-respecter/ # noqa: B950,E501
for move in self:
if (
move.move_type in ("out_invoice", "out_refund")
and move.transmit_method_code == "fr-chorus"
and move.invoice_sending_method == "fr_chorus"
):
total_size = 0
for attach in move.chorus_attachment_ids:
Expand All @@ -98,13 +109,11 @@ def _check_chorus_attachments(self):
" is %(filename_max)s caracters maximum"
" (extension included)."
" The filename '%(filename)s' has %(filename_size)s"
" caracters."
" caracters.",
filename_max=CHORUS_FILENAME_MAX,
filename=attach.name,
filename_size=len(attach.name),
)
% {
"filename_max": CHORUS_FILENAME_MAX,
"filename": attach.name,
"filename_size": len(attach.name),
}
)
filename, file_extension = os.path.splitext(attach.name)
if not file_extension:
Expand All @@ -122,12 +131,10 @@ def _check_chorus_attachments(self):
"On Chorus Pro, the allowed formats for the "
"attachments are the following: %(extension_list)s.\n"
"The attachment '%(filename)s'"
" is not part of this list."
" is not part of this list.",
extension_list=", ".join(CHORUS_ALLOWED_FORMATS),
filename=attach.name,
)
% {
"extension_list": ", ".join(CHORUS_ALLOWED_FORMATS),
"filename": attach.name,
}
)
if not attach.file_size:
raise ValidationError(
Expand All @@ -140,13 +147,11 @@ def _check_chorus_attachments(self):
_(
"On Chorus Pro, each attachment"
" cannot exceed %(size_max)s Mb. "
"The attachment '%(filename)s' weights %(size)s Mb."
"The attachment '%(filename)s' weights %(size)s Mb.",
size_max=CHORUS_FILESIZE_MAX_MO,
filename=attach.name,
size=formatLang(self.env, filesize_mo),
)
% {
"size_max": CHORUS_FILESIZE_MAX_MO,
"filename": attach.name,
"size": formatLang(self.env, filesize_mo),
}
)
if total_size:
total_size_mo = round(total_size / (1024 * 1024), 1)
Expand All @@ -156,45 +161,43 @@ def _check_chorus_attachments(self):
"On Chorus Pro, an invoice with its attachments "
"cannot exceed %(size_max)s Mb, so we set a limit of "
"%(attach_size_max)s Mb for the attachments. "
"The attachments have a total size of %(size)s Mb."
"The attachments have a total size of %(size)s Mb.",
size_max=CHORUS_TOTAL_FILESIZE_MAX_MO,
attach_size_max=CHORUS_TOTAL_ATTACHMENTS_MAX_MO,
size=formatLang(self.env, total_size_mo),
)
% {
"size_max": CHORUS_TOTAL_FILESIZE_MAX_MO,
"attach_size_max": CHORUS_TOTAL_ATTACHMENTS_MAX_MO,
"size": formatLang(self.env, total_size_mo),
}
)

def action_post(self):
def _post(self, soft=True):
"""Check validity of Chorus invoices"""
for inv in self.filtered(
for move in self.filtered(
lambda x: x.move_type in ("out_invoice", "out_refund")
and x.transmit_method_code == "fr-chorus"
and x.invoice_sending_method == "fr_chorus"
):
inv._chorus_validation_checks()
return super().action_post()
move._chorus_validation_checks()
return super()._post(soft=soft)

def _chorus_validation_checks(self):
self.ensure_one()
self.company_id._chorus_common_validation_checks(
self, self.partner_id, self.ref
)
if self.move_type == "out_invoice":
if not self.payment_mode_id:
if not self.preferred_payment_method_line_id:
raise UserError(
_(
"Missing Payment Mode on invoice '%s'. "
"Missing Payment Method on invoice '%s'. "
"This information is required for Chorus Pro."
)
% self.display_name
)
payment_means_code = (
self.payment_mode_id.payment_method_id.unece_code or "30"
self.preferred_payment_method_line_id.payment_method_id.unece_code
or "30"
)
if payment_means_code in CREDIT_TRF_CODES:
partner_bank_id = self.partner_bank_id or (
self.payment_mode_id.bank_account_link == "fixed"
and self.payment_mode_id.fixed_journal_id.bank_account_id
self.preferred_payment_method_line_id.journal_id.bank_account_id
)
if not partner_bank_id:
raise UserError(
Expand All @@ -207,36 +210,34 @@ def _chorus_validation_checks(self):
"'fixed' and the related bank journal should have "
"a 'Bank Account' set, or the field "
"'Bank Account' should be set on the customer "
"invoice."
"invoice.",
invoice=self.display_name,
company=self.company_id.display_name,
)
% {
"invoice": self.display_name,
"company": self.company_id.display_name,
}
)
if partner_bank_id.acc_type != "iban":
raise UserError(
_(
"Chorus Pro only accepts IBAN. But the bank account "
"'%(acc_number)s' of %(company)s is not an IBAN."
"'%(acc_number)s' of %(company)s is not an IBAN.",
acc_number=partner_bank_id.acc_number,
company=self.company_id.display_name,
)
% {
"acc_number": partner_bank_id.acc_number,
"company": self.company_id.display_name,
}
)
elif self.move_type == "out_refund":
if self.payment_mode_id:
if self.preferred_payment_method_line_id:
raise UserError(
_(
"The Payment Mode must be empty on %s "
"The Payment Method must be empty on %s "
"because customer refunds sent to Chorus Pro mustn't "
"have a Payment Mode."
"have a Payment Method."
)
% self.display_name
)

def chorus_get_invoice(self, chorus_invoice_format):
def _chorus_get_invoice(self, chorus_invoice_format):
"""Method inherited in format-specific modules,
such as l10n_fr_chorus_facturx"""
self.ensure_one()
return False

Expand All @@ -256,23 +257,22 @@ def _prepare_chorus_deposer_flux_payload(self):
chorus_invoice_format
]
if len(self) == 1:
chorus_file_content = self.chorus_get_invoice(chorus_invoice_format)
chorus_file_content = self._chorus_get_invoice(chorus_invoice_format)
inv_name = self.name.replace("/", "-")
filename = f"{short_format}_chorus_facture_{inv_name}.{file_ext}"
else:
filename = f"{short_format}_chorus_lot_factures.tar.gz"
tarfileobj = BytesIO()
with tarfile.open(fileobj=tarfileobj, mode="w:gz") as tar:
for inv in self:
inv_file_data = inv.chorus_get_invoice(chorus_invoice_format)
inv_file_data = inv._chorus_get_invoice(chorus_invoice_format)
invfileio = BytesIO(inv_file_data)
inv_name = inv.name.replace("/", "-")
invfilename = f"{short_format}_chorus_facture_{inv_name}.{file_ext}"
tarinfo = tarfile.TarInfo(name=invfilename)
tarinfo.size = len(inv_file_data)
tarinfo.mtime = int(time.time())
tar.addfile(tarinfo=tarinfo, fileobj=invfileio)
tar.close()
tarfileobj.seek(0)
chorus_file_content = tarfileobj.read()
payload = {
Expand Down Expand Up @@ -344,7 +344,7 @@ def _fr_chorus_send(self):
for invoice in self:
assert invoice.state == "posted"
assert invoice.move_type in ("out_invoice", "out_refund")
assert invoice.transmit_method_code == "fr-chorus"
assert invoice.invoice_sending_method == "fr_chorus"
assert not invoice.chorus_flow_id
assert invoice.company_id == company
company._check_chorus_invoice_format()
Expand Down
30 changes: 20 additions & 10 deletions l10n_fr_chorus_account/models/chorus_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import logging

from markupsafe import Markup

from odoo import _, api, fields, models
from odoo.exceptions import UserError

Expand All @@ -14,11 +16,16 @@ class ChorusFlow(models.Model):
_name = "chorus.flow"
_description = "Chorus Flow"
_order = "id desc"
_check_company_auto = True

name = fields.Char("Flow Ref", readonly=True, copy=False, required=True)
date = fields.Date("Flow Date", readonly=True, copy=False, required=True)
attachment_id = fields.Many2one(
"ir.attachment", string="File Sent to Chorus", readonly=True, copy=False
"ir.attachment",
string="File Sent to Chorus",
readonly=True,
copy=False,
check_company=True,
)
status = fields.Char(string="Flow Status (raw value)", readonly=True, copy=False)
status_display = fields.Char(
Expand Down Expand Up @@ -46,6 +53,7 @@ class ChorusFlow(models.Model):
"move_id",
string="Initial Invoices",
readonly=True,
check_company=True,
help="Invoices in the flow before potential rejections",
)
invoice_ids = fields.One2many(
Expand Down Expand Up @@ -138,14 +146,16 @@ def _chorus_api_consulter_cr(self, api_params, session=None):
)
if invoice:
invoice.message_post(
body=_(
"This invoice has been "
"<b>rejected by Chorus Pro</b> "
"for the following reason:<br/><i>%s</i><br/>"
"You should fix the error and send this invoice to "
"Chorus Pro again."
body=Markup(
_(
"This invoice has been "
"<b>rejected by Chorus Pro</b> "
"for the following reason:<br/><i>%s</i><br/>"
"You should fix the error and send this "
"invoice to Chorus Pro again."
)
% error.get("libelleErreurDP")
)
% error.get("libelleErreurDP")
)
invoice.sudo().write({"chorus_flow_id": False})
if not notes and answer.get("libelle") != "TRA_MSG_00.000":
Expand Down Expand Up @@ -221,7 +231,7 @@ def get_invoice_identifiers(self):
"On flow %s, the status is not 'INTEGRE' "
"nor 'INTEGRE PARTIEL'."
)
% (flow.name, flow.status)
% flow.name
)
logger.warning(
"Skipping flow %s: chorus flow status should be "
Expand Down Expand Up @@ -281,7 +291,7 @@ def chorus_cron(self):
[
("state", "=", "posted"),
("move_type", "in", ("out_invoice", "out_refund")),
("transmit_method_code", "=", "fr-chorus"),
("invoice_sending_method", "=", "fr_chorus"),
("chorus_identifier", "!=", False),
("chorus_status", "not in", ("MANDATEE", "MISE_EN_PAIEMENT")),
]
Expand Down
Loading

0 comments on commit 31e5620

Please sign in to comment.