Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0][ADD] export_invoice_edi_auchan module #295

Open
wants to merge 10 commits into
base: 16.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
1 change: 1 addition & 0 deletions export_invoice_edi_auchan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
19 changes: 19 additions & 0 deletions export_invoice_edi_auchan/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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",
],
}
16 changes: 16 additions & 0 deletions export_invoice_edi_auchan/data/task_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo noupdate="1">
<record id="auchan_fs_storage" model="fs.storage">
<field name="name">@gp Auchan</field>
<field name="protocol">ftp</field>
<field name="code">ftpgp</field>
</record>

<record id="export_to_auchan_ftp" model="attachment.synchronize.task">
<field name="name">Export EDI Auchan</field>
<field name="backend_id" ref="auchan_fs_storage" />
<field name="method_type">export</field>
<field name="filepath">factures</field>
</record>

</odoo>
2 changes: 2 additions & 0 deletions export_invoice_edi_auchan/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import account_move
from . import res_partner
129 changes: 129 additions & 0 deletions export_invoice_edi_auchan/models/account_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 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",
)

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("/", "-")
13 changes: 13 additions & 0 deletions export_invoice_edi_auchan/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -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()
73 changes: 73 additions & 0 deletions export_invoice_edi_auchan/schema/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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="", required=True, ctx=False):
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
15 changes: 15 additions & 0 deletions export_invoice_edi_auchan/schema/invoice_footer.py
Original file line number Diff line number Diff line change
@@ -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
]
75 changes: 75 additions & 0 deletions export_invoice_edi_auchan/schema/invoice_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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, "", False), # Heure de commande HH:MN opt
(10, "", False), # date du message opt
(5, "", 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, "", False), # Date avis d'expédition JJ/MM/AAAA opt
(35, "", False), # Numéro de l'avis d'expédition opt
(10, "", False), # Date d'enlèvement JJ/MM/AAAA opt
(5, "", 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, "", False), # Date d'échéance pour l'escompte JJ/MM/AAAA opt
(
10,
"",
False,
), # Montant de l'escompte (le pourcentage de l'escompte est préconisé) opt
(
35,
"",
False,
), # Numéro de facture en référence (obligatoire si avoir) opt
(10, "", False), # Date de facture en référence (obligatoire si avoir) opt
(6, "", False), # Pourcentage de l'escompte opt
(3, "", False), # Nb de jour de l'escompte opt
(6, "", False), # Pourcentage de pénalité opt
(3, "", 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 ?
]
Loading
Loading