Initial commit
This commit is contained in:
235
Fusion Accounting/models/edi_document.py
Normal file
235
Fusion Accounting/models/edi_document.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
Fusion Accounting - EDI Document Framework
|
||||
|
||||
Manages the lifecycle of Electronic Data Interchange (EDI) documents
|
||||
associated with accounting journal entries. Each EDI document tracks a
|
||||
single rendition of an invoice in a specific electronic format (UBL, CII,
|
||||
etc.), from initial generation through transmission and eventual
|
||||
cancellation when required.
|
||||
|
||||
Original implementation by Nexa Systems Inc.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionEDIDocument(models.Model):
|
||||
"""
|
||||
Represents one EDI rendition of a journal entry.
|
||||
|
||||
A single ``account.move`` may have several EDI documents if the
|
||||
company is required to report in more than one format (e.g. UBL for
|
||||
Peppol and CII for Factur-X). Each record progresses through a
|
||||
linear state machine:
|
||||
|
||||
to_send -> sent -> to_cancel -> cancelled
|
||||
|
||||
Errors encountered during generation or transmission are captured in
|
||||
``error_message`` and the document remains in its current state so
|
||||
the user can resolve the issue and retry.
|
||||
"""
|
||||
|
||||
_name = "fusion.edi.document"
|
||||
_description = "Fusion EDI Document"
|
||||
_order = "create_date desc"
|
||||
_rec_name = "display_name"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Fields
|
||||
# ------------------------------------------------------------------
|
||||
move_id = fields.Many2one(
|
||||
comodel_name="account.move",
|
||||
string="Journal Entry",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
help="The journal entry that this EDI document represents.",
|
||||
)
|
||||
edi_format_id = fields.Many2one(
|
||||
comodel_name="fusion.edi.format",
|
||||
string="EDI Format",
|
||||
required=True,
|
||||
ondelete="restrict",
|
||||
help="The electronic format used for this document.",
|
||||
)
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
("to_send", "To Send"),
|
||||
("sent", "Sent"),
|
||||
("to_cancel", "To Cancel"),
|
||||
("cancelled", "Cancelled"),
|
||||
],
|
||||
string="Status",
|
||||
default="to_send",
|
||||
required=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
help=(
|
||||
"Lifecycle state of the EDI document.\n"
|
||||
"- To Send: document needs to be generated and/or transmitted.\n"
|
||||
"- Sent: document has been successfully delivered.\n"
|
||||
"- To Cancel: a cancellation has been requested.\n"
|
||||
"- Cancelled: the document has been formally cancelled."
|
||||
),
|
||||
)
|
||||
attachment_id = fields.Many2one(
|
||||
comodel_name="ir.attachment",
|
||||
string="Attachment",
|
||||
copy=False,
|
||||
ondelete="set null",
|
||||
help="The generated XML/PDF file for this EDI document.",
|
||||
)
|
||||
error_message = fields.Text(
|
||||
string="Error Message",
|
||||
copy=False,
|
||||
readonly=True,
|
||||
help="Details of the last error encountered during processing.",
|
||||
)
|
||||
blocking_level = fields.Selection(
|
||||
selection=[
|
||||
("info", "Info"),
|
||||
("warning", "Warning"),
|
||||
("error", "Error"),
|
||||
],
|
||||
string="Error Severity",
|
||||
copy=False,
|
||||
help="Severity of the last processing error.",
|
||||
)
|
||||
|
||||
# Related / display helpers
|
||||
move_name = fields.Char(
|
||||
related="move_id.name",
|
||||
string="Invoice Number",
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
related="move_id.partner_id",
|
||||
string="Partner",
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
related="move_id.company_id",
|
||||
string="Company",
|
||||
store=True,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Computed display name
|
||||
# ------------------------------------------------------------------
|
||||
@api.depends("move_id.name", "edi_format_id.name")
|
||||
def _compute_display_name(self):
|
||||
for doc in self:
|
||||
doc.display_name = (
|
||||
f"{doc.move_id.name or _('Draft')} - "
|
||||
f"{doc.edi_format_id.name or _('Unknown Format')}"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Actions
|
||||
# ------------------------------------------------------------------
|
||||
def action_send(self):
|
||||
"""Generate the EDI file and advance the document to *sent*.
|
||||
|
||||
Delegates the actual XML/PDF creation to the linked
|
||||
``fusion.edi.format`` record. On success the resulting binary
|
||||
payload is stored as an ``ir.attachment`` and the state flips to
|
||||
``sent``. Errors are captured rather than raised so that batch
|
||||
processing can continue for the remaining documents.
|
||||
"""
|
||||
for doc in self:
|
||||
if doc.state != "to_send":
|
||||
raise UserError(
|
||||
_("Only documents in 'To Send' state can be sent. "
|
||||
"Document '%s' is in state '%s'.",
|
||||
doc.display_name, doc.state)
|
||||
)
|
||||
try:
|
||||
xml_bytes = doc.edi_format_id.generate_document(doc.move_id)
|
||||
if not xml_bytes:
|
||||
doc.write({
|
||||
"error_message": _(
|
||||
"The EDI format returned an empty document."
|
||||
),
|
||||
"blocking_level": "error",
|
||||
})
|
||||
continue
|
||||
|
||||
filename = doc._build_attachment_filename()
|
||||
attachment = self.env["ir.attachment"].create({
|
||||
"name": filename,
|
||||
"raw": xml_bytes,
|
||||
"res_model": doc.move_id._name,
|
||||
"res_id": doc.move_id.id,
|
||||
"mimetype": "application/xml",
|
||||
"type": "binary",
|
||||
})
|
||||
doc.write({
|
||||
"attachment_id": attachment.id,
|
||||
"state": "sent",
|
||||
"error_message": False,
|
||||
"blocking_level": False,
|
||||
})
|
||||
_log.info(
|
||||
"EDI document %s generated successfully for %s.",
|
||||
doc.edi_format_id.code,
|
||||
doc.move_id.name,
|
||||
)
|
||||
except Exception as exc:
|
||||
_log.exception(
|
||||
"Failed to generate EDI document for %s.", doc.move_id.name
|
||||
)
|
||||
doc.write({
|
||||
"error_message": str(exc),
|
||||
"blocking_level": "error",
|
||||
})
|
||||
|
||||
def action_cancel(self):
|
||||
"""Request cancellation of a previously sent EDI document."""
|
||||
for doc in self:
|
||||
if doc.state not in ("sent", "to_cancel"):
|
||||
raise UserError(
|
||||
_("Only sent documents can be cancelled. "
|
||||
"Document '%s' is in state '%s'.",
|
||||
doc.display_name, doc.state)
|
||||
)
|
||||
doc.write({
|
||||
"state": "cancelled",
|
||||
"error_message": False,
|
||||
"blocking_level": False,
|
||||
})
|
||||
_log.info(
|
||||
"EDI document %s cancelled for %s.",
|
||||
doc.edi_format_id.code,
|
||||
doc.move_id.name,
|
||||
)
|
||||
|
||||
def action_retry(self):
|
||||
"""Reset a failed document back to *to_send* so it can be re-processed."""
|
||||
for doc in self:
|
||||
if not doc.error_message:
|
||||
raise UserError(
|
||||
_("Document '%s' has no error to retry.", doc.display_name)
|
||||
)
|
||||
doc.write({
|
||||
"state": "to_send",
|
||||
"error_message": False,
|
||||
"blocking_level": False,
|
||||
"attachment_id": False,
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
def _build_attachment_filename(self):
|
||||
"""Construct a human-readable filename for the EDI attachment.
|
||||
|
||||
Returns:
|
||||
str: e.g. ``INV-2026-00001_ubl21.xml``
|
||||
"""
|
||||
self.ensure_one()
|
||||
move_name = (self.move_id.name or "DRAFT").replace("/", "-")
|
||||
fmt_code = self.edi_format_id.code or "edi"
|
||||
return f"{move_name}_{fmt_code}.xml"
|
||||
Reference in New Issue
Block a user