diff --git a/l10n_fr_chorus_account/__manifest__.py b/l10n_fr_chorus_account/__manifest__.py
index eead14225..2a35d346d 100644
--- a/l10n_fr_chorus_account/__manifest__.py
+++ b/l10n_fr_chorus_account/__manifest__.py
@@ -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",
diff --git a/l10n_fr_chorus_account/data/cron.xml b/l10n_fr_chorus_account/data/ir_cron.xml
similarity index 86%
rename from l10n_fr_chorus_account/data/cron.xml
rename to l10n_fr_chorus_account/data/ir_cron.xml
index 356d6b01f..0b4b37697 100644
--- a/l10n_fr_chorus_account/data/cron.xml
+++ b/l10n_fr_chorus_account/data/ir_cron.xml
@@ -11,8 +11,6 @@
1
days
- -1
-
code
model.chorus_cron()
@@ -23,8 +21,6 @@
1
weeks
- -1
-
code
model.chorus_cron()
@@ -35,8 +31,6 @@
1
days
- -1
-
code
model.chorus_api_expiry_reminder_cron()
diff --git a/l10n_fr_chorus_account/data/transmit_method.xml b/l10n_fr_chorus_account/data/transmit_method.xml
deleted file mode 100644
index 31cdd1e0d..000000000
--- a/l10n_fr_chorus_account/data/transmit_method.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
- fr-chorus
- Chorus Pro
-
-
-
diff --git a/l10n_fr_chorus_account/demo/demo.xml b/l10n_fr_chorus_account/demo/demo.xml
index 7f02dfbce..1cf5d2535 100644
--- a/l10n_fr_chorus_account/demo/demo.xml
+++ b/l10n_fr_chorus_account/demo/demo.xml
@@ -17,7 +17,7 @@
http://www.education.gouv.fr/
110043015
00012
-
+ fr_chorus
engagement
@@ -56,7 +56,7 @@
http://www.aphp.fr/
267500452
00011
-
+ fr_chorus
engagement
diff --git a/l10n_fr_chorus_account/models/account_move.py b/l10n_fr_chorus_account/models/account_move.py
index 77531d852..1c6cc5388 100644
--- a/l10n_fr_chorus_account/models/account_move.py
+++ b/l10n_fr_chorus_account/models/account_move.py
@@ -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
@@ -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:
@@ -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:
@@ -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(
@@ -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)
@@ -156,23 +161,21 @@ 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()
@@ -180,21 +183,21 @@ def _chorus_validation_checks(self):
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(
@@ -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
@@ -256,7 +257,7 @@ 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:
@@ -264,7 +265,7 @@ def _prepare_chorus_deposer_flux_payload(self):
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}"
@@ -272,7 +273,6 @@ def _prepare_chorus_deposer_flux_payload(self):
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 = {
@@ -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()
diff --git a/l10n_fr_chorus_account/models/chorus_flow.py b/l10n_fr_chorus_account/models/chorus_flow.py
index e1f37b5f6..d3a496c18 100644
--- a/l10n_fr_chorus_account/models/chorus_flow.py
+++ b/l10n_fr_chorus_account/models/chorus_flow.py
@@ -4,6 +4,8 @@
import logging
+from markupsafe import Markup
+
from odoo import _, api, fields, models
from odoo.exceptions import UserError
@@ -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(
@@ -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(
@@ -138,14 +146,16 @@ def _chorus_api_consulter_cr(self, api_params, session=None):
)
if invoice:
invoice.message_post(
- body=_(
- "This invoice has been "
- "rejected by Chorus Pro "
- "for the following reason:
%s
"
- "You should fix the error and send this invoice to "
- "Chorus Pro again."
+ body=Markup(
+ _(
+ "This invoice has been "
+ "rejected by Chorus Pro "
+ "for the following reason:
%s
"
+ "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":
@@ -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 "
@@ -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")),
]
diff --git a/l10n_fr_chorus_account/models/chorus_partner_service.py b/l10n_fr_chorus_account/models/chorus_partner_service.py
index 92ace0af8..ae816af9d 100644
--- a/l10n_fr_chorus_account/models/chorus_partner_service.py
+++ b/l10n_fr_chorus_account/models/chorus_partner_service.py
@@ -15,6 +15,7 @@ class ChorusPartnerService(models.Model):
_name = "chorus.partner.service"
_description = "Chorus Services attached to a partner"
_order = "partner_id, code"
+ _rec_names_search = ["name", "code"]
partner_id = fields.Many2one(
"res.partner",
@@ -61,31 +62,6 @@ def _compute_display_name(self):
)
]
- @api.model
- def _name_search(self, name, domain=None, operator="ilike", limit=None, order=None):
- if domain is None:
- domain = []
- if name and operator == "ilike":
- ids = list(
- self._search(
- [("code", "=ilike", name)] + domain, limit=limit, order=order
- )
- )
- if ids:
- return ids
- ids = list(
- self._search(
- ["|", ("code", "ilike", name), ("name", "ilike", name)] + domain,
- limit=limit,
- order=order,
- )
- )
- if ids:
- return ids
- return super()._name_search(
- name, domain=domain, operator=operator, limit=limit, order=order
- )
-
def _api_consulter_service(self, api_params, session):
assert self.chorus_identifier
url_path = "structures/v1/consulter/service"
@@ -118,12 +94,10 @@ def service_update(self):
raise UserError(
_(
"Missing Chorus Identifier on service '%(service_name)s' "
- "of partner '%(partner_name)s'."
+ "of partner '%(partner_name)s'.",
+ service_name=service.display_name,
+ partner_name=partner.display_name,
)
- % {
- "service_name": service.display_name,
- "partner_name": partner.display_name,
- }
)
else:
logger.warning(
diff --git a/l10n_fr_chorus_account/models/res_company.py b/l10n_fr_chorus_account/models/res_company.py
index 56afebb81..fcb1dfc71 100644
--- a/l10n_fr_chorus_account/models/res_company.py
+++ b/l10n_fr_chorus_account/models/res_company.py
@@ -12,6 +12,7 @@
from odoo import _, api, fields, models, tools
from odoo.exceptions import UserError
+from odoo.tools.misc import format_date
logger = logging.getLogger(__name__)
@@ -37,9 +38,6 @@ class ResCompany(models.Model):
fr_chorus_api_password = fields.Char(
string="Chorus Technical User Password", groups="base.group_system"
)
- fr_chorus_qualif = fields.Boolean(
- "Chorus Test Mode", help="Use the Chorus Pro qualification website"
- )
# The values of the selection field below should
# start with either 'xml_' or 'pdf_'
fr_chorus_invoice_format = fields.Selection([], string="Chorus Invoice Format")
@@ -69,7 +67,6 @@ def _server_env_fields(self):
{
"fr_chorus_api_login": {},
"fr_chorus_api_password": {},
- "fr_chorus_qualif": {},
}
)
return env_fields
@@ -101,6 +98,15 @@ def _chorus_get_piste_api_oauth_identifiers(self, raise_if_ko=False):
return False
return (oauth_id, oauth_secret)
+ def _chorus_qualif(self):
+ self.ensure_one()
+ # Warning: with the OCA module server_environment,
+ # when there is no key 'running_env' in the odoo server config file,
+ # it will return "test" as a "safe default"
+ running_env = tools.config.get("running_env", "prod")
+ qualif = running_env in ("test", "dev")
+ return qualif
+
def _chorus_get_api_params(self, raise_if_ko=False):
self.ensure_one()
api_params = {}
@@ -116,7 +122,7 @@ def _chorus_get_api_params(self, raise_if_ko=False):
api_params = {
"login": self.sudo().fr_chorus_api_login,
"password": self.sudo().fr_chorus_api_password,
- "qualif": self.fr_chorus_qualif,
+ "qualif": self._chorus_qualif(),
"oauth_id": oauth_identifiers[0],
"oauth_secret": oauth_identifiers[1],
}
@@ -139,9 +145,9 @@ def _chorus_get_api_params(self, raise_if_ko=False):
"Chorus API is %s. You should login to Chorus Pro, "
"generate a new password for the technical user and "
"update it in the menu Accounting > Configuration > "
- "Configuration."
+ "Settings."
)
- % self.fr_chorus_pwd_expiry_date
+ % format_date(self.env, self.fr_chorus_pwd_expiry_date)
)
else:
logger.warning(
@@ -173,9 +179,10 @@ def _get_new_token(self, oauth_id, oauth_secret, qualif):
_(
"Connection to PISTE (URL %(url)s) failed. "
"Check the internet connection of the Odoo server.\n\n"
- "Error details: %(error)s"
+ "Error details: %(error)s",
+ url=url,
+ error=e,
)
- % {"url": url, "error": e}
) from e
except requests.exceptions.RequestException as e:
logger.error("PISTE request for new token failed. Error: %s", e)
@@ -209,13 +216,11 @@ def _get_new_token(self, oauth_id, oauth_secret, qualif):
_(
"Error in the request to get a new token via PISTE.\n\n"
"HTTP error code: %(status_code)s. Error type: %(error_type)s. "
- "Error description: %(error_description)s."
+ "Error description: %(error_description)s.",
+ status_code=r.status_code,
+ error_type=token.get("error"),
+ error_description=token.get("error_description"),
)
- % {
- "status_code": r.status_code,
- "error_type": token.get("error"),
- "error_description": token.get("error_description"),
- }
) from None
# {'access_token': 'xxxxxxxxxxxxxxxxx',
# 'token_type': 'Bearer', 'expires_in': 3600, 'scope': 'openid'}
@@ -279,9 +284,10 @@ def _chorus_post(self, api_params, url_path, payload, session=None):
_(
"Connection to Chorus API (URL %(url)s) failed. "
"Check the Internet connection of the Odoo server.\n\n"
- "Error details: %(error)s"
+ "Error details: %(error)s",
+ url=url,
+ error=e,
)
- % {"url": url, "error": e}
) from e
except requests.exceptions.RequestException as e:
logger.error("Chorus POST request failed. Error: %s", e)
@@ -299,12 +305,21 @@ def _chorus_post(self, api_params, url_path, payload, session=None):
r.status_code,
r.text,
)
+ error_label = ""
+ if r.headers.get("Content-Type").startswith("application/json"):
+ try:
+ error_dict = r.json()
+ error_label = error_dict.get("libelle")
+ except Exception:
+ error_label = r.text
raise UserError(
_(
"Wrong request on %(url)s. HTTP error code received from "
- "Chorus: %(status_code)s."
+ "Chorus: %(status_code)s.\nError: %(error)s",
+ url=url,
+ status_code=r.status_code,
+ error=error_label,
)
- % {"url": url, "status_code": r.status_code}
) from None
answer = r.json()
@@ -320,8 +335,8 @@ def chorus_expiry_remind_user_list(self):
@api.model
def chorus_api_expiry_reminder_cron(self):
logger.info("Starting the Chorus Pro API expiry reminder cron")
- today_dt = fields.Date.from_string(fields.Date.context_today(self))
- limit_date = fields.Date.to_string(today_dt + relativedelta(days=15))
+ today_dt = fields.Date.context_today(self)
+ limit_date_dt = today_dt + relativedelta(days=15)
companies = (
self.env["res.company"]
.sudo()
@@ -330,7 +345,7 @@ def chorus_api_expiry_reminder_cron(self):
("fr_chorus_api_password", "!=", False),
("fr_chorus_api_login", "!=", False),
("fr_chorus_pwd_expiry_date", "!=", False),
- ("fr_chorus_pwd_expiry_date", "<=", limit_date),
+ ("fr_chorus_pwd_expiry_date", "<=", limit_date_dt),
]
)
)
@@ -339,18 +354,16 @@ def chorus_api_expiry_reminder_cron(self):
)
for company in companies:
if company.fr_chorus_expiry_remind_user_ids:
- expiry_date_dt = fields.Date.from_string(
- company.fr_chorus_pwd_expiry_date
- )
+ expiry_date_dt = company.fr_chorus_pwd_expiry_date
pwd_days = (expiry_date_dt - today_dt).days
mail_tpl.with_context(pwd_days=pwd_days).send_mail(company.id)
logger.info(
- "The Chorus API expiry reminder has been sent " "for company %s",
+ "The Chorus API expiry reminder has been sent for company %s",
company.name,
)
else:
logger.warning(
- "The Chorus API credentials or certificate will "
+ "The Chorus API credentials will "
"soon expire for company %s but the field "
"fr_chorus_expiry_remind_user_ids is empty!",
company.name,
@@ -378,12 +391,10 @@ def _chorus_common_validation_checks(
raise UserError(
_(
"Missing SIRET on partner '%(partner)s'"
- " linked to company '%(company)s'."
+ " linked to company '%(company)s'.",
+ partner=company_partner.display_name,
+ company=self.display_name,
)
- % {
- "partner": company_partner.display_name,
- "company": self.display_name,
- }
)
cpartner = invoice_partner.commercial_partner_id
if not cpartner.siren or not cpartner.nic:
@@ -404,13 +415,11 @@ def _chorus_common_validation_checks(
"Chorus Pro, so you must select a contact as %(partner_field)s "
"for %(obj_display_name)s and this contact should have a name "
"and a Chorus service and the Chorus service must "
- "be active."
+ "be active.",
+ partner=cpartner.display_name,
+ obj_display_name=obj_display_name,
+ partner_field=partner_field,
)
- % {
- "partner": cpartner.display_name,
- "obj_display_name": obj_display_name,
- "partner_field": partner_field,
- }
)
if cpartner.fr_chorus_required in ("engagement", "service_and_engagement"):
if not client_order_ref:
@@ -420,12 +429,10 @@ def _chorus_common_validation_checks(
"as Engagement required for "
"Chorus Pro, so the 'Customer Reference' "
"of %(obj_display_name)s must "
- "contain a commitment number."
+ "contain a commitment number.",
+ partner=cpartner.display_name,
+ obj_display_name=obj_display_name,
)
- % {
- "partner": cpartner.display_name,
- "obj_display_name": obj_display_name,
- }
)
self._chorus_check_commitment_number(source_object, client_order_ref)
elif (
@@ -439,13 +446,11 @@ def _chorus_common_validation_checks(
"is linked to Chorus service '%(service)s' "
"which is configured with 'Engagement Required', so the "
"'Customer Reference' of %(obj_display_name)s must "
- "contain a commitment number."
+ "contain a commitment number.",
+ partner=invoice_partner.display_name,
+ service=invoice_partner.fr_chorus_service_id.code,
+ obj_display_name=obj_display_name,
)
- % {
- "partner": invoice_partner.display_name,
- "service": invoice_partner.fr_chorus_service_id.code,
- "obj_display_name": obj_display_name,
- }
)
self._chorus_check_commitment_number(source_object, client_order_ref)
if cpartner.fr_chorus_required == "service_or_engagement":
@@ -459,13 +464,11 @@ def _chorus_common_validation_checks(
"is not set and the '%(partner_field)s' "
"is not correctly configured as a service "
"(should be a contact with a Chorus service "
- "and a name)."
+ "and a name).",
+ partner=cpartner.display_name,
+ partner_field=partner_field,
+ obj_display_name=obj_display_name,
)
- % {
- "partner": cpartner.display_name,
- "partner_field": partner_field,
- "obj_display_name": obj_display_name,
- }
)
self._chorus_check_commitment_number(source_object, client_order_ref)
@@ -493,13 +496,11 @@ def _chorus_check_commitment_number(
_(
"On %(obj_display_name)s, the Customer Reference "
"'%(client_order_ref)s' is %(size)s caracters long. "
- "The maximum is 50. Please update the Customer Reference."
+ "The maximum is 50. Please update the Customer Reference.",
+ obj_display_name=source_object.display_name,
+ client_order_ref=client_order_ref,
+ size=len(client_order_ref),
)
- % {
- "obj_display_name": source_object.display_name,
- "client_order_ref": client_order_ref,
- "size": len(client_order_ref),
- }
)
return self._chorus_api_check_commitment_number(
source_object,
@@ -542,12 +543,10 @@ def _chorus_api_check_commitment_number(
_(
"%(obj_display_name)s: Customer Reference "
"'%(client_order_ref)s' not found in Chorus Pro. "
- "Please check the Customer Reference carefully."
+ "Please check the Customer Reference carefully.",
+ obj_display_name=source_object.display_name,
+ client_order_ref=client_order_ref,
)
- % {
- "obj_display_name": source_object.display_name,
- "client_order_ref": client_order_ref,
- }
)
logger.warning(
"%s: commitment number %s not found in Chorus Pro.",
diff --git a/l10n_fr_chorus_account/models/res_partner.py b/l10n_fr_chorus_account/models/res_partner.py
index fdd1274b2..ae20f7d4d 100644
--- a/l10n_fr_chorus_account/models/res_partner.py
+++ b/l10n_fr_chorus_account/models/res_partner.py
@@ -26,6 +26,9 @@ class ResPartner(models.Model):
],
string="Info Required for Chorus",
tracking=True,
+ compute="_compute_fr_chorus_required",
+ store=True,
+ readonly=False,
)
fr_chorus_identifier = fields.Integer("Chorus Identifier", readonly=True)
fr_chorus_service_count = fields.Integer(
@@ -44,6 +47,16 @@ class ResPartner(models.Model):
fr_chorus_service_ids = fields.One2many(
"chorus.partner.service", "partner_id", string="Chorus Services"
)
+ # inherit field of the account module (field added in odoo 18.0)
+ invoice_sending_method = fields.Selection(
+ selection_add=[("fr_chorus", "Chorus Pro")], ondelete={"fr_chorus": "set null"}
+ )
+
+ @api.depends("invoice_sending_method")
+ def _compute_fr_chorus_required(self):
+ self.filtered(
+ lambda x: x.invoice_sending_method != "fr_chorus"
+ ).fr_chorus_required = False
def _compute_fr_chorus_service_count(self):
rg_res = self.env["chorus.partner.service"]._read_group(
@@ -65,12 +78,10 @@ def _check_fr_chorus_service(self):
"Chorus service codes can only be set on contacts, "
"not on parent partners. Chorus service code "
"'%(service_code)s' has been set on "
- "partner %(partner_name)s that has no parent."
+ "partner %(partner_name)s that has no parent.",
+ service_code=partner.fr_chorus_service_id.code,
+ partner_name=partner.display_name,
)
- % {
- "service_code": partner.fr_chorus_service_id.code,
- "partner_name": partner.display_name,
- }
)
if not partner.name:
raise ValidationError(
@@ -87,13 +98,11 @@ def _check_fr_chorus_service(self):
_(
"The Chorus Service '%(service_name)s' configured on "
"contact '%(partner_name)s' is attached to another partner "
- "(%(other_partner_name)s)."
+ "(%(other_partner_name)s).",
+ service_name=partner.fr_chorus_service_id.display_name,
+ partner_name=partner.display_name,
+ other_partner_name=chorus_service_partner.display_name,
)
- % {
- "service_name": partner.fr_chorus_service_id.display_name,
- "partner_name": partner.display_name,
- "other_partner_name": chorus_service_partner.display_name,
- }
)
def _fr_chorus_api_structures_rechercher(self, api_params, session=None):
@@ -145,21 +154,20 @@ def fr_chorus_identifier_get(self):
)
continue
if (
- partner.customer_invoice_transmit_method_code != "fr-chorus"
+ partner.invoice_sending_method != "fr_chorus"
and not self.env.context.get("get_company_identifier")
):
if raise_if_ko:
raise UserError(
_(
- "On partner '%s', the invoice transmit method "
+ "On partner '%s', Invoice Sending "
"is not set to 'Chorus Pro'."
)
% partner.display_name
)
else:
logger.warning(
- "Skipping partner %s: invoice transmit method "
- "not set to fr-chorus",
+ "Skipping partner %s: invoice sending not set to chorus pro",
partner.display_name,
)
continue
@@ -435,7 +443,7 @@ def chorus_cron(self):
to_update_partners = self.search(
[
("parent_id", "=", False),
- ("customer_invoice_transmit_method_code", "=", "fr-chorus"),
+ ("invoice_sending_method", "=", "fr_chorus"),
("siren", "!=", False),
("nic", "!=", False),
]
diff --git a/l10n_fr_chorus_account/security/group.xml b/l10n_fr_chorus_account/security/group.xml
index 3be6d5727..8c95f652d 100644
--- a/l10n_fr_chorus_account/security/group.xml
+++ b/l10n_fr_chorus_account/security/group.xml
@@ -9,14 +9,16 @@
Chorus API
+
+ Chorus API Debugging
+
+
Chorus Flow multi-company
- ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
+ [('company_id', 'in', company_ids)]
diff --git a/l10n_fr_chorus_account/security/ir.model.access.csv b/l10n_fr_chorus_account/security/ir.model.access.csv
index 128c49d89..603b157fd 100644
--- a/l10n_fr_chorus_account/security/ir.model.access.csv
+++ b/l10n_fr_chorus_account/security/ir.model.access.csv
@@ -1,4 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_chorus_flow_readonly,Read-only access on chorus.flow to account viewer,model_chorus_flow,account.group_account_readonly,1,0,0,0
access_chorus_flow_user,Read/Write/Create access on chorus.flow to invoicing grp,model_chorus_flow,account.group_account_invoice,1,1,1,0
access_chorus_flow_full,Full access on chorus.flow to system grp,model_chorus_flow,base.group_system,1,1,1,1
access_chorus_partner_service_full,Full access on chorus.partner.service to system grp,model_chorus_partner_service,base.group_system,1,1,1,1
diff --git a/l10n_fr_chorus_account/views/account_move.xml b/l10n_fr_chorus_account/views/account_move.xml
index 4e58a5c7e..e90ee8fda 100644
--- a/l10n_fr_chorus_account/views/account_move.xml
+++ b/l10n_fr_chorus_account/views/account_move.xml
@@ -8,13 +8,13 @@
Chorus support on Customer Invoice form view
account.move
-
+
diff --git a/l10n_fr_chorus_account/views/chorus_flow.xml b/l10n_fr_chorus_account/views/chorus_flow.xml
index 452784dcd..0ba1bc914 100644
--- a/l10n_fr_chorus_account/views/chorus_flow.xml
+++ b/l10n_fr_chorus_account/views/chorus_flow.xml
@@ -23,54 +23,62 @@
invisible="status not in ('IN_INTEGRE', 'IN_INTEGRE_PARTIEL') or invoice_identifiers"
/>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- chorus.flow.tree
+ chorus.flow.list
chorus.flow
-
+
@@ -82,14 +90,14 @@
decoration-warning="status == 'IN_INTEGRE_PARTIEL'"
decoration-danger="status == 'IN_REJETE'"
/>
-
+
-
+
@@ -98,6 +106,8 @@
+
+
Chorus Flows
chorus.flow
- tree,form
+ list,form
@@ -37,19 +37,19 @@
chorus.partner.service.tree
chorus.partner.service
-
+
-
+
@@ -88,7 +88,7 @@
Chorus Partner Services
chorus.partner.service
- tree,form
+ list,form
{'chorus_partner_service_main_view': True}
diff --git a/l10n_fr_chorus_account/wizard/res_config_settings.py b/l10n_fr_chorus_account/wizard/res_config_settings.py
index 79144eba1..5efb87555 100644
--- a/l10n_fr_chorus_account/wizard/res_config_settings.py
+++ b/l10n_fr_chorus_account/wizard/res_config_settings.py
@@ -2,7 +2,11 @@
# @author: Alexis de Lattre
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-from odoo import fields, models
+import logging
+
+from odoo import _, fields, models
+
+logger = logging.getLogger(__name__)
class ResConfigSettings(models.TransientModel):
@@ -21,7 +25,9 @@ class ResConfigSettings(models.TransientModel):
related="company_id.fr_chorus_api_password", readonly=False
)
fr_chorus_qualif = fields.Boolean(
- related="company_id.fr_chorus_qualif", readonly=False
+ string="Use Chorus Qualification Platform",
+ readonly=True,
+ default=lambda self: self.env.company._chorus_qualif(),
)
fr_chorus_invoice_format = fields.Selection(
related="company_id.fr_chorus_invoice_format", readonly=False
@@ -35,3 +41,29 @@ class ResConfigSettings(models.TransientModel):
fr_chorus_expiry_remind_user_ids = fields.Many2many(
related="company_id.fr_chorus_expiry_remind_user_ids", readonly=False
)
+
+ def fr_chorus_test_api(self):
+ self.ensure_one()
+ # The code will raise UserError if the test fails
+ logger.info("Start test Chorus Pro API for company %s", self.company_id.name)
+ api_params = self.company_id._chorus_get_api_params(raise_if_ko=True)
+ url_path = "structures/v1/rechercher"
+ payload = {"structure": {"nomStructure": "This is just a test"}}
+ answer, session = self.env["res.company"]._chorus_post(
+ api_params, url_path, payload
+ )
+ message = _(
+ "Successful test of the Chorus Pro API for company '%(company)s'!",
+ company=self.company_id.display_name,
+ )
+ logger.info(message)
+ action = {
+ "type": "ir.actions.client",
+ "tag": "display_notification",
+ "params": {
+ "message": message,
+ "type": "success",
+ "sticky": False,
+ },
+ }
+ return action
diff --git a/l10n_fr_chorus_facturx/__manifest__.py b/l10n_fr_chorus_facturx/__manifest__.py
index 0310e7e1d..fc54b45a0 100644
--- a/l10n_fr_chorus_facturx/__manifest__.py
+++ b/l10n_fr_chorus_facturx/__manifest__.py
@@ -5,7 +5,7 @@
{
"name": "L10n FR Chorus Factur-X",
"summary": "Generate Chorus-compliant Factur-X invoices",
- "version": "17.0.1.0.0",
+ "version": "18.0.1.0.0",
"category": "French Localization",
"author": "Akretion,Odoo Community Association (OCA)",
"maintainers": ["alexis-via"],
diff --git a/l10n_fr_chorus_facturx/models/account_move.py b/l10n_fr_chorus_facturx/models/account_move.py
index 46703f941..ec04d1eee 100644
--- a/l10n_fr_chorus_facturx/models/account_move.py
+++ b/l10n_fr_chorus_facturx/models/account_move.py
@@ -20,7 +20,7 @@ def _cii_trade_agreement_buyer_ref(self, partner):
return partner.fr_chorus_service_id.code
return super()._cii_trade_agreement_buyer_ref(partner)
- def chorus_get_invoice(self, chorus_invoice_format):
+ def _chorus_get_invoice(self, chorus_invoice_format):
self.ensure_one()
if chorus_invoice_format == "xml_cii":
chorus_file_content = self.with_context(
@@ -28,11 +28,11 @@ def chorus_get_invoice(self, chorus_invoice_format):
).generate_facturx_xml()[0]
elif chorus_invoice_format == "pdf_factur-x":
chorus_file_content, filetype = self.env["ir.actions.report"]._render(
- "account.report_invoice", [self.id]
+ "account.report_invoice_with_payments", [self.id]
)
assert filetype == "pdf", "wrong filetype"
else:
- chorus_file_content = super().chorus_get_invoice(chorus_invoice_format)
+ chorus_file_content = super()._chorus_get_invoice(chorus_invoice_format)
return chorus_file_content
def _prepare_facturx_attachments(self):
diff --git a/l10n_fr_chorus_sale/__manifest__.py b/l10n_fr_chorus_sale/__manifest__.py
index 279bbf0de..1b580ffeb 100644
--- a/l10n_fr_chorus_sale/__manifest__.py
+++ b/l10n_fr_chorus_sale/__manifest__.py
@@ -5,14 +5,14 @@
{
"name": "L10n FR Chorus Sale",
"summary": "Add checks on sale orders for Chorus Pro",
- "version": "17.0.1.0.0",
+ "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_chorus_account", "sale"],
- "data": ["views/sale_order.xml"],
+ "data": [],
"installable": True,
"auto_install": True,
}
diff --git a/l10n_fr_chorus_sale/models/sale_order.py b/l10n_fr_chorus_sale/models/sale_order.py
index 68e256b0d..8b932d44f 100644
--- a/l10n_fr_chorus_sale/models/sale_order.py
+++ b/l10n_fr_chorus_sale/models/sale_order.py
@@ -2,24 +2,17 @@
# @author: Alexis de Lattre
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-from odoo import fields, models
+from odoo import models
class SaleOrder(models.Model):
_inherit = "sale.order"
- invoice_transmit_method_id = fields.Many2one(
- related="partner_invoice_id.customer_invoice_transmit_method_id",
- string="Invoice Transmission Method",
- )
- invoice_transmit_method_code = fields.Char(
- related="partner_invoice_id.customer_invoice_transmit_method_id.code",
- )
-
def action_confirm(self):
"""Check validity of Chorus orders"""
for order in self.filtered(
- lambda so: so.invoice_transmit_method_code == "fr-chorus"
+ lambda x: x.partner_invoice_id.commercial_partner_id.invoice_sending_method
+ == "fr_chorus"
):
order._chorus_validation_checks()
return super().action_confirm()
diff --git a/l10n_fr_chorus_sale/views/sale_order.xml b/l10n_fr_chorus_sale/views/sale_order.xml
deleted file mode 100644
index 61dc8db77..000000000
--- a/l10n_fr_chorus_sale/views/sale_order.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
- chorus.sale.order.form
- sale.order
-
-
-
-
-
-
-
-
-