diff --git a/account_factoring_receivable_balance_factofrance/README.rst b/account_factoring_receivable_balance_factofrance/README.rst new file mode 100644 index 000000000..cb5ea64bb --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/README.rst @@ -0,0 +1,123 @@ +================================= +Account Factoring for FactoFrance +================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:063d495a7d079ae9bafe1d7f47890862938bed76c7a1a0a94afd8fe018ea6303 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--france-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-france/tree/17.0/account_factoring_receivable_balance_factofrance + :alt: OCA/l10n-france +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-france-17-0/l10n-france-17-0-account_factoring_receivable_balance_factofrance + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-france&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Gestion de balance avec BPCE + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +- use a company with l10n_fr +- alternatively you may create a new one with + +Here you can create a new company with BPCE settings for default +currency + +.. code:: python + + env["res.company"]._create_french_company(company_name="my new company") + +Here you may create settings for a new installed currency + +.. code:: python + + env.browse(mycompany_id)._configure_bpce_factoring(currency_record) + +- you may execute this last method with UI in res.company form (Factor + tab) +- now you can go to journals and filter them with Factor type. +- set bpce to Factor Type to each bank journal related to currency of + previous journals. + +Known issues / Roadmap +====================== + +Seul le mode de réglement est géré dans ce module. + +Les prélèvements, effets, BOR, et chèques ne le sont pas + +Dans le fichier émis en position 53, seul le cas export est traité. + +See TODO in source code + +Bug Tracker +=========== + +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 +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Open Source Integrators + +Contributors +------------ + +- Akretion: + + - David BEAL + - Alexis DE LATTRE + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-dreispt| image:: https://github.com/dreispt.png?size=40px + :target: https://github.com/dreispt + :alt: dreispt + +Current `maintainer `__: + +|maintainer-dreispt| + +This module is part of the `OCA/l10n-france `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_factoring_receivable_balance_factofrance/__init__.py b/account_factoring_receivable_balance_factofrance/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_factoring_receivable_balance_factofrance/__manifest__.py b/account_factoring_receivable_balance_factofrance/__manifest__.py new file mode 100644 index 000000000..54eca7444 --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/__manifest__.py @@ -0,0 +1,16 @@ +# © 2024 Open Source Integrators, Daniel Reis +{ + "name": "Account Factoring for FactoFrance", + "version": "17.0.1.0.0", + "category": "Accounting", + "license": "AGPL-3", + "website": "https://github.com/OCA/l10n-france", + "author": "Open Source Integrators,Odoo Community Association (OCA)", + "maintainers": ["dreispt"], + "depends": [ + "account_factoring_receivable_balance", + "l10n_fr", + ], + "data": [], + "demo": [], +} diff --git a/account_factoring_receivable_balance_factofrance/models/__init__.py b/account_factoring_receivable_balance_factofrance/models/__init__.py new file mode 100644 index 000000000..4efa5ec25 --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/models/__init__.py @@ -0,0 +1,3 @@ +from . import partner +from . import account_journal +from . import subrogation_receipt diff --git a/account_factoring_receivable_balance_factofrance/models/account_journal.py b/account_factoring_receivable_balance_factofrance/models/account_journal.py new file mode 100644 index 000000000..920b3b438 --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/models/account_journal.py @@ -0,0 +1,13 @@ +# © 2024 Open Source Integrators, Daniel Reis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + factor_type = fields.Selection( + selection_add=[("factofrance", "FactoFrance")], + ondelete={"factofrance": "set null"}, + ) diff --git a/account_factoring_receivable_balance_factofrance/models/partner.py b/account_factoring_receivable_balance_factofrance/models/partner.py new file mode 100644 index 000000000..2b293436a --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/models/partner.py @@ -0,0 +1,20 @@ +# © 2024 Open Source Integrators, Daniel Reis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, models +from odoo.exceptions import UserError + + +class ResPartner(models.Model): + _inherit = "res.partner" + + @api.constrains("factor_journal_id", "ref", "siret") + def _constrains_factor_journal_id(self): + for rec in self: + is_factofrance = rec.factor_journal_id == "factofrance" + if is_factofrance and (not rec.ref or not rec.siret): + msg = _( + "Les balances clients gérées par FactoFrance doivent avoir " + "les champs Référence et SIRET remplis" + ) + raise UserError(msg) diff --git a/account_factoring_receivable_balance_factofrance/models/subrogation_receipt.py b/account_factoring_receivable_balance_factofrance/models/subrogation_receipt.py new file mode 100644 index 000000000..07d1016c7 --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/models/subrogation_receipt.py @@ -0,0 +1,260 @@ +# © 2024 Open Source Integrators, Daniel Reis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import base64 +import re + +from odoo import _, fields, models, tools +from odoo.exceptions import UserError + +FORMAT_VERSION = "7.0" +RETURN = "\r\n" + + +class SubrogationReceipt(models.Model): + _inherit = "subrogation.receipt" + + def _prepare_factor_file_factofrance(self): + "Entry-point to generate the factor file" + self.ensure_one() + name = "BPCE_{}_{}_{}.txt".format( # TODO! + self._sanitize_filepath(f"{fields.Date.today()}"), + self.id, + self._sanitize_filepath(self.company_id.name), + ) + return { + "name": name, + "res_id": self.id, + "res_model": self._name, + "datas": self._prepare_factor_file_data_factofrance(), + } + + def _prepare_factor_file_data_factofrance(self): + self.ensure_one() + if not self.factor_journal_id.factor_code: + msg = _( + "Le code du factor n'est pas renseigné dans l'onglet 'Factor'.\n" + "Vous devez mettre le code du factor dans la société '{}'.\n" + "Champ dans l'onglet 'Factor'", + self.env.company.name, + ) + raise UserError(msg) + if not self.statement_date: + raise UserError(_("Vous devez spécifier la date du dernier relevé")) + body, max_row, balance = self._get_factofrance_body() + header = self._get_factofrance_header() + check_column_size(header) + ender = self._get_factofrance_ender(max_row, balance) + check_column_size(ender) + raw_data = f"{header}{RETURN}{body}{RETURN}{ender}{RETURN}".replace( + "False", " " + ) + data = clean_string(raw_data) + # check there is no regression in columns position + check_column_position(raw_data, self.factor_journal_id, False) + check_column_position(data, self.factor_journal_id) + dev_mode = tools.config.options.get("dev_mode") + if dev_mode and dev_mode[0][-3:] == "pdb" or False: + # make debugging easier saving file on filesystem to check + debug(raw_data, "_raw") + debug(data) + # pylint: disable=C8107 + raise UserError("See files /odoo/subrog*.txt") + total_in_erp = sum(self.line_ids.mapped("amount_currency")) + if round(balance, 2) != round(total_in_erp, 2): + # pylint: disable=C8107 + raise UserError( + "Erreur dans le calul de la balance :" + f"\n - erp : {total_in_erp}\n - fichier : {balance}" + ) + self.write({"balance": balance}) + # non ascii chars are replaced + data = bytes(data, "ascii", "replace").replace(b"?", b" ") + return base64.b64encode(data) + + def _get_factofrance_header(self): + self = self.sudo() + info = { + "code": pad(self.company_id.bpce_factor_code, 6, 0), + "devise": self.factor_journal_id.currency_id.name, + "name": pad(self.company_id.partner_id.name, 25), + "statem_date": bpce_date(self.statement_date), + "date": bpce_date(self.date), + "idfile": pad(self.id, 3, 0), + "reserved": pad(" ", 208), + "format": FORMAT_VERSION, + } + string = "01000001138{code}{devise}{name}{statem_date}{date}" + string += "{idfile}{format}{reserved}" + return string.format(**info) + + def _get_factofrance_ender(self, max_row, balance): + self = self.sudo() + info = { + "seq": pad(max_row + 2, 6, 0), + "code": pad(self.company_id.bpce_factor_code, 6, 0), + "name": pad(self.company_id.partner_id.name[:25], 25), + "balance": pad(round(balance * 100), 13, 0), + "reserved": pad(" ", 220), + } + return "09{seq}138{code}{name}{balance}{reserved}".format(**info) + + def _get_factofrance_body(self): + self = self.sudo() + sequence = 1 + rows = [] + balance = 0 + for line in self.line_ids: + move = line.move_id + partner = line.move_id.partner_id.commercial_partner_id + if not partner: + raise UserError(f"Pas de partenaire sur la pièce {line.move_id}") + sequence += 1 + name = pad(move.name, 30, position="left") + p_type = get_type_piece(move) + total = move.amount_total_in_currency_signed + info = { + "seq": pad(sequence, 6, 0), + "siret": pad(" ", 14) + if not partner.siret + else pad(partner.siret, 14, 0, position="left"), + "pname": pad(partner.name[:15], 15, position="left"), + "ref_cli": pad(partner.ref, 10, position="left"), + "res1": pad(" ", 5), + "activity": "D" + if partner.country_id == self.env.ref("base.fr") + else "E", + "res2": pad(" ", 9), + "cmt": pad(" ", 20), + "piece": name, + "piece_factor": get_piece_factor(name, p_type), + "type": p_type, + "paym": "VIR" + if p_type == "FAC" + else " ", # TODO only VIR is implemented + "date": bpce_date(move.invoice_date if p_type == "FAC" else move.date), + "date_due": bpce_date(move.invoice_date_due) or pad(" ", 8), + "total": pad(round(abs(total) * 100), 13, 0), + "devise": move.currency_id.name, + "res3": " ", + "eff_non_echu": " ", # TODO + "eff_num": pad(" ", 7), # TODO + "eff_total": pad("", 13, 0), # effet total TODO not implemented + "eff_imputed": pad("", 13, 0), # effet imputé TODO not implemented + "rib": pad(" ", 23), # TODO + "eff_echeance": pad(" ", 8), # date effet echeance TODO not implemented + "eff_pull": pad(" ", 10), # reférence tiré/le nom TODO not implemented + # 0: traite non accepté, 1: traite accepté, 2: BOR TODO not implemented + "eff_type": " ", + "res4": pad(" ", 17), + } + balance += total + fstring = "02{seq}{siret}{pname}{ref_cli}{res1}{activity}{res2}{cmt}" + fstring += "{piece}{piece_factor}{type}{paym}{date}{date_due}" + fstring += "{total}{devise}{res3}{eff_non_echu}{eff_num}{eff_total}" + fstring += "{eff_imputed}{rib}{eff_echeance}{eff_pull}{eff_type}{res4}" + string = fstring.format(**info) + check_column_size(string, fstring, info) + rows.append(string) + return (RETURN.join(rows), len(rows), balance) + + +def get_piece_factor(name, p_type): + if not p_type: + return "{}{}".format(name[:15], pad(" ", 15)) + return name[:30] + + +def get_type_piece(move): + journal_type = move.journal_id.type + p_type = False + move_type = move.move_type + if move_type == "entry": + if journal_type == "general": + # TODO : improve + od_type = False + lines = move.line_ids.filtered( + lambda s: s.account_id.group_id == s.env.ref("l10n_fr.1_pcg_411") + ) + for line in lines: + if max(line.debit, line.credit) == move.amount_total: + if line.debit == move.amount_total: + od_type = "D" + else: + od_type = "C" + break + if not od_type: + # pylint: disable=C8107 + raise UserError(f"Impossible de déterminer le type de l'OD {move.name}") + p_type = f"OD{od_type}" + elif move_type == "out_invoice": + p_type = "FAC" + elif move_type == "out_refund": + p_type = "AVO" + assert len(p_type) == 3 + return p_type + + +def bpce_date(date_field): + return date_field.strftime("%d%m%Y") + + +def pad(string, pad, end=" ", position="right"): + "Complete string by leading `end` string from `position`" + if isinstance(end, int | float): + end = str(end) + if isinstance(string, int | float): + string = str(string) + if position == "right": + string = string.rjust(pad, end) + else: + string = string.ljust(pad, end) + return string + + +def clean_string(string): + """Remove all except [A-Z], space, \r, \n + https://www.rapidtables.com/code/text/ascii-table.html""" + string = string.replace(FORMAT_VERSION, "FORMATVERSION") + string = string.upper() + string = re.sub(r"[\x21-\x2F]|[\x3A-\x40]|[\x5E-\x7F]|\x0A\x0D", r" ", string) + string = string.replace("FORMATVERSION", FORMAT_VERSION) + return string + + +def debug(content, suffix=""): + mpath = f"/odoo/subrog{suffix}.txt" + with open(mpath, "wb") as f: + if isinstance(content, str): + content = bytes(content, "ascii", "replace") + f.write(content) + + +def check_column_size(string, fstring=None, info=None): + if len(string) != 275: + if fstring and info: + fstring = fstring.replace("{", "|{") + fstring = fstring.format(**info) + strings = fstring.split("|") + for mystr in strings: + strings[strings.index(mystr)] = f"{mystr}({len(mystr)})" + fstring = "|".join(strings) + else: + fstring = "" + # pylint: disable=C8107 + raise UserError( + "La ligne suivante contient {} caractères au lieu de 275\n\n{}" + "\n\nDebugging string:\n{}s".format(len(string), string, fstring) + ) + + +def check_column_position(content, factor_journal, final=True): + line2 = content.split(RETURN)[1] + # line2 = content.readline(2) + currency = line2[177:180] + msg = "Problème de décalage colonne dans le fichier" + if final: + msg += " final" + else: + msg += " brut" + assert currency == factor_journal.currency_id.name, msg diff --git a/account_factoring_receivable_balance_factofrance/pyproject.toml b/account_factoring_receivable_balance_factofrance/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_factoring_receivable_balance_factofrance/readme/CONFIGURE.md b/account_factoring_receivable_balance_factofrance/readme/CONFIGURE.md new file mode 100644 index 000000000..ae7894ea1 --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/readme/CONFIGURE.md @@ -0,0 +1,21 @@ +- use a company with l10n_fr +- alternatively you may create a new one with + +Here you can create a new company with BPCE settings for default +currency + +``` python +env["res.company"]._create_french_company(company_name="my new company") +``` + +Here you may create settings for a new installed currency + +``` python +env.browse(mycompany_id)._configure_bpce_factoring(currency_record) +``` + +- you may execute this last method with UI in res.company form (Factor + tab) +- now you can go to journals and filter them with Factor type. +- set bpce to Factor Type to each bank journal related to currency of + previous journals. diff --git a/account_factoring_receivable_balance_factofrance/readme/CONTRIBUTORS.md b/account_factoring_receivable_balance_factofrance/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..8c6e0d92a --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Akretion: + - David BEAL + - Alexis DE LATTRE diff --git a/account_factoring_receivable_balance_factofrance/readme/DESCRIPTION.md b/account_factoring_receivable_balance_factofrance/readme/DESCRIPTION.md new file mode 100644 index 000000000..299661695 --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/readme/DESCRIPTION.md @@ -0,0 +1 @@ +Gestion de balance avec BPCE diff --git a/account_factoring_receivable_balance_factofrance/readme/ROADMAP.md b/account_factoring_receivable_balance_factofrance/readme/ROADMAP.md new file mode 100644 index 000000000..f816b8b4c --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/readme/ROADMAP.md @@ -0,0 +1,7 @@ +Seul le mode de réglement est géré dans ce module. + +Les prélèvements, effets, BOR, et chèques ne le sont pas + +Dans le fichier émis en position 53, seul le cas export est traité. + +See TODO in source code diff --git a/account_factoring_receivable_balance_factofrance/static/description/index.html b/account_factoring_receivable_balance_factofrance/static/description/index.html new file mode 100644 index 000000000..f401d5435 --- /dev/null +++ b/account_factoring_receivable_balance_factofrance/static/description/index.html @@ -0,0 +1,458 @@ + + + + + +Account Factoring for FactoFrance + + + +
+

Account Factoring for FactoFrance

+ + +

Beta License: AGPL-3 OCA/l10n-france Translate me on Weblate Try me on Runboat

+

Gestion de balance avec BPCE

+

Table of contents

+ +
+

Configuration

+
    +
  • use a company with l10n_fr
  • +
  • alternatively you may create a new one with
  • +
+

Here you can create a new company with BPCE settings for default +currency

+
+env["res.company"]._create_french_company(company_name="my new company")
+
+

Here you may create settings for a new installed currency

+
+env.browse(mycompany_id)._configure_bpce_factoring(currency_record)
+
+
    +
  • you may execute this last method with UI in res.company form (Factor +tab)
  • +
  • now you can go to journals and filter them with Factor type.
  • +
  • set bpce to Factor Type to each bank journal related to currency of +previous journals.
  • +
+
+
+

Known issues / Roadmap

+

Seul le mode de réglement est géré dans ce module.

+

Les prélèvements, effets, BOR, et chèques ne le sont pas

+

Dans le fichier émis en position 53, seul le cas export est traité.

+

See TODO in source code

+
+
+

Bug Tracker

+

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 +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Open Source Integrators
  • +
+
+
+

Contributors

+
    +
  • Akretion:
      +
    • David BEAL
    • +
    • Alexis DE LATTRE
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

dreispt

+

This module is part of the OCA/l10n-france project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ +