""" Fusion Accounting - EDI Format Registry Provides a configuration model for registering electronic document interchange formats. Each format record carries a unique code and delegates the actual XML generation / parsing to dedicated generator classes (e.g. ``FusionUBLGenerator``, ``FusionCIIGenerator``). Administrators may restrict a format to customer invoices, vendor bills, or allow it for both through the ``applicable_to`` field. Original implementation by Nexa Systems Inc. """ import logging from odoo import api, fields, models, _ from odoo.exceptions import UserError _log = logging.getLogger(__name__) class FusionEDIFormat(models.Model): """ Registry entry for a supported EDI format. This model acts as a strategy-pattern dispatcher: the ``code`` field selects the concrete generator/parser to invoke when creating or reading an electronic document. Format records are typically seeded via XML data files and should not be deleted while EDI documents reference them. """ _name = "fusion.edi.format" _description = "Fusion EDI Format" _order = "sequence, name" _rec_name = "name" # ------------------------------------------------------------------ # Fields # ------------------------------------------------------------------ name = fields.Char( string="Format Name", required=True, help="Human-readable name shown in selection lists.", ) code = fields.Char( string="Code", required=True, help=( "Unique technical identifier used to dispatch generation / " "parsing logic. Examples: 'ubl_21', 'cii', 'facturx'." ), ) description = fields.Text( string="Description", help="Optional notes about the format, its version, or usage.", ) applicable_to = fields.Selection( selection=[ ("invoices", "Customer Invoices / Credit Notes"), ("bills", "Vendor Bills"), ("both", "Both"), ], string="Applicable To", default="both", required=True, help="Restricts this format to customer-side, vendor-side, or both.", ) active = fields.Boolean( string="Active", default=True, help="Inactive formats are hidden from selection lists.", ) sequence = fields.Integer( string="Sequence", default=10, help="Controls display ordering in lists and dropdowns.", ) # ------------------------------------------------------------------ # Constraints # ------------------------------------------------------------------ _sql_constraints = [ ( "code_unique", "UNIQUE(code)", "Each EDI format must have a unique code.", ), ] # ------------------------------------------------------------------ # Generation / Parsing Dispatch # ------------------------------------------------------------------ def generate_document(self, move): """Generate an electronic document for the given journal entry. Dispatches to the appropriate generator based on ``self.code``. Args: move: An ``account.move`` recordset (single record). Returns: bytes: The XML payload of the generated document. Raises: UserError: When no generator is registered for this format code or the move type is incompatible. """ self.ensure_one() move.ensure_one() self._check_applicability(move) generator_map = self._get_generator_map() generator_method = generator_map.get(self.code) if not generator_method: raise UserError( _("No generator is registered for EDI format '%s'.", self.code) ) return generator_method(move) def parse_document(self, xml_bytes): """Parse an incoming EDI XML document and return invoice data. Dispatches to the appropriate parser based on ``self.code``. Args: xml_bytes (bytes): Raw XML content to parse. Returns: dict: A dictionary of invoice field values ready for ``account.move.create()``. Raises: UserError: When no parser is registered for this format code. """ self.ensure_one() parser_map = self._get_parser_map() parser_method = parser_map.get(self.code) if not parser_method: raise UserError( _("No parser is registered for EDI format '%s'.", self.code) ) return parser_method(xml_bytes) # ------------------------------------------------------------------ # Internal dispatch helpers # ------------------------------------------------------------------ def _get_generator_map(self): """Return a mapping of format codes to generator callables. Each callable accepts a single ``account.move`` record and returns ``bytes`` (the XML payload). """ ubl = self.env["fusion.ubl.generator"] cii = self.env["fusion.cii.generator"] return { "ubl_21": ubl.generate_ubl_invoice, "cii": cii.generate_cii_invoice, "facturx": cii.generate_cii_invoice, } def _get_parser_map(self): """Return a mapping of format codes to parser callables. Each callable accepts ``bytes`` (raw XML) and returns a ``dict`` of invoice values. """ ubl = self.env["fusion.ubl.generator"] cii = self.env["fusion.cii.generator"] return { "ubl_21": ubl.parse_ubl_invoice, "cii": cii.parse_cii_invoice, "facturx": cii.parse_cii_invoice, } def _check_applicability(self, move): """Verify that this format is applicable to the given move type. Raises: UserError: When the format/move combination is invalid. """ self.ensure_one() if self.applicable_to == "invoices" and move.move_type in ( "in_invoice", "in_refund", ): raise UserError( _( "EDI format '%s' is restricted to customer invoices / " "credit notes and cannot be used for vendor bills.", self.name, ) ) if self.applicable_to == "bills" and move.move_type in ( "out_invoice", "out_refund", ): raise UserError( _( "EDI format '%s' is restricted to vendor bills and " "cannot be used for customer invoices.", self.name, ) )