diff --git a/export_invoice_edi_auchan/README.rst b/export_invoice_edi_auchan/README.rst
new file mode 100644
index 000000000..e69de29bb
diff --git a/export_invoice_edi_auchan/__init__.py b/export_invoice_edi_auchan/__init__.py
new file mode 100644
index 000000000..0650744f6
--- /dev/null
+++ b/export_invoice_edi_auchan/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/export_invoice_edi_auchan/__manifest__.py b/export_invoice_edi_auchan/__manifest__.py
new file mode 100644
index 000000000..8f173c1a5
--- /dev/null
+++ b/export_invoice_edi_auchan/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ "name": "Custom export invoice edi format Auchan",
+ "version": "16.0.0.0.0",
+ "author": "Akretion",
+ "category": "EDI",
+ "website": "https://github.com/akretion/ak-odoo-incubator",
+ "license": "AGPL-3",
+ "depends": [
+ "account",
+ "stock_picking_invoice_link",
+ "fs_storage",
+ "attachment_synchronize_record",
+ ],
+ "data": [
+ "data/task_data.xml",
+ "views/res_partner.xml",
+ "views/account_move.xml",
+ ],
+}
diff --git a/export_invoice_edi_auchan/data/task_data.xml b/export_invoice_edi_auchan/data/task_data.xml
new file mode 100644
index 000000000..09fe0ae32
--- /dev/null
+++ b/export_invoice_edi_auchan/data/task_data.xml
@@ -0,0 +1,16 @@
+
+
+
+ @gp Auchan
+ ftp
+ ftpgp
+
+
+
+ Export EDI Auchan
+
+ export
+ factures
+
+
+
diff --git a/export_invoice_edi_auchan/models/__init__.py b/export_invoice_edi_auchan/models/__init__.py
new file mode 100644
index 000000000..74548553b
--- /dev/null
+++ b/export_invoice_edi_auchan/models/__init__.py
@@ -0,0 +1,2 @@
+from . import account_move
+from . import res_partner
diff --git a/export_invoice_edi_auchan/models/account_move.py b/export_invoice_edi_auchan/models/account_move.py
new file mode 100644
index 000000000..c83d2b88c
--- /dev/null
+++ b/export_invoice_edi_auchan/models/account_move.py
@@ -0,0 +1,130 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+import logging
+from io import StringIO
+from os import linesep
+
+from odoo import fields, models
+
+from ..schema.base import SegmentInterfaceExc
+from ..schema.invoice_footer import PIESegment
+from ..schema.invoice_header import ENTSegment
+from ..schema.invoice_line import LIGSegment
+from ..schema.invoice_taxes import TVASegment
+from ..schema.partner import PARSegment
+
+_logger = logging.getLogger()
+
+
+class AccountMove(models.Model):
+ _inherit = ["account.move", "synchronize.exportable.mixin"]
+ _name = "account.move"
+
+ is_edi_exportable = fields.Boolean(
+ related="partner_id.is_edi_exportable",
+ )
+ regenerate_edi = fields.Boolean()
+
+ def _find_bl_info(self):
+ """Find entête "Numéro de BL" and date"""
+ raise NotImplementedError
+
+ def _render_segment(self, segment, vals):
+ try:
+ res = segment(**vals).render()
+ except SegmentInterfaceExc as e:
+ self.env.context["export_auchan_errors"].append(str(e))
+ else:
+ return res
+
+ def _prepare_export_data(self, idx):
+ self.ensure_one()
+ _logger.info(f"Exporting {self.name}")
+ res = []
+ source_orders = self.line_ids.sale_line_ids.order_id or self.env[
+ "sale.order"
+ ].search([("name", "=", self.invoice_origin)])
+ bl_nbr, bl_date = self._find_bl_info()
+ self = self.with_context(export_auchan_errors=[])
+ # Segment Entete facture
+ res.append(
+ self._render_segment(
+ ENTSegment,
+ {
+ "invoice": self,
+ "source_orders": source_orders,
+ "bl_nbr": bl_nbr,
+ "bl_date": bl_date,
+ },
+ )
+ )
+ # segment partner
+ res.append(
+ self._render_segment(
+ PARSegment,
+ {
+ "invoice": self,
+ },
+ )
+ )
+ # segment ligne de fatcure
+ for idx, line in enumerate(self.invoice_line_ids, start=1):
+ res.append(
+ self._render_segment(
+ LIGSegment,
+ {
+ "line": line,
+ "line_num": str(idx),
+ },
+ )
+ )
+ # Segment pied facture
+ res.append(
+ self._render_segment(
+ PIESegment,
+ {
+ "invoice": self,
+ },
+ )
+ )
+ # segment ligne de TVA (détail des TVA)
+ for tax_line in self.line_ids.filtered(lambda x: x.tax_line_id):
+ res.append(
+ self._render_segment(
+ TVASegment,
+ {
+ "tax_line": tax_line,
+ },
+ )
+ )
+ # Segment END
+ res.append("END")
+ errs = self.env.context.get("export_auchan_errors")
+ if errs:
+ errstr = "Erreur lors de la génération du fichier Auchan: \n"
+ errstr += "\n".join(errs)
+ _logger.error(errstr)
+ raise ValueError(errstr)
+ return res
+
+ def _get_export_task(self):
+ return self.env.ref("export_invoice_edi_auchan.export_to_auchan_ftp")
+
+ def _prepare_aq_data(self, data):
+ _logger.info(f"Exporting {self} to EDI Auchan")
+ if self._name == "account.move":
+ return self._format_to_exportfile_auchan_edi(data)
+ return self._prepare_aq_data_csv(data)
+
+ def _format_to_exportfile_auchan_edi(self, data):
+ txt_file = StringIO()
+ for row in data:
+ txt_file.write(row)
+ txt_file.write(linesep)
+ txt_file.seek(0)
+
+ return txt_file.getvalue().encode("utf-8")
+
+ def _get_export_name(self):
+ return self.name.replace("/", "-")
diff --git a/export_invoice_edi_auchan/models/res_partner.py b/export_invoice_edi_auchan/models/res_partner.py
new file mode 100644
index 000000000..1d59f809e
--- /dev/null
+++ b/export_invoice_edi_auchan/models/res_partner.py
@@ -0,0 +1,13 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ barcode = fields.Char(
+ string="Code EAN",
+ )
+ is_edi_exportable = fields.Boolean()
diff --git a/export_invoice_edi_auchan/schema/base.py b/export_invoice_edi_auchan/schema/base.py
new file mode 100644
index 000000000..c741747e0
--- /dev/null
+++ b/export_invoice_edi_auchan/schema/base.py
@@ -0,0 +1,78 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+import datetime
+
+import freezegun
+from unidecode import unidecode
+
+from odoo.tools import float_compare
+
+
+class SegmentInterfaceExc(Exception):
+ pass
+
+
+class SegmentInterface:
+ def __init__(self, **kwargs):
+ self.__dict__.update(kwargs)
+
+ def _format_values(self, size, value="", ctx={}):
+ required = True
+ if not ctx:
+ ctx = {} # Fix bug bool value
+ if "required" in ctx.keys():
+ required = ctx.get("required")
+ if required and (value is False or value is None or value == ""):
+ raise ValueError()
+ if not ctx:
+ ctx = {}
+ if not value:
+ value = ""
+ if isinstance(value, freezegun.api.FakeDate):
+ fmt_val = datetime.date.strftime(value, "%d/%m/%Y")
+ elif isinstance(value, datetime.datetime):
+ fmt_val = datetime.date.strftime(value.date(), "%d/%m/%Y %H:%M")
+ elif isinstance(value, int):
+ fmt_val = str(value)
+ elif isinstance(value, float):
+ if float_compare(value, 0, 3) == 0 and ctx.get("empty_if_zero"):
+ fmt_val = ""
+ else:
+ if ctx.get("decimal_3"):
+ fmt_val = str("{:.3f}".format(value))
+ else:
+ fmt_val = str("{:.2f}".format(value))
+ elif isinstance(value, str):
+ fmt_val = unidecode(value)
+ else:
+ raise ValueError(f"Unsupported value type: {type(value)}")
+ if len(fmt_val) > size:
+ if ctx.get("truncate_silent"):
+ fmt_val = fmt_val[:size]
+ else:
+ errorstr = "{} trop long, taille maximale est de {}".format(
+ fmt_val, size
+ )
+ if ctx:
+ errorstr = "contexte: {}".format(ctx) + errorstr
+ raise ValueError(errorstr)
+ return fmt_val
+
+ def render(self):
+ res = ""
+ errors = []
+ for idx, fmt_data in enumerate(self.get_values(), start=1):
+ try:
+ fmt_val = self._format_values(*fmt_data)
+ res += fmt_val + ";"
+ except ValueError:
+ errors += [(self.__class__.__name__, idx)]
+ if errors:
+ errstr = ""
+ for el in errors:
+ errstr += f"Segment {el[0]}: missing value on line {el[1]}\n"
+ raise SegmentInterfaceExc(errstr)
+ return res[:-1]
+
+ def get_values(self):
+ raise NotImplementedError
diff --git a/export_invoice_edi_auchan/schema/invoice_footer.py b/export_invoice_edi_auchan/schema/invoice_footer.py
new file mode 100644
index 000000000..4723de544
--- /dev/null
+++ b/export_invoice_edi_auchan/schema/invoice_footer.py
@@ -0,0 +1,15 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+
+from .base import SegmentInterface
+
+
+class PIESegment(SegmentInterface):
+ def get_values(self):
+ return [
+ (3, "PIE"),
+ (10, self.invoice.amount_untaxed), # Montant total hors taxes
+ (10, self.invoice.amount_tax), # Montant taxes
+ (10, self.invoice.amount_total), # Montant total TTC
+ ]
diff --git a/export_invoice_edi_auchan/schema/invoice_header.py b/export_invoice_edi_auchan/schema/invoice_header.py
new file mode 100644
index 000000000..be23b5d0c
--- /dev/null
+++ b/export_invoice_edi_auchan/schema/invoice_header.py
@@ -0,0 +1,83 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+
+from .base import SegmentInterface
+
+
+class ENTSegment(SegmentInterface):
+ def get_values(self):
+ return [
+ (3, "ENT"), # Étiquette de segment "ENT"
+ (70, self.invoice.invoice_origin), # Numéro de commande du client
+ (
+ 10,
+ self.source_orders and self.source_orders[0].date_order or "",
+ ), # Date de commande JJ/MM/AAAA
+ (5, "", {"required": False}), # Heure de commande HH:MN opt
+ (10, "", {"required": False}), # date du message opt
+ (5, "", {"required": False}), # Heure du message opt
+ (
+ 10,
+ self.bl_date,
+ ), # Date du BL JJ/MM/AAAA
+ (
+ 35,
+ self.bl_nbr,
+ ), # num du BL JJ/MM/AAAA
+ (10, "", {"required": False}), # Date avis d'expédition JJ/MM/AAAA opt
+ (35, "", {"required": False}), # Numéro de l'avis d'expédition opt
+ (10, "", {"required": False}), # Date d'enlèvement JJ/MM/AAAA opt
+ (5, "", {"required": False}), # Heure d'enlèvement HH:MN opt
+ (35, self.invoice.name), # Numéro de document
+ (
+ 16,
+ self.invoice.invoice_date,
+ ), # Date/heure facture ou avoir (document) JJ/MM/AAAA HH:MN
+ (10, self.invoice.invoice_date_due), # Date d'échéance JJ/MM/AAAA
+ (
+ 7,
+ self.invoice.move_type == "out_invoice"
+ and "Facture"
+ or (self.invoice.move_type == "out_refund" and "Avoir")
+ or "",
+ ), # Type de document (Facture/Avoir)
+ # depend on 'move_type', 'in', ('out_invoice', 'out_refund')
+ (3, self.invoice.currency_id.name), # Code monnaie (EUR pour Euro)
+ (
+ 10,
+ "",
+ {"required": False},
+ ), # Date d'échéance pour l'escompte JJ/MM/AAAA opt
+ (
+ 10,
+ "",
+ {"required": False},
+ ), # Montant de l'escompte (le pourcentage de l'escompte est préconisé) opt
+ (
+ 35,
+ "",
+ {"required": False},
+ ), # Numéro de facture en référence (obligatoire si avoir) opt
+ (
+ 10,
+ "",
+ {"required": False},
+ ), # Date de facture en référence (obligatoire si avoir) opt
+ (6, "", {"required": False}), # Pourcentage de l'escompte opt
+ (3, "", {"required": False}), # Nb de jour de l'escompte opt
+ (6, "", {"required": False}), # Pourcentage de pénalité opt
+ (3, "", {"required": False}), # Nb de jour de pénalité opt
+ (
+ 1,
+ self.invoice.env.context.get("test_mode") and "1" or "0",
+ ), # Document de test (1/0)
+ (
+ 3,
+ "42",
+ ), # Code paiement (cf table ENT.27) 42 ==> Paiement à un compte
+ # bancaire (virement client)
+ # fix ==> rendre ce champs dynamique ?
+ (3, "MAR"), # Nature document (MAR pour marchandise et SRV pour service)
+ # fix ==> rendre ce champs dynamique ?
+ ]
diff --git a/export_invoice_edi_auchan/schema/invoice_line.py b/export_invoice_edi_auchan/schema/invoice_line.py
new file mode 100644
index 000000000..c9929d973
--- /dev/null
+++ b/export_invoice_edi_auchan/schema/invoice_line.py
@@ -0,0 +1,63 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+
+from .base import SegmentInterface
+
+
+class LIGSegment(SegmentInterface):
+ def get_values(self):
+ uom = (
+ self.line.product_uom_id.name == "kg"
+ and "KGM"
+ or self.line.product_uom_id.name == "m"
+ and "MTR"
+ or "PCE"
+ )
+
+ return [
+ (3, "LIG"),
+ (
+ 6,
+ self.line_num,
+ ), # Numéro de ligne (de 1 à n remis à 0 pour chaque facture)
+ (14, self.line.product_id.barcode), # Code EAN produit
+ (
+ 35,
+ self.line.product_id.default_code,
+ ), # Code interne produit chez le fournisseur
+ (
+ 10,
+ self.line.sale_line_ids
+ and self.line.sale_line_ids[0].product_packaging_id.qty
+ or 1,
+ ), # Par combien
+ # (multiple de commande)
+ (
+ 10,
+ self.line.sale_line_ids
+ and self.line.sale_line_ids[0].product_uom_qty
+ or self.line.quantity,
+ ),
+ # Quantité commandée
+ (3, uom), # Unité de quantité (PCE = pièce, KGM = kilogramme, MTR = mètre)
+ (10, self.line.quantity), # Quantité facturée
+ (15, self.line.price_unit), # Prix unitaire net
+ (3, self.line.move_id.currency_id.name), # Code monnaie (EUR = euro)
+ (1, "", {"required": False}),
+ (1, "", {"required": False}),
+ (5, self.line.tax_ids and self.line.tax_ids[0].amount or 0),
+ (
+ 15,
+ self.line.quantity
+ and (self.line.price_total / self.line.quantity)
+ or 0.0,
+ ), # Prix unitaire brut
+ (1, "", {"required": False}),
+ (
+ 70,
+ self.line.name and self.line.name.replace("\n", " ") or "",
+ {"truncate_silent": True},
+ ),
+ (17, self.line.price_subtotal), # Montant Net Ht de la ligne
+ ]
diff --git a/export_invoice_edi_auchan/schema/invoice_taxes.py b/export_invoice_edi_auchan/schema/invoice_taxes.py
new file mode 100644
index 000000000..8dcc6dc93
--- /dev/null
+++ b/export_invoice_edi_auchan/schema/invoice_taxes.py
@@ -0,0 +1,20 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+
+from .base import SegmentInterface
+
+
+class TVASegment(SegmentInterface):
+ def get_values(self):
+ return [
+ (3, "TVA"), # Étiquette de segment "TVA"
+ (5, self.tax_line.tax_line_id.amount or 0.0),
+ (10, self.tax_line.tax_base_amount or 0.0),
+ (
+ 10,
+ self.tax_line.amount_currency
+ and abs(self.tax_line.amount_currency)
+ or 0.0,
+ ),
+ ]
diff --git a/export_invoice_edi_auchan/schema/partner.py b/export_invoice_edi_auchan/schema/partner.py
new file mode 100644
index 000000000..24a811654
--- /dev/null
+++ b/export_invoice_edi_auchan/schema/partner.py
@@ -0,0 +1,51 @@
+# Copyright 2023 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+
+from .base import SegmentInterface
+
+
+class PARSegment(SegmentInterface):
+ def get_values(self):
+ return [
+ (3, "PAR"),
+ (13, self.invoice.partner_shipping_id.barcode), # Code EAN client
+ (
+ 35,
+ self.invoice.partner_shipping_id.name,
+ {"required": False},
+ ), # Libellé client
+ (
+ 13,
+ self.invoice.company_id.partner_id.barcode,
+ ), # Code EAN Fournisseur (vendeur)
+ (
+ 35,
+ self.invoice.company_id.partner_id.name,
+ False,
+ ), # Libellé Fournisseur (vendeur)
+ (13, self.invoice.partner_shipping_id.barcode), # Code EAN client livré
+ (
+ 35,
+ self.invoice.partner_shipping_id.name,
+ {"required": False},
+ ), # Libellé client livré
+ (13, self.invoice.partner_id.barcode), # Code EAN client facturé à
+ (
+ 35,
+ self.invoice.partner_id.name,
+ {"required": False},
+ ), # Libellé client facturé à
+ (10, "", {"required": False}), # Code EAN factor (obligatoire si factor)
+ (
+ 10,
+ "",
+ {"required": False},
+ ), # Libellé alias factor (obligatoire si factor)
+ (13, self.invoice.company_id.partner_id.barcode), # Code EAN régler à
+ (
+ 35,
+ self.invoice.company_id.partner_id.name,
+ {"required": False},
+ ), # Libellé régler à
+ ]
diff --git a/export_invoice_edi_auchan/views/account_move.xml b/export_invoice_edi_auchan/views/account_move.xml
new file mode 100644
index 000000000..1854ecf58
--- /dev/null
+++ b/export_invoice_edi_auchan/views/account_move.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ account.move
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/export_invoice_edi_auchan/views/res_partner.xml b/export_invoice_edi_auchan/views/res_partner.xml
new file mode 100644
index 000000000..644a9361b
--- /dev/null
+++ b/export_invoice_edi_auchan/views/res_partner.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ res.partner.form (Auchan Fac edi Format)
+ res.partner
+
+
+
+
+
+
+
+
+
+
diff --git a/setup/export_invoice_edi_auchan/odoo/addons/export_invoice_edi_auchan b/setup/export_invoice_edi_auchan/odoo/addons/export_invoice_edi_auchan
new file mode 120000
index 000000000..108fcce6c
--- /dev/null
+++ b/setup/export_invoice_edi_auchan/odoo/addons/export_invoice_edi_auchan
@@ -0,0 +1 @@
+../../../../export_invoice_edi_auchan
\ No newline at end of file
diff --git a/setup/export_invoice_edi_auchan/setup.py b/setup/export_invoice_edi_auchan/setup.py
new file mode 100644
index 000000000..28c57bb64
--- /dev/null
+++ b/setup/export_invoice_edi_auchan/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)