Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View 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"