diff --git a/account_banking_fr_lcr/README.rst b/account_banking_fr_lcr/README.rst
index 66c1fa71b..fe9b57566 100644
--- a/account_banking_fr_lcr/README.rst
+++ b/account_banking_fr_lcr/README.rst
@@ -28,23 +28,36 @@ French Letter of Change
|badge1| |badge2| |badge3| |badge4| |badge5|
-This module adds support for French Letters of Change (in French:
-*Lettre de Change Relevé* aka LCR). This module supports direct LCR
-(in French, *LCR Directe*) and not paper LCR.
+This module adds support for French Letters of Change. This module supports:
-This payment type is still in use in France and it is *not* replaced by SEPA
-one-off Direct Debits.
+* **Direct letter of change** (in French : *Lettre de change directe* or *LCR directe*),
+* **Accepted letter of change** (in French : *Lettre de change acceptée* ; I call it *paper letter of change*),
+* **Promissory note** (in French : *Billet à ordre*),
-With this module, you can generate an LCR CFONB file to send to your
-bank. Then, your customer will be notified by their bank about the debit
-(amount, date of debit). Eventually, the debit will take place at the
-planned date.
+It supports cash discounts debit orders and Dailly convention.
+
+This module has 2 main features:
+
+* for **Accepted Letter of Change**, generate a paper letter of change as PDF following the official layout NF K 11-030-1.
+* generate of LCR (or BOR) CFONB files to send to your bank.
+
+This module follows the specifications published on the `CFONB website `_, section *Espace documentaire > Instruments de paiement > Effet de commerce* (document version of September 2002).
**Table of contents**
.. contents::
:local:
+Installation
+============
+
+This module requires 2 Python libs:
+
+* `pypdf `_ version 3.10 or above,
+* `unidecode `_ (any version).
+
+In order to have the SIREN of the company and of the customer set in the CFONB file (optional field) and printed on the paper letter of exchange, the OCA module **l10n_fr_siret** must be installed. The installation of the module **l10n_fr_siret** is optional (because the SIREN field in the CFONB file is optional).
+
Configuration
=============
@@ -52,11 +65,25 @@ To configure this module, you need to create a new payment mode linked
to the payment method *Lettre de Change Relevé* that is automatically
created when you install this module.
+Once you selected this payment method, you will have a new section *Bill of Exchange* on the payment mode where you will have to configure:
+
+* the *LCR type*: *Lettre de change non acceptée (LCR directe)*, *Lettre de change acceptée* or *Billet à ordre*,
+* the *Default Collection Option*,
+* if you have a *Dailly Convention*,
+* in case you have a Dailly convention, you will be able to configure the *Default Dailly Option* and the *Convention Type*.
+
Usage
=====
-To use this module, you need to create a new Debit Order and
-select the LCR payment mode.
+This module adds a new field *Bill of Exchange Bank Account* on customer invoices to select the bank account of the customer that will be debited by the letter of exchange. This bank account must be a french IBAN.
+
+If you configured the payment mode for **Accepted Letter of Change**, you will have a button *Print Bill of Exchange* on customer invoices to get the letter of change as PDF.
+
+This module uses the standard workflow of debit orders as implemented in the OCA module **account_payment_order**. A debit order linked to a payment mode with the payment method *Lettre de change relevé* has a few additionnal constraints:
+
+* all payment lines must be in euro currency,
+* the bank accounts on the payment lines must be french IBANs,
+* if the payment order is configured with cash discount, you must configure the value date on the payment order (new field added by this module).
Bug Tracker
===========
diff --git a/account_banking_fr_lcr/__manifest__.py b/account_banking_fr_lcr/__manifest__.py
index d519d85f6..c96f5d007 100644
--- a/account_banking_fr_lcr/__manifest__.py
+++ b/account_banking_fr_lcr/__manifest__.py
@@ -13,8 +13,13 @@
"website": "https://github.com/OCA/l10n-france",
"category": "French localisation",
"depends": ["account_payment_order"],
- "external_dependencies": {"python": ["unidecode"]},
- "data": ["data/account_payment_method.xml"],
+ "external_dependencies": {"python": ["unidecode", "pypdf>=3.1.0"]},
+ "data": [
+ "data/account_payment_method.xml",
+ "views/account_payment_mode.xml",
+ "views/account_payment_order.xml",
+ "views/account_move.xml",
+ ],
"demo": ["demo/lcr_demo.xml"],
"post_init_hook": "lcr_set_unece",
"installable": True,
diff --git a/account_banking_fr_lcr/data/account_payment_method.xml b/account_banking_fr_lcr/data/account_payment_method.xml
index 09c81a2bf..772ea54f3 100644
--- a/account_banking_fr_lcr/data/account_payment_method.xml
+++ b/account_banking_fr_lcr/data/account_payment_method.xml
@@ -5,6 +5,6 @@
fr_lcrinbound
-
+
diff --git a/account_banking_fr_lcr/i18n/account_banking_fr_lcr.pot b/account_banking_fr_lcr/i18n/account_banking_fr_lcr.pot
index 2b5d2e11f..0675988a7 100644
--- a/account_banking_fr_lcr/i18n/account_banking_fr_lcr.pot
+++ b/account_banking_fr_lcr/i18n/account_banking_fr_lcr.pot
@@ -6,6 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-08-27 10:38+0000\n"
+"PO-Revision-Date: 2024-08-27 10:38+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -13,6 +15,84 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
+#. module: account_banking_fr_lcr
+#: model:ir.model,name:account_banking_fr_lcr.model_res_partner_bank
+msgid "Bank Accounts"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,help:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_partner_bank_id
+#: model:ir.model.fields,help:account_banking_fr_lcr.field_account_move__fr_lcr_partner_bank_id
+#: model:ir.model.fields,help:account_banking_fr_lcr.field_account_payment__fr_lcr_partner_bank_id
+msgid ""
+"Bank account of the customer that will be debited by the bill of exchange. "
+"By default, Odoo selects the first French IBAN bank account of the partner."
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model_terms:ir.ui.view,arch_db:account_banking_fr_lcr.account_payment_mode_form
+msgid "Bill of Exchange"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_attachment_id
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__fr_lcr_attachment_id
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__fr_lcr_attachment_id
+msgid "Bill of Exchange Attachment"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_partner_bank_id
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__fr_lcr_partner_bank_id
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__fr_lcr_partner_bank_id
+msgid "Bill of Exchange Bank Account"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_attachment_datas
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__fr_lcr_attachment_datas
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__fr_lcr_attachment_datas
+msgid "Bill of Exchange File"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_attachment_name
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__fr_lcr_attachment_name
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__fr_lcr_attachment_name
+msgid "Bill of Exchange Filename"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__payment_mode_fr_lcr_type
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__payment_mode_fr_lcr_type
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__payment_mode_fr_lcr_type
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_type
+msgid "Bill of Exchange Type"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields.selection,name:account_banking_fr_lcr.selection__account_payment_mode__fr_lcr_type__promissory_note
+msgid "Billet à ordre"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/res_partner_bank.py:0
+#, python-format
+msgid ""
+"Bills of exchange can only use French bank accounts. The IBAN "
+"'%(acc_number)s' of partner '%(partner)s' is not a French IBAN."
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/res_partner_bank.py:0
+#, python-format
+msgid ""
+"Bills of exchange can only use IBAN bank accounts. Bank account "
+"'%(acc_number)s' of partner '%(partner)s' is not an IBAN."
+msgstr ""
+
#. module: account_banking_fr_lcr
#. odoo-python
#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
@@ -20,6 +100,69 @@ msgstr ""
msgid "Cannot convert the field '%s' to ASCII"
msgstr ""
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_order__fr_lcr_collection_option
+msgid "Collection Option"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_convention_type
+msgid "Convention Type"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_move.py:0
+#, python-format
+msgid ""
+"Customer invoice '%(move)s' is configured with payment mode "
+"'%(payment_mode)s' which require a bill of exchange bank account."
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_dailly
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_order__fr_lcr_dailly
+msgid "Dailly Convention"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_order__fr_lcr_dailly_option
+msgid "Dailly Option"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_default_collection_option
+msgid "Default Collection Option"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_default_dailly_option
+msgid "Default Dailly Option"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid ""
+"Error in the generation of the CFONB file: '%(field)s' should be a string, "
+"but it is %(value_type)s (value: %(value)s)."
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid ""
+"Error in the generation of the CFONB file: the field '%s' is empty or 0. It "
+"should have a non-null value."
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,help:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_convention_type
+msgid "Field C1 'Convention Type' in CFONB header line, 6 characters maximum."
+msgstr ""
+
#. module: account_banking_fr_lcr
#. odoo-python
#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
@@ -30,8 +173,8 @@ msgid ""
msgstr ""
#. module: account_banking_fr_lcr
-#: model:ir.model,name:account_banking_fr_lcr.model_account_move_line
-msgid "Journal Item"
+#: model:ir.model,name:account_banking_fr_lcr.model_account_move
+msgid "Journal Entry"
msgstr ""
#. module: account_banking_fr_lcr
@@ -53,16 +196,71 @@ msgstr ""
msgid "Lettre de Change Relevé"
msgstr ""
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields.selection,name:account_banking_fr_lcr.selection__account_payment_mode__fr_lcr_type__accepted
+msgid "Lettre de change acceptée"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields.selection,name:account_banking_fr_lcr.selection__account_payment_mode__fr_lcr_type__not_accepted
+msgid "Lettre de change non acceptée (LCR directe)"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid ""
+"On debit order '%(order)s', the value date has been set to %(value_date)s: "
+"it must be in the future."
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model,name:account_banking_fr_lcr.model_account_payment_line
+msgid "Payment Lines"
+msgstr ""
+
#. module: account_banking_fr_lcr
#: model:ir.model,name:account_banking_fr_lcr.model_account_payment_method
msgid "Payment Methods"
msgstr ""
+#. module: account_banking_fr_lcr
+#: model:ir.model,name:account_banking_fr_lcr.model_account_payment_mode
+msgid "Payment Modes"
+msgstr ""
+
#. module: account_banking_fr_lcr
#: model:ir.model,name:account_banking_fr_lcr.model_account_payment_order
msgid "Payment Order"
msgstr ""
+#. module: account_banking_fr_lcr
+#: model:ir.model,name:account_banking_fr_lcr.model_account_payment
+msgid "Payments"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model_terms:ir.ui.view,arch_db:account_banking_fr_lcr.view_move_form
+msgid "Print Bill of Exchange"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid "The Collection Option is not set on debit order '%s'."
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_line.py:0
+#, python-format
+msgid ""
+"The currency of payment line '%(payment_line)s' is %(currency)s. To be "
+"included in a french bill of exchange, the currency must be EUR."
+msgstr ""
+
#. module: account_banking_fr_lcr
#. odoo-python
#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
@@ -78,3 +276,24 @@ msgstr ""
#, python-format
msgid "The field '%s' is empty or 0. It should have a non-null value."
msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_mode.py:0
+#, python-format
+msgid "The field 'Bill of Exchange Type' must be set on payment mode '%s'."
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_order__fr_lcr_value_date
+msgid "Value Date"
+msgstr ""
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid ""
+"Value date is not set on debit order '%s'. It is required on letters of "
+"exchange with cash discount."
+msgstr ""
diff --git a/account_banking_fr_lcr/i18n/fr.po b/account_banking_fr_lcr/i18n/fr.po
index f022ac22e..cec6d934f 100644
--- a/account_banking_fr_lcr/i18n/fr.po
+++ b/account_banking_fr_lcr/i18n/fr.po
@@ -1,23 +1,107 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
-# * account_banking_fr_lcr
+# * account_banking_fr_lcr
#
-# Translators:
-# OCA Transbot , 2017
msgid ""
msgstr ""
-"Project-Id-Version: Odoo Server 10.0\n"
+"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-30 10:45+0000\n"
-"PO-Revision-Date: 2023-06-27 18:08+0000\n"
-"Last-Translator: Alexis de Lattre \n"
-"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n"
+"POT-Creation-Date: 2024-08-27 10:39+0000\n"
+"PO-Revision-Date: 2024-08-27 10:40+0000\n"
+"Last-Translator: Alexis de Lattre \n"
+"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
-"Plural-Forms: nplurals=2; plural=n > 1;\n"
-"X-Generator: Weblate 4.17\n"
+"Plural-Forms: \n"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model,name:account_banking_fr_lcr.model_res_partner_bank
+msgid "Bank Accounts"
+msgstr "Comptes bancaires"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,help:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_partner_bank_id
+#: model:ir.model.fields,help:account_banking_fr_lcr.field_account_move__fr_lcr_partner_bank_id
+#: model:ir.model.fields,help:account_banking_fr_lcr.field_account_payment__fr_lcr_partner_bank_id
+msgid ""
+"Bank account of the customer that will be debited by the bill of exchange. "
+"By default, Odoo selects the first French IBAN bank account of the partner."
+msgstr ""
+"Compte bancaire du client qui sera débité par la lettre de change. Par "
+"défaut, Odoo sélectionne le premier IBAN français du "
+"partenaire."
+
+#. module: account_banking_fr_lcr
+#: model_terms:ir.ui.view,arch_db:account_banking_fr_lcr.account_payment_mode_form
+msgid "Bill of Exchange"
+msgstr "Lettre de change"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_attachment_id
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__fr_lcr_attachment_id
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__fr_lcr_attachment_id
+msgid "Bill of Exchange Attachment"
+msgstr "Lettre de change jointe"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_partner_bank_id
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__fr_lcr_partner_bank_id
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__fr_lcr_partner_bank_id
+msgid "Bill of Exchange Bank Account"
+msgstr "Compte bancaire pour la lettre de change"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_attachment_datas
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__fr_lcr_attachment_datas
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__fr_lcr_attachment_datas
+msgid "Bill of Exchange File"
+msgstr "Fichier de lettre de change"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__fr_lcr_attachment_name
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__fr_lcr_attachment_name
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__fr_lcr_attachment_name
+msgid "Bill of Exchange Filename"
+msgstr "Nom du fichier de la lettre de change"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_bank_statement_line__payment_mode_fr_lcr_type
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_move__payment_mode_fr_lcr_type
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment__payment_mode_fr_lcr_type
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_type
+msgid "Bill of Exchange Type"
+msgstr "Type de lettre de change"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields.selection,name:account_banking_fr_lcr.selection__account_payment_mode__fr_lcr_type__promissory_note
+msgid "Billet à ordre"
+msgstr "Billet à ordre"
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/res_partner_bank.py:0
+#, python-format
+msgid ""
+"Bills of exchange can only use French bank accounts. The IBAN "
+"'%(acc_number)s' of partner '%(partner)s' is not a French IBAN."
+msgstr ""
+"Les lettres de change ne peuvent utiliser que des comptes bancaires "
+"français. L'IBAN '%(acc_number)s' du partenaire '%(partner)s' n'est pas un "
+"IBAN français."
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/res_partner_bank.py:0
+#, python-format
+msgid ""
+"Bills of exchange can only use IBAN bank accounts. Bank account "
+"'%(acc_number)s' of partner '%(partner)s' is not an IBAN."
+msgstr ""
+"Les lettres de change ne peuvent utiliser que des comptes bancaires de type IBAN. Le "
+"compte bancaire '%(acc_number)s' du partenaire '%(partner)s' n'est pas un "
+"IBAN."
#. module: account_banking_fr_lcr
#. odoo-python
@@ -26,6 +110,77 @@ msgstr ""
msgid "Cannot convert the field '%s' to ASCII"
msgstr "Impossible de convertir le champ '%s' en ASCII"
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_order__fr_lcr_collection_option
+msgid "Collection Option"
+msgstr "Option d'encaissement"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_convention_type
+msgid "Convention Type"
+msgstr "Type de convention"
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_move.py:0
+#, python-format
+msgid ""
+"Customer invoice '%(move)s' is configured with payment mode "
+"'%(payment_mode)s' which require a bill of exchange bank account."
+msgstr ""
+"La facture client \"%(move)s\" est configurée avec le mode de paiement "
+"\"%(payment_mode)s\" qui nécessite un compte bancaire pour la lettre de change."
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_dailly
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_order__fr_lcr_dailly
+msgid "Dailly Convention"
+msgstr "Convention Dailly"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_order__fr_lcr_dailly_option
+msgid "Dailly Option"
+msgstr "Option Dailly"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_default_collection_option
+msgid "Default Collection Option"
+msgstr "Option d'encaissement par défaut"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_default_dailly_option
+msgid "Default Dailly Option"
+msgstr "Option Dailly par défaut"
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid ""
+"Error in the generation of the CFONB file: '%(field)s' should be a string, "
+"but it is %(value_type)s (value: %(value)s)."
+msgstr ""
+"Erreur dans la génération du fichier CFONB : '%(field)s' devrait être une "
+"chaîne de caractères, mais il s'agit de %(value_type)s (valeur : %(value)s)."
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid ""
+"Error in the generation of the CFONB file: the field '%s' is empty or 0. It "
+"should have a non-null value."
+msgstr ""
+"Erreur dans la génération du fichier CFONB : le champ '%s' est vide ou 0. Il "
+"doit avoir une valeur non nulle."
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,help:account_banking_fr_lcr.field_account_payment_mode__fr_lcr_convention_type
+msgid "Field C1 'Convention Type' in CFONB header line, 6 characters maximum."
+msgstr ""
+"Champ C1 \"Convention Type\" dans la ligne d'en-tête de la CFONB, 6 "
+"caractères maximum."
+
#. module: account_banking_fr_lcr
#. odoo-python
#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
@@ -34,11 +189,13 @@ msgid ""
"For the bank account '%(acc_number)s' of partner '%(partner)s', the Bank "
"Account Type should be 'IBAN'."
msgstr ""
+"Pour le compte bancaire \"%(acc_number)s\" du partenaire \"%(partner)s\", le "
+"type de compte bancaire doit être \"IBAN\"."
#. module: account_banking_fr_lcr
-#: model:ir.model,name:account_banking_fr_lcr.model_account_move_line
-msgid "Journal Item"
-msgstr "Écriture comptable"
+#: model:ir.model,name:account_banking_fr_lcr.model_account_move
+msgid "Journal Entry"
+msgstr "Pièce comptable"
#. module: account_banking_fr_lcr
#: model:account.payment.mode,name:account_banking_fr_lcr.payment_mode_lcr
@@ -53,22 +210,83 @@ msgid ""
"LCR are only for French bank accounts. The IBAN '%(acc_number)s' of partner "
"'%(partner)s' is not a French IBAN."
msgstr ""
+"Les LCR ne concernent que les comptes bancaires français. L'IBAN "
+"'%(acc_number)s' du partenaire '%(partner)s' n'est pas un IBAN français."
#. module: account_banking_fr_lcr
#: model:account.payment.method,name:account_banking_fr_lcr.fr_lcr
msgid "Lettre de Change Relevé"
msgstr "Lettre de Change Relevé"
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields.selection,name:account_banking_fr_lcr.selection__account_payment_mode__fr_lcr_type__accepted
+msgid "Lettre de change acceptée"
+msgstr "Lettre de change acceptée"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields.selection,name:account_banking_fr_lcr.selection__account_payment_mode__fr_lcr_type__not_accepted
+msgid "Lettre de change non acceptée (LCR directe)"
+msgstr "Lettre de change non acceptée (LCR directe)"
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid ""
+"On debit order '%(order)s', the value date has been set to %(value_date)s: "
+"it must be in the future."
+msgstr ""
+"Sur l'ordre de prélèvement '%(order)s', la date de valeur a été fixée à "
+"%(value_date)s: elle doit être dans le futur."
+
+#. module: account_banking_fr_lcr
+#: model:ir.model,name:account_banking_fr_lcr.model_account_payment_line
+msgid "Payment Lines"
+msgstr "Lignes de paiement"
+
#. module: account_banking_fr_lcr
#: model:ir.model,name:account_banking_fr_lcr.model_account_payment_method
msgid "Payment Methods"
-msgstr ""
+msgstr "Méthodes de paiement"
+
+#. module: account_banking_fr_lcr
+#: model:ir.model,name:account_banking_fr_lcr.model_account_payment_mode
+msgid "Payment Modes"
+msgstr "Modes de paiement"
#. module: account_banking_fr_lcr
#: model:ir.model,name:account_banking_fr_lcr.model_account_payment_order
msgid "Payment Order"
msgstr "Ordre de paiement"
+#. module: account_banking_fr_lcr
+#: model:ir.model,name:account_banking_fr_lcr.model_account_payment
+msgid "Payments"
+msgstr "Paiements"
+
+#. module: account_banking_fr_lcr
+#: model_terms:ir.ui.view,arch_db:account_banking_fr_lcr.view_move_form
+msgid "Print Bill of Exchange"
+msgstr "Imprimer la lettre de change"
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
+#, python-format
+msgid "The Collection Option is not set on debit order '%s'."
+msgstr "L'option d'encaissement n'est pas activée pour l'ordre de prélèvement '%s'."
+
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_line.py:0
+#, python-format
+msgid ""
+"The currency of payment line '%(payment_line)s' is %(currency)s. To be "
+"included in a french bill of exchange, the currency must be EUR."
+msgstr ""
+"La devise de la ligne de paiement '%(payment_line)s' est %(currency)s. Pour "
+"être incluse dans une lettre de change française, la devise doit être l'euro."
+
#. module: account_banking_fr_lcr
#. odoo-python
#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
@@ -77,6 +295,8 @@ msgid ""
"The currency of payment line '%(payment_line)s' is '%(currency)s'. To be "
"included in a French LCR, the currency must be EUR."
msgstr ""
+"La devise de la ligne de paiement \"%(payment_line)s\" est \"%(currency)s\". "
+"Pour être incluse dans un LCR français, la devise doit être l'euro."
#. module: account_banking_fr_lcr
#. odoo-python
@@ -86,35 +306,27 @@ msgid "The field '%s' is empty or 0. It should have a non-null value."
msgstr ""
"Le champ '%s' est vide ou égal à 0. Il devrait avoir une valeur non nulle."
-#~ msgid "Display Name"
-#~ msgstr "Nom affiché"
-
-#, python-format
-#~ msgid ""
-#~ "For the bank account '%s' of partner '%s', the Bank Account Type should "
-#~ "be 'IBAN'."
-#~ msgstr ""
-#~ "Pour le compte bancaire '%s' du partenaire '%s', le type de compte "
-#~ "bancaire devrait être 'IBAN'."
-
-#~ msgid "ID"
-#~ msgstr "ID"
-
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_mode.py:0
#, python-format
-#~ msgid ""
-#~ "LCR are only for French bank accounts. The IBAN '%s' of partner '%s' is "
-#~ "not a French IBAN."
-#~ msgstr ""
-#~ "Les LCR ne fonctionnent qu'avec des comptes bancaires français. L'IBAN "
-#~ "'%s' du partenaire '%s' n'est pas un IBAN français."
+msgid "The field 'Bill of Exchange Type' must be set on payment mode '%s'."
+msgstr ""
+"Le champ \"Type de lettre de change\" doit être défini sur le mode de "
+"paiement \"%s\"."
-#~ msgid "Last Modified on"
-#~ msgstr "Dernière modification le"
+#. module: account_banking_fr_lcr
+#: model:ir.model.fields,field_description:account_banking_fr_lcr.field_account_payment_order__fr_lcr_value_date
+msgid "Value Date"
+msgstr "Date de valeur"
+#. module: account_banking_fr_lcr
+#. odoo-python
+#: code:addons/account_banking_fr_lcr/models/account_payment_order.py:0
#, python-format
-#~ msgid ""
-#~ "The currency of payment line '%s' is '%s'. To be included in a French "
-#~ "LCR, the currency must be EUR."
-#~ msgstr ""
-#~ "La devise de la ligne de paiement \"%s\" est \"%s\". Pour être incluse "
-#~ "dans une LCR, la devise doit être l'euro."
+msgid ""
+"Value date is not set on debit order '%s'. It is required on letters of "
+"exchange with cash discount."
+msgstr ""
+"La date de valeur n'est pas fixée sur l'ordre de prélèvement \"%s\". Elle est "
+"requise sur les lettres de change avec escompte."
diff --git a/account_banking_fr_lcr/models/__init__.py b/account_banking_fr_lcr/models/__init__.py
index 0c5be616c..66a161100 100644
--- a/account_banking_fr_lcr/models/__init__.py
+++ b/account_banking_fr_lcr/models/__init__.py
@@ -1,3 +1,7 @@
from . import account_payment_method
+from . import account_payment_mode
from . import account_payment_order
-from . import account_move_line
+from . import account_payment_line
+from . import account_payment
+from . import account_move
+from . import res_partner_bank
diff --git a/account_banking_fr_lcr/models/account_move.py b/account_banking_fr_lcr/models/account_move.py
new file mode 100644
index 000000000..a0b5146e3
--- /dev/null
+++ b/account_banking_fr_lcr/models/account_move.py
@@ -0,0 +1,290 @@
+# Copyright 2024 Akretion France (https://www.akretion.com/)
+# @author: Alexis de Lattre
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+import io
+import logging
+
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.pdfgen import canvas
+from reportlab.platypus import Paragraph
+
+from odoo import _, api, fields, models, tools
+from odoo.exceptions import UserError
+from odoo.tools.misc import format_amount, format_date
+
+logger = logging.getLogger(__name__)
+
+
+try:
+ from pypdf import PdfReader, PdfWriter
+except (ImportError, IOError) as err:
+ logger.debug(err)
+
+
+class AccountMove(models.Model):
+ _inherit = "account.move"
+
+ payment_mode_fr_lcr_type = fields.Selection(
+ related="payment_mode_id.fr_lcr_type", store=True
+ )
+ fr_lcr_attachment_id = fields.Many2one(
+ "ir.attachment", string="Bill of Exchange Attachment"
+ )
+ fr_lcr_attachment_name = fields.Char(
+ related="fr_lcr_attachment_id.name", string="Bill of Exchange Filename"
+ )
+ fr_lcr_attachment_datas = fields.Binary(
+ related="fr_lcr_attachment_id.datas", string="Bill of Exchange File"
+ )
+ fr_lcr_partner_bank_id = fields.Many2one(
+ "res.partner.bank",
+ compute="_compute_fr_lcr_partner_bank_id",
+ store=True,
+ precompute=True,
+ states={"draft": [("readonly", False)]},
+ string="Bill of Exchange Bank Account",
+ help="Bank account of the customer that will be debited by "
+ "the bill of exchange. By default, Odoo selects the first French "
+ "IBAN bank account of the partner.",
+ check_company=True,
+ tracking=True,
+ domain="[('partner_id', '=', commercial_partner_id)]",
+ )
+
+ @api.depends("partner_id", "payment_mode_id")
+ def _compute_fr_lcr_partner_bank_id(self):
+ for move in self:
+ partner_bank_id = False
+ if (
+ move.move_type == "out_invoice"
+ and move.payment_method_code == "fr_lcr"
+ and move.partner_id
+ and move.partner_id.commercial_partner_id.bank_ids
+ ):
+ for partner_bank in move.partner_id.commercial_partner_id.bank_ids:
+ if (
+ partner_bank.acc_type == "iban"
+ and partner_bank.sanitized_acc_number
+ and partner_bank.sanitized_acc_number.startswith("FR")
+ and (
+ not partner_bank.company_id
+ or partner_bank.company_id == move.company_id
+ )
+ ):
+ partner_bank_id = partner_bank.id
+ break
+ move.fr_lcr_partner_bank_id = partner_bank_id
+
+ def _post(self, soft=True):
+ for move in self:
+ if move.move_type == "out_invoice" and move.payment_method_code == "fr_lcr":
+ # We consider bank account as required only for letter of change,
+ # not for promissory note (we may only know the bank account when receiving it)
+ if (
+ move.payment_mode_fr_lcr_type in ("accepted", "not_accepted")
+ and not move.fr_lcr_partner_bank_id
+ ):
+ raise UserError(
+ _(
+ "Customer invoice '%(move)s' is configured with "
+ "payment mode '%(payment_mode)s' which require "
+ "a bill of exchange bank account.",
+ move=move.display_name,
+ payment_mode=move.payment_mode_id.display_name,
+ )
+ )
+ if move.fr_lcr_partner_bank_id:
+ move.fr_lcr_partner_bank_id._fr_iban_validate()
+ return super()._post(soft=soft)
+
+ def fr_lcr_print(self):
+ self.ensure_one()
+ assert self.state == "posted"
+ assert self.move_type == "out_invoice"
+ assert self.payment_method_code == "fr_lcr"
+ assert self.payment_mode_fr_lcr_type == "accepted"
+ if self.fr_lcr_attachment_id and self.payment_state not in (
+ "in_payment",
+ "paid",
+ ):
+ self.fr_lcr_attachment_id.unlink()
+ if not self.fr_lcr_attachment_id:
+ self.fr_lcr_generate_attachment()
+ action = {
+ "name": self.fr_lcr_attachment_id.name,
+ "type": "ir.actions.act_url",
+ "url": f"web/content/?model={self._name}&id={self.id}&"
+ f"filename_field=fr_lcr_attachment_name&field=fr_lcr_attachment_datas&"
+ f"download=true&filename={self.fr_lcr_attachment_id.name}",
+ "target": "new",
+ # target: "new" and NOT "self", otherwise you get the following bug:
+ # after this action, all UserError won't show a pop-up to the user
+ # but will only show a warning message in the logs until the web
+ # page is reloaded
+ }
+ return action
+
+ def _prepare_fr_lcr_report_values(self):
+ self.ensure_one()
+ lang = "fr_FR"
+ rib = self.fr_lcr_partner_bank_id._fr_iban2rib()
+ # I take the commercial_partner, because I don't want to have the name
+ # of a specific contact as customer address
+ partner = self.commercial_partner_id
+ company_partner = self.company_id.partner_id
+ amount_value = format_amount(
+ self.env, self.amount_residual, self.currency_id, lang_code=lang
+ )
+ ref_tire = self._get_payment_order_communication_direct()
+ ref_tire_ascii = self.env["account.payment.order"]._prepare_lcr_field(
+ "Reférence tiré", ref_tire, 10, reference=True
+ )
+ res = {
+ "amount_check": {
+ "value": amount_value,
+ "x": 123,
+ "y": 147,
+ "align": "right",
+ },
+ "amount": {
+ "value": amount_value,
+ "x": 548,
+ "y": 147,
+ "align": "right",
+ },
+ "invoice_date": {
+ "value": format_date(self.env, self.invoice_date, lang_code=lang),
+ "x": 145,
+ "y": 147,
+ },
+ "invoice_date_due": {
+ "value": format_date(self.env, self.invoice_date_due, lang_code=lang),
+ "x": 223,
+ "y": 147,
+ },
+ "ref_tire": {
+ "value": ref_tire_ascii,
+ "x": 293,
+ "y": 147,
+ },
+ "partner_address": {
+ "value": partner._display_address(),
+ "x": 251,
+ "y": 43,
+ "width": 403 - 251,
+ "height": 91 - 43,
+ },
+ "company_address": {
+ "value": company_partner._display_address(),
+ "x": 46,
+ "y": 202,
+ "width": 202 - 46,
+ "height": 255 - 202,
+ },
+ "company_name": {
+ "value": company_partner.name,
+ "x": 304,
+ "y": 204,
+ },
+ "company_city": {
+ "value": company_partner.city,
+ "x": 63,
+ "y": 181,
+ },
+ "rib_bank": {
+ "value": rib["bank"],
+ "x": 56,
+ "y": 94,
+ },
+ "rib_branch": {
+ "value": rib["branch"],
+ "x": 99,
+ "y": 94,
+ },
+ "rib_account": {
+ "value": rib["account"],
+ "x": 144,
+ "y": 94,
+ },
+ "rib_key": {
+ "value": rib["key"],
+ "x": 219,
+ "y": 94,
+ },
+ "bank_name": {
+ "value": self.fr_lcr_partner_bank_id.bank_id.name,
+ "x": 410,
+ "y": 90,
+ },
+ "partner_siren": {
+ "value": hasattr(partner, "siren") and partner.siren or False,
+ "x": 115,
+ "y": 40,
+ },
+ }
+ return res
+
+ def fr_lcr_generate_attachment(self):
+ packet = io.BytesIO()
+ # create a new PDF that contains the additional text with Reportlab
+ text_canvas = canvas.Canvas(packet, pagesize=A4)
+ text_canvas.setFont("Helvetica", 10)
+
+ # for address blocks
+ styleSheet = getSampleStyleSheet()
+ style = styleSheet["BodyText"]
+ style.fontSize = 8
+ style.leading = 9
+
+ # Add text strings and blocks
+ report_values = self._prepare_fr_lcr_report_values()
+ for field_name, field_val in report_values.items():
+ if field_val["value"]:
+ if field_name.endswith("_address"):
+ # Address => use flowable because it is multiline
+ addr_para = Paragraph(
+ field_val["value"].replace("\n", " "), style
+ )
+ addr_para.wrap(field_val["width"], field_val["height"])
+ addr_para.drawOn(text_canvas, field_val["x"], field_val["y"])
+ elif field_val.get("align") == "right":
+ text_canvas.drawRightString(
+ field_val["x"], field_val["y"], field_val["value"]
+ )
+ else:
+ text_canvas.drawString(
+ field_val["x"], field_val["y"], field_val["value"]
+ )
+ text_canvas.save()
+
+ # move to the beginning of the StringIO buffer
+ packet.seek(0)
+ watermark_pdf_reader = PdfReader(packet)
+ # read your existing PDF
+ with tools.file_open(
+ "account_banking_fr_lcr/reports/lettre_de_change.pdf", "rb"
+ ) as empty_report_fd:
+ empty_report_reader = PdfReader(empty_report_fd)
+ final_report_writer = PdfWriter()
+ # add the "watermark" (which is the new pdf) on the existing page
+ page = empty_report_reader.pages[0]
+ page.merge_page(watermark_pdf_reader.pages[0])
+ final_report_writer.add_page(page)
+ final_report_writer.pages[0].compress_content_streams()
+ # finally, write "output" to a real file
+ final_report_io = io.BytesIO()
+ final_report_writer.write(final_report_io)
+ final_report_bytes = final_report_io.getvalue()
+
+ filename = "lettre_de_change-%s.pdf" % self.name.replace("/", "-")
+ attach = self.env["ir.attachment"].create(
+ {
+ "name": filename,
+ "res_id": self.id,
+ "res_model": self._name,
+ "raw": final_report_bytes,
+ }
+ )
+ self.write({"fr_lcr_attachment_id": attach.id})
diff --git a/account_banking_fr_lcr/models/account_move_line.py b/account_banking_fr_lcr/models/account_move_line.py
deleted file mode 100644
index b493d8346..000000000
--- a/account_banking_fr_lcr/models/account_move_line.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright 2016-2022 Akretion France (http://www.akretion.com/)
-# @author: Alexis de Lattre
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-
-from odoo import models
-
-
-class AccountMoveLine(models.Model):
- _inherit = "account.move.line"
-
- def _prepare_payment_line_vals(self, payment_order):
- vals = super()._prepare_payment_line_vals(payment_order)
- if payment_order.payment_mode_id.payment_method_id.code == "fr_lcr":
- # Take the first IBAN account of the partner
- bank_account = self.env["res.partner.bank"].search(
- [("partner_id", "=", self.partner_id.id), ("acc_type", "=", "iban")],
- limit=1,
- )
- if bank_account:
- vals["partner_bank_id"] = bank_account.id
- return vals
diff --git a/account_banking_fr_lcr/models/account_payment.py b/account_banking_fr_lcr/models/account_payment.py
new file mode 100644
index 000000000..cee8e05d2
--- /dev/null
+++ b/account_banking_fr_lcr/models/account_payment.py
@@ -0,0 +1,83 @@
+# Copyright 2014-2022 Akretion France (http://www.akretion.com/)
+# @author: Alexis de Lattre
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+from .account_payment_order import LCR_DATE_FORMAT
+
+LCR_TYPE_CODES = {
+ "not_accepted": "0",
+ "accepted": "1",
+ "promissory_note": "2",
+}
+
+
+class AccountPayment(models.Model):
+ _inherit = "account.payment"
+
+ def _prepare_cfonb_line(self, transactions_count):
+ """Generate each debit line of the CFONB file"""
+ # I use French variable names because the specs are in French
+ self.ensure_one()
+ order = self.payment_order_id
+ payment_line = self.payment_line_ids[0]
+ assert order
+ code_enregistrement = "06"
+ code_operation = "60"
+ numero_enregistrement = str(transactions_count + 1).zfill(8)
+ reference_tire = order._prepare_lcr_field(
+ "Référence tiré", self.payment_reference, 10, reference=True
+ )
+ rib = self.partner_bank_id._fr_iban2rib()
+
+ nom_tire = order._prepare_lcr_field("Nom tiré", self.partner_id.name, 24)
+ if self.partner_bank_id.bank_id:
+ nom_banque = order._prepare_lcr_field(
+ "Nom banque", self.partner_bank_id.bank_id.name, 24
+ )
+ else:
+ nom_banque = " " * 24
+ code_acceptation = LCR_TYPE_CODES[order.payment_mode_id.fr_lcr_type]
+ montant_centimes = str(round(self.amount * 100))
+ zero_montant_centimes = montant_centimes.zfill(12)
+ if payment_line.move_line_id and payment_line.move_line_id.move_id.invoice_date:
+ date_creation_dt = payment_line.move_line_id.move_id.invoice_date
+ else:
+ date_creation_dt = fields.Date.context_today(self)
+ date_creation = date_creation_dt.strftime(LCR_DATE_FORMAT)
+ date_echeance = self.date.strftime(LCR_DATE_FORMAT)
+ if hasattr(self.partner_id, "siren") and self.partner_id.siren:
+ siren_tire = self.partner_id.siren
+ else:
+ siren_tire = " " * 9
+ # I can't use self.name because payment.state == 'draft' so self.name = '/'
+ reference_tireur = order._prepare_lcr_field(
+ "Référence tireur", payment_line.name, 10, reference=True
+ )
+
+ cfonb_line = "".join(
+ [
+ code_enregistrement,
+ code_operation,
+ numero_enregistrement,
+ " " * (6 + 2),
+ reference_tire,
+ nom_tire,
+ nom_banque,
+ code_acceptation,
+ " " * 2,
+ rib["bank"],
+ rib["branch"],
+ rib["account"],
+ zero_montant_centimes,
+ " " * 4,
+ date_echeance,
+ date_creation,
+ " " * (4 + 1 + 3 + 3),
+ siren_tire,
+ reference_tireur,
+ ]
+ )
+ assert len(cfonb_line) == 160, "LCR CFONB line must have 160 chars"
+ return cfonb_line
diff --git a/account_banking_fr_lcr/models/account_payment_line.py b/account_banking_fr_lcr/models/account_payment_line.py
new file mode 100644
index 000000000..4da5c5c93
--- /dev/null
+++ b/account_banking_fr_lcr/models/account_payment_line.py
@@ -0,0 +1,39 @@
+# Copyright 2016-2022 Akretion France (http://www.akretion.com/)
+# @author: Alexis de Lattre
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo import _, models
+from odoo.exceptions import UserError
+
+
+class AccountPaymentLine(models.Model):
+ _inherit = "account.payment.line"
+
+ def _compute_payment_line(self):
+ res = super()._compute_payment_line()
+ for line in self:
+ if (
+ line.order_id.payment_mode_id.payment_method_id.code == "fr_lcr"
+ and line.move_line_id
+ and line.move_line_id.move_id.fr_lcr_partner_bank_id
+ ):
+ line.partner_bank_id = (
+ line.move_line_id.move_id.fr_lcr_partner_bank_id.id
+ )
+ return res
+
+ def draft2open_payment_line_check(self):
+ res = super().draft2open_payment_line_check()
+ eur_currency_id = self.env.ref("base.EUR").id
+ if self.currency_id.id != eur_currency_id:
+ raise UserError(
+ _(
+ "The currency of payment line '%(payment_line)s' is "
+ "%(currency)s. To be included in a french bill of exchange, "
+ "the currency must be EUR.",
+ payment_line=self.display_name,
+ currency=self.currency_id.name,
+ )
+ )
+ self.partner_bank_id._fr_iban_validate()
+ return res
diff --git a/account_banking_fr_lcr/models/account_payment_mode.py b/account_banking_fr_lcr/models/account_payment_mode.py
new file mode 100644
index 000000000..feaa8de77
--- /dev/null
+++ b/account_banking_fr_lcr/models/account_payment_mode.py
@@ -0,0 +1,93 @@
+# Copyright 2022 Akretion France (http://www.akretion.com/)
+# @author: Alexis de Lattre
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class AccountPaymentMode(models.Model):
+ _inherit = "account.payment.mode"
+
+ fr_lcr_type = fields.Selection(
+ [
+ ("not_accepted", "Lettre de change non acceptée (LCR directe)"),
+ ("accepted", "Lettre de change acceptée"),
+ ("promissory_note", "Billet à ordre"),
+ ],
+ compute="_compute_fr_lcr_type",
+ store=True,
+ readonly=False,
+ precompute=True,
+ string="Bill of Exchange Type",
+ )
+ fr_lcr_default_collection_option = fields.Selection(
+ "_fr_lcr_collection_option_selection",
+ string="Default Collection Option",
+ default="due_date",
+ )
+ fr_lcr_dailly = fields.Boolean(string="Dailly Convention")
+ fr_lcr_default_dailly_option = fields.Selection(
+ "_fr_lcr_dailly_option_selection",
+ default="none",
+ string="Default Dailly Option",
+ )
+ # It seems this field is only used for Dailly... but not 100% sure
+ # For the moment, we only display it for Dailly
+ fr_lcr_convention_type = fields.Char(
+ string="Convention Type",
+ size=6,
+ help="Field C1 'Convention Type' in CFONB header line, 6 characters maximum.",
+ )
+
+ @api.model
+ def _fr_lcr_collection_option_selection(self):
+ sel = [
+ ("due_date", "Encaissement, crédit forfaitaire après l’échéance"),
+ (
+ "due_date_fixed_delay",
+ "Encaissement, crédit après expiration d’un délai forfaitaire",
+ ),
+ ("cash_discount", "Escompte"),
+ # "Escompte en valeur" is also called "Escompte en compte"
+ # Great explaination here https://www.netpme.fr/conseil/escompte/
+ # click on "2. Modalités de fonctionnement de l’escompte"
+ ("value_cash_discount", "Escompte en valeur"),
+ ]
+ return sel
+
+ @api.model
+ def _fr_lcr_dailly_option_selection(self):
+ sel = [
+ ("none", "Pas d’indication"),
+ ("cash_discount", "Cession escompte dans le cadre d’une convention Dailly"),
+ (
+ "debt_pledge",
+ "Nantissement de créance dans le cadre d’une convention Dailly",
+ ),
+ ("out_of_agreement", "Cession ou nantissement hors convention Dailly"),
+ ]
+ return sel
+
+ @api.depends("payment_method_id")
+ def _compute_fr_lcr_type(self):
+ for mode in self:
+ fr_lcr_type = False
+ if mode.payment_method_id and mode.payment_method_id.code == "fr_lcr":
+ fr_lcr_type = "not_accepted"
+ mode.fr_lcr_type = fr_lcr_type
+
+ @api.constrains("payment_method_id", "fr_lcr_type")
+ def _check_fr_lcr(self):
+ for mode in self:
+ if (
+ mode.payment_method_id
+ and mode.payment_method_id.code == "fr_lcr"
+ and not mode.fr_lcr_type
+ ):
+ raise ValidationError(
+ _(
+ "The field 'Bill of Exchange Type' must be set on payment mode '%s'."
+ )
+ % mode.display_name
+ )
diff --git a/account_banking_fr_lcr/models/account_payment_order.py b/account_banking_fr_lcr/models/account_payment_order.py
index ab276008a..9039a2ceb 100644
--- a/account_banking_fr_lcr/models/account_payment_order.py
+++ b/account_banking_fr_lcr/models/account_payment_order.py
@@ -3,9 +3,11 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
+import re
from odoo import _, api, fields, models
from odoo.exceptions import UserError
+from odoo.tools.misc import format_date
logger = logging.getLogger(__name__)
@@ -17,101 +19,173 @@
unidecode = False
LCR_DATE_FORMAT = "%d%m%y"
+LCR_TYPE_CODES = {
+ "not_accepted": "0",
+ "accepted": "1",
+ "promissory_note": "2",
+}
+LCR_COLLECTION_OPTION = {
+ "due_date": "3",
+ "due_date_fixed_delay": "4",
+ "cash_discount": "1",
+ "value_cash_discount": "2",
+}
+LCR_DAILLY_OPTION = {
+ "none": "0",
+ "cash_discount": "1",
+ "debt_pledge": "2",
+ "out_of_agreement": "3",
+}
class AccountPaymentOrder(models.Model):
_inherit = "account.payment.order"
- @api.model
- def _prepare_lcr_field(self, field_name, field_value, size):
- """This function is designed to be inherited."""
- if not field_value:
- raise UserError(
- _("The field '%s' is empty or 0. It should have a non-null " "value.")
- % field_name
- )
- try:
- value = unidecode(field_value)
- unallowed_ascii_chars = [
- '"',
- "#",
- "$",
- "%",
- "&",
- ";",
- "<",
- ">",
- "=",
- "@",
- "[",
- "]",
- "^",
- "_",
- "`",
- "{",
- "}",
- "|",
- "~",
- "\\",
- "!",
- ]
- for unallowed_ascii_char in unallowed_ascii_chars:
- value = value.replace(unallowed_ascii_char, "-")
- except Exception:
- # seems that unidecode doesn't raise exception so might
- # be useless
- raise UserError(
- _("Cannot convert the field '%s' to ASCII") % field_name
- ) from None
- value = value.upper()
- # Cut if too long
- value = value[0:size]
- # enlarge if too small
- if len(value) < size:
- value = value.ljust(size, " ")
- assert len(value) == size, "The length of the field is wrong"
- return value
+ fr_lcr_collection_option = fields.Selection(
+ lambda self: self.env[
+ "account.payment.mode"
+ ]._fr_lcr_collection_option_selection(),
+ compute="_compute_fr_lcr_fields",
+ store=True,
+ precompute=True,
+ states={"draft": [("readonly", False)]},
+ string="Collection Option",
+ )
+ # if fr_lcr_value_date is also used for Dailly, we'll have to change the code and the view
+ fr_lcr_value_date = fields.Date(string="Value Date")
+ # invisible, to show or not the field 'fr_lcr_dailly_option'
+ fr_lcr_dailly = fields.Boolean(
+ string="Dailly Convention",
+ compute="_compute_fr_lcr_fields",
+ store=True,
+ precompute=True,
+ )
+ fr_lcr_dailly_option = fields.Selection(
+ lambda self: self.env["account.payment.mode"]._fr_lcr_dailly_option_selection(),
+ compute="_compute_fr_lcr_fields",
+ store=True,
+ precompute=True,
+ states={"draft": [("readonly", False)]},
+ string="Dailly Option",
+ )
+
+ @api.depends("payment_mode_id")
+ def _compute_fr_lcr_fields(self):
+ for order in self:
+ fr_lcr_collection_option = False
+ fr_lcr_dailly = False
+ fr_lcr_dailly_option = False
+ if order.payment_mode_id.payment_method_id.code == "fr_lcr":
+ mode = order.payment_mode_id
+ fr_lcr_collection_option = mode.fr_lcr_default_collection_option
+ fr_lcr_dailly = mode.fr_lcr_dailly
+ fr_lcr_dailly_option = mode.fr_lcr_default_dailly_option
+ order.fr_lcr_collection_option = fr_lcr_collection_option
+ order.fr_lcr_dailly = fr_lcr_dailly
+ order.fr_lcr_dailly_option = fr_lcr_dailly_option
+
+ def draft2open(self):
+ # I call super() first to raise error immediately if partner_bank_id is missing
+ # so, in my code below, I know that line.partner_bank_id is set
+ # Check on payment lines is handled by draft2open_payment_line_check()
+ # on account.payment.line
+ res = super().draft2open()
+ today = fields.Date.context_today(self)
+ self.env.ref("base.EUR").id
+ for order in self:
+ if order.payment_method_code == "fr_lcr":
+ if not order.fr_lcr_collection_option:
+ raise UserError(
+ _("The Collection Option is not set on debit order '%s'.")
+ % order.display_name
+ )
+ if order.fr_lcr_collection_option in (
+ "cash_discount",
+ "value_cash_discount",
+ ):
+ if not order.fr_lcr_value_date:
+ raise UserError(
+ _(
+ "Value date is not set on debit order '%s'. It is "
+ "required on letters of exchange with cash discount."
+ )
+ % order.display_name
+ )
+ elif order.fr_lcr_value_date < today:
+ raise UserError(
+ _(
+ "On debit order '%(order)s', the value date has been "
+ "set to %(value_date)s: it must be in the future.",
+ order=order.display_name,
+ value_date=format_date(
+ self.env, order.fr_lcr_value_date
+ ),
+ )
+ )
+ return res
@api.model
- def _get_rib_from_iban(self, partner_bank):
- if partner_bank.acc_type != "iban":
+ def _prepare_lcr_field(self, field_name, value, size, reference=False):
+ """if reference is True: cut from end (not from start)
+ adjust with 0 (instead of space) and only accept letters and digits
+ """
+ if not value:
raise UserError(
_(
- "For the bank account '%(acc_number)s' of partner '%(partner)s', "
- "the Bank Account Type should be 'IBAN'."
+ "Error in the generation of the CFONB file: "
+ "the field '%s' is empty or 0. It should have a non-null value."
)
- % {
- "acc_number": partner_bank.acc_number,
- "partner": partner_bank.partner_id.display_name,
- }
+ % field_name
)
- iban = partner_bank.sanitized_acc_number
- if iban[0:2] != "FR":
+ if not isinstance(value, str):
raise UserError(
_(
- "LCR are only for French bank accounts. The IBAN '%(acc_number)s' "
- "of partner '%(partner)s' is not a French IBAN."
+ "Error in the generation of the CFONB file: "
+ "'%(field)s' should be a string, "
+ "but it is %(value_type)s (value: %(value)s).",
+ field=field_name,
+ value_type=type(value),
+ value=value,
)
- % {
- "acc_number": partner_bank.acc_number,
- "partner": partner_bank.partner_id.display_name,
- }
)
- assert len(iban) == 27, "French IBANs must have 27 caracters"
- return {
- "code_banque": iban[4:9],
- "code_guichet": iban[9:14],
- "numero_compte": iban[14:25],
- "cle_rib": iban[25:27],
- }
+ value = unidecode(value)
+ # page 25 of the CFONB specs:
+ # allowed chars are a-z, digits and * ( ) . , / + - :
+ value = value.upper()
+ if reference:
+ value = re.sub(r"[^A-Z0-9]", "", value)
+ else:
+ value = re.sub(r"[^A-Z0-9\*\(\)\.,/\+\-:\s]", "-", value)
+ # Cut if too long
+ if len(value) > size:
+ if reference:
+ value = value[-size:] # cut from end
+ else:
+ value = value[:size] # cut from start
+ # enlarge if too small: add spaces at the end
+ elif len(value) < size:
+ if reference:
+ value = value.rjust(size, "0")
+ else:
+ value = value.ljust(size, " ")
+ assert len(value) == size, "The length of the field is wrong"
+ return value
- @api.model
def _prepare_first_cfonb_line(self):
"""Generate the header line of the CFONB file"""
+ self.ensure_one()
code_enregistrement = "03"
code_operation = "60"
numero_enregistrement = "00000001"
numero_emetteur = "000000" # It is not needed for LCR
+ if self.payment_mode_id.fr_lcr_convention_type:
+ type_convention = self._prepare_lcr_field(
+ "Type de convention",
+ self.payment_mode_id.fr_lcr_convention_type,
+ 6,
+ )
+ else:
+ type_convention = " " * 6
# this number is only required for old national direct debits
today_dt = fields.Date.context_today(self)
date_remise = today_dt.strftime(LCR_DATE_FORMAT)
@@ -123,28 +197,45 @@ def _prepare_first_cfonb_line(self):
self.company_partner_bank_id.bank_id.name,
24,
)
- code_entree = "3"
- code_dailly = " "
+ code_entree = LCR_COLLECTION_OPTION[self.fr_lcr_collection_option]
+ if self.fr_lcr_dailly and self.fr_lcr_dailly_option:
+ code_dailly = LCR_DAILLY_OPTION[self.fr_lcr_dailly_option]
+ else:
+ code_dailly = " "
code_monnaie = "E"
- rib = self._get_rib_from_iban(self.company_partner_bank_id)
+ rib = self.company_partner_bank_id._fr_iban2rib()
ref_remise = self._prepare_lcr_field("Référence de la remise", self.name, 11)
+ if self.fr_lcr_collection_option in ("cash_discount", "value_cash_discount"):
+ date_de_valeur = self.fr_lcr_value_date.strftime(LCR_DATE_FORMAT)
+ else:
+ date_de_valeur = " " * 6
+ if (
+ hasattr(self.company_id.partner_id, "siren")
+ and self.company_id.partner_id.siren
+ ):
+ siren_cedant = self.company_id.partner_id.siren + " " * 6
+ else:
+ siren_cedant = " " * 15
cfonb_line = "".join(
[
code_enregistrement,
code_operation,
numero_enregistrement,
numero_emetteur,
- " " * 6,
+ type_convention,
date_remise,
raison_sociale_cedant,
domiciliation_bancaire_cedant,
code_entree,
code_dailly,
code_monnaie,
- rib["code_banque"],
- rib["code_guichet"],
- rib["numero_compte"],
- " " * (16 + 6 + 10 + 15),
+ rib["bank"],
+ rib["branch"],
+ rib["account"],
+ " " * 16,
+ date_de_valeur,
+ " " * 10,
+ siren_cedant,
# Date de valeur is left empty because it is only for
# "remise à l'escompte" and we do
# "Encaissement, crédit forfaitaire après l’échéance"
@@ -152,61 +243,6 @@ def _prepare_first_cfonb_line(self):
]
)
assert len(cfonb_line) == 160, "LCR CFONB line must have 160 chars"
- cfonb_line += "\r\n"
- return cfonb_line
-
- @api.model
- def _prepare_cfonb_line(self, line, transactions_count):
- """Generate each debit line of the CFONB file"""
- # I use French variable names because the specs are in French
- code_enregistrement = "06"
- code_operation = "60"
- numero_enregistrement = str(transactions_count + 1).zfill(8)
- reference_tire = self._prepare_lcr_field(
- "Référence tiré", line.payment_reference, 10
- )
- rib = self._get_rib_from_iban(line.partner_bank_id)
-
- nom_tire = self._prepare_lcr_field("Nom tiré", line.partner_id.name, 24)
- if line.partner_bank_id.bank_id:
- nom_banque = self._prepare_lcr_field(
- "Nom banque", line.partner_bank_id.bank_id.name, 24
- )
- else:
- nom_banque = " " * 24
- code_acceptation = "0"
- montant_centimes = str(round(line.amount * 100))
- zero_montant_centimes = montant_centimes.zfill(12)
- today_dt = fields.Date.context_today(self)
- date_creation = today_dt.strftime(LCR_DATE_FORMAT)
- requested_date_dt = line.date
- date_echeance = requested_date_dt.strftime(LCR_DATE_FORMAT)
- reference_tireur = reference_tire
-
- cfonb_line = "".join(
- [
- code_enregistrement,
- code_operation,
- numero_enregistrement,
- " " * (6 + 2),
- reference_tire,
- nom_tire,
- nom_banque,
- code_acceptation,
- " " * 2,
- rib["code_banque"],
- rib["code_guichet"],
- rib["numero_compte"],
- zero_montant_centimes,
- " " * 4,
- date_echeance,
- date_creation,
- " " * (4 + 1 + 3 + 3 + 9),
- reference_tireur,
- ]
- )
- assert len(cfonb_line) == 160, "LCR CFONB line must have 160 chars"
- cfonb_line += "\r\n"
return cfonb_line
def _prepare_final_cfonb_line(self, total_amount, transactions_count):
@@ -229,35 +265,38 @@ def _prepare_final_cfonb_line(self, total_amount, transactions_count):
assert len(cfonb_line) == 160, "LCR CFONB line must have 160 chars"
return cfonb_line
+ def _fr_lcr_line_separator(self):
+ """It seems that some bank don't want a line break. For the moment, we have this hook.
+ If it is confirm, we'll make it a configuration parameter"""
+ return "\r\n"
+
def generate_payment_file(self):
"""Creates the LCR CFONB file."""
self.ensure_one()
if self.payment_method_id.code != "fr_lcr":
return super().generate_payment_file()
- cfonb_string = self._prepare_first_cfonb_line()
+ cfonb_lines = []
+ cfonb_lines.append(self._prepare_first_cfonb_line())
total_amount = 0.0
transactions_count = 0
- eur_currency = self.env.ref("base.EUR")
- # Iterate each bank payment lines
- for line in self.payment_ids:
- if line.currency_id != eur_currency:
- raise UserError(
- _(
- "The currency of payment line '%(payment_line)s' is "
- "'%(currency)s'. To be included in a French LCR, "
- "the currency must be EUR."
- )
- % {
- "payment_line": line.display_name,
- "currency": line.currency_id.name,
- }
- )
+ eur_currency_id = self.env.ref("base.EUR").id
+ for payment in self.payment_ids:
+ assert payment.currency_id.id == eur_currency_id
transactions_count += 1
- cfonb_string += self._prepare_cfonb_line(line, transactions_count)
- total_amount += line.amount
+ cfonb_lines.append(payment._prepare_cfonb_line(transactions_count))
+ total_amount += payment.amount
- cfonb_string += self._prepare_final_cfonb_line(total_amount, transactions_count)
-
- filename = "LCR_%s.txt" % self.name.replace("/", "-")
- return (cfonb_string.encode("ascii"), filename)
+ cfonb_lines.append(
+ self._prepare_final_cfonb_line(total_amount, transactions_count)
+ )
+ if self.payment_mode_id.fr_lcr_type == "promissory_note":
+ file_prefix = "BOR"
+ else:
+ file_prefix = "LCR"
+ filename = f"{file_prefix}_{self.name.replace('/', '-')}.txt"
+ line_separator = self._fr_lcr_line_separator()
+ logger.debug("LCR line separator is %s", line_separator)
+ cfonb_string = line_separator.join(cfonb_lines)
+ cfonb_bytes = cfonb_string.encode("ascii")
+ return (cfonb_bytes, filename)
diff --git a/account_banking_fr_lcr/models/res_partner_bank.py b/account_banking_fr_lcr/models/res_partner_bank.py
new file mode 100644
index 000000000..144e5b060
--- /dev/null
+++ b/account_banking_fr_lcr/models/res_partner_bank.py
@@ -0,0 +1,50 @@
+# Copyright 2024 Akretion France (https://www.akretion.com/)
+# @author: Alexis de Lattre
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
+
+from odoo import _, models
+from odoo.exceptions import UserError
+
+
+class ResPartnerBank(models.Model):
+ _inherit = "res.partner.bank"
+
+ def _fr_iban_validate(self):
+ self.ensure_one()
+ if self.acc_type != "iban":
+ raise UserError(
+ _(
+ "Bills of exchange can only use IBAN bank accounts. "
+ "Bank account '%(acc_number)s' of partner '%(partner)s' "
+ "is not an IBAN."
+ )
+ % {
+ "acc_number": self.acc_number,
+ "partner": self.partner_id.display_name,
+ }
+ )
+ if not self.sanitized_acc_number.startswith("FR"):
+ raise UserError(
+ _(
+ "Bills of exchange can only use French bank accounts. "
+ "The IBAN '%(acc_number)s' of partner '%(partner)s' "
+ "is not a French IBAN."
+ )
+ % {
+ "acc_number": self.acc_number,
+ "partner": self.partner_id.display_name,
+ }
+ )
+ assert (
+ len(self.sanitized_acc_number) == 27
+ ), "French IBANs must have 27 caracters"
+
+ def _fr_iban2rib(self):
+ self._fr_iban_validate()
+ acc_number = self.sanitized_acc_number
+ return {
+ "bank": acc_number[4:9], # code banque
+ "branch": acc_number[9:14], # code guichet
+ "account": acc_number[14:25], # numéro de compte
+ "key": acc_number[25:27], # clé RIB
+ }
diff --git a/account_banking_fr_lcr/readme/CONFIGURE.rst b/account_banking_fr_lcr/readme/CONFIGURE.rst
index 6ea29093d..dfe26bea0 100644
--- a/account_banking_fr_lcr/readme/CONFIGURE.rst
+++ b/account_banking_fr_lcr/readme/CONFIGURE.rst
@@ -1,3 +1,10 @@
To configure this module, you need to create a new payment mode linked
to the payment method *Lettre de Change Relevé* that is automatically
created when you install this module.
+
+Once you selected this payment method, you will have a new section *Bill of Exchange* on the payment mode where you will have to configure:
+
+* the *LCR type*: *Lettre de change non acceptée (LCR directe)*, *Lettre de change acceptée* or *Billet à ordre*,
+* the *Default Collection Option*,
+* if you have a *Dailly Convention*,
+* in case you have a Dailly convention, you will be able to configure the *Default Dailly Option* and the *Convention Type*.
diff --git a/account_banking_fr_lcr/readme/DESCRIPTION.rst b/account_banking_fr_lcr/readme/DESCRIPTION.rst
index 936284726..392addab6 100644
--- a/account_banking_fr_lcr/readme/DESCRIPTION.rst
+++ b/account_banking_fr_lcr/readme/DESCRIPTION.rst
@@ -1,11 +1,14 @@
-This module adds support for French Letters of Change (in French:
-*Lettre de Change Relevé* aka LCR). This module supports direct LCR
-(in French, *LCR Directe*) and not paper LCR.
+This module adds support for French Letters of Change. This module supports:
-This payment type is still in use in France and it is *not* replaced by SEPA
-one-off Direct Debits.
+* **Direct letter of change** (in French : *Lettre de change directe* or *LCR directe*),
+* **Accepted letter of change** (in French : *Lettre de change acceptée* ; I call it *paper letter of change*),
+* **Promissory note** (in French : *Billet à ordre*),
-With this module, you can generate an LCR CFONB file to send to your
-bank. Then, your customer will be notified by their bank about the debit
-(amount, date of debit). Eventually, the debit will take place at the
-planned date.
+It supports cash discounts debit orders and Dailly convention.
+
+This module has 2 main features:
+
+* for **Accepted Letter of Change**, generate a paper letter of change as PDF following the official layout NF K 11-030-1.
+* generate of LCR (or BOR) CFONB files to send to your bank.
+
+This module follows the specifications published on the `CFONB website `_, section *Espace documentaire > Instruments de paiement > Effet de commerce* (document version of September 2002).
diff --git a/account_banking_fr_lcr/readme/INSTALL.rst b/account_banking_fr_lcr/readme/INSTALL.rst
new file mode 100644
index 000000000..1cc815f1b
--- /dev/null
+++ b/account_banking_fr_lcr/readme/INSTALL.rst
@@ -0,0 +1,6 @@
+This module requires 2 Python libs:
+
+* `pypdf `_ version 3.10 or above,
+* `unidecode `_ (any version).
+
+In order to have the SIREN of the company and of the customer set in the CFONB file (optional field) and printed on the paper letter of exchange, the OCA module **l10n_fr_siret** must be installed. The installation of the module **l10n_fr_siret** is optional (because the SIREN field in the CFONB file is optional).
diff --git a/account_banking_fr_lcr/readme/USAGE.rst b/account_banking_fr_lcr/readme/USAGE.rst
index 2bacb4bd0..25354933e 100644
--- a/account_banking_fr_lcr/readme/USAGE.rst
+++ b/account_banking_fr_lcr/readme/USAGE.rst
@@ -1,2 +1,9 @@
-To use this module, you need to create a new Debit Order and
-select the LCR payment mode.
+This module adds a new field *Bill of Exchange Bank Account* on customer invoices to select the bank account of the customer that will be debited by the letter of exchange. This bank account must be a french IBAN.
+
+If you configured the payment mode for **Accepted Letter of Change**, you will have a button *Print Bill of Exchange* on customer invoices to get the letter of change as PDF.
+
+This module uses the standard workflow of debit orders as implemented in the OCA module **account_payment_order**. A debit order linked to a payment mode with the payment method *Lettre de change relevé* has a few additionnal constraints:
+
+* all payment lines must be in euro currency,
+* the bank accounts on the payment lines must be french IBANs,
+* if the payment order is configured with cash discount, you must configure the value date on the payment order (new field added by this module).
diff --git a/account_banking_fr_lcr/reports/lettre_de_change.pdf b/account_banking_fr_lcr/reports/lettre_de_change.pdf
new file mode 100644
index 000000000..e820e098a
Binary files /dev/null and b/account_banking_fr_lcr/reports/lettre_de_change.pdf differ
diff --git a/account_banking_fr_lcr/static/description/index.html b/account_banking_fr_lcr/static/description/index.html
index 82a93b73f..52ea2fc0e 100644
--- a/account_banking_fr_lcr/static/description/index.html
+++ b/account_banking_fr_lcr/static/description/index.html
@@ -1,4 +1,3 @@
-
@@ -370,42 +369,69 @@
This module adds support for French Letters of Change (in French:
-Lettre de Change Relevé aka LCR). This module supports direct LCR
-(in French, LCR Directe) and not paper LCR.
-
This payment type is still in use in France and it is not replaced by SEPA
-one-off Direct Debits.
-
With this module, you can generate an LCR CFONB file to send to your
-bank. Then, your customer will be notified by their bank about the debit
-(amount, date of debit). Eventually, the debit will take place at the
-planned date.
+
This module adds support for French Letters of Change. This module supports:
+
+
Direct letter of change (in French : Lettre de change directe or LCR directe),
+
Accepted letter of change (in French : Lettre de change acceptée ; I call it paper letter of change),
+
Promissory note (in French : Billet à ordre),
+
+
It supports cash discounts debit orders and Dailly convention.
+
This module has 2 main features:
+
+
for Accepted Letter of Change, generate a paper letter of change as PDF following the official layout NF K 11-030-1.
+
generate of LCR (or BOR) CFONB files to send to your bank.
+
+
This module follows the specifications published on the CFONB website, section Espace documentaire > Instruments de paiement > Effet de commerce (document version of September 2002).
In order to have the SIREN of the company and of the customer set in the CFONB file (optional field) and printed on the paper letter of exchange, the OCA module l10n_fr_siret must be installed. The installation of the module l10n_fr_siret is optional (because the SIREN field in the CFONB file is optional).
To configure this module, you need to create a new payment mode linked
to the payment method Lettre de Change Relevé that is automatically
created when you install this module.
+
Once you selected this payment method, you will have a new section Bill of Exchange on the payment mode where you will have to configure:
+
+
the LCR type: Lettre de change non acceptée (LCR directe), Lettre de change acceptée or Billet à ordre,
+
the Default Collection Option,
+
if you have a Dailly Convention,
+
in case you have a Dailly convention, you will be able to configure the Default Dailly Option and the Convention Type.
This module adds a new field Bill of Exchange Bank Account on customer invoices to select the bank account of the customer that will be debited by the letter of exchange. This bank account must be a french IBAN.
+
If you configured the payment mode for Accepted Letter of Change, you will have a button Print Bill of Exchange on customer invoices to get the letter of change as PDF.
+
This module uses the standard workflow of debit orders as implemented in the OCA module account_payment_order. A debit order linked to a payment mode with the payment method Lettre de change relevé has a few additionnal constraints:
+
+
all payment lines must be in euro currency,
+
the bank accounts on the payment lines must be french IBANs,
+
if the payment order is configured with cash discount, you must configure the value date on the payment order (new field added by this module).
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
@@ -413,21 +439,21 @@