mirror of
https://github.com/bringout/oca-edi.git
synced 2026-04-23 18:52:02 +02:00
Initial commit: OCA Edi packages (42 packages)
This commit is contained in:
commit
df976c03db
2184 changed files with 571602 additions and 0 deletions
|
|
@ -0,0 +1,6 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import account_move
|
||||
from . import ir_actions_report
|
||||
|
|
@ -0,0 +1,574 @@
|
|||
# Copyright 2016-2017 Akretion (http://www.akretion.com)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# Copyright 2019 Onestein (<https://www.onestein.eu>)
|
||||
# Copyright 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo.tools import float_is_zero, float_round
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_name = "account.move"
|
||||
_inherit = ["account.move", "base.ubl"]
|
||||
|
||||
def _ubl_add_header(self, parent_node, ns, version="2.1"):
|
||||
self.ensure_one()
|
||||
ubl_version = etree.SubElement(parent_node, ns["cbc"] + "UBLVersionID")
|
||||
ubl_version.text = version
|
||||
doc_id = etree.SubElement(parent_node, ns["cbc"] + "ID")
|
||||
doc_id.text = self.name
|
||||
issue_date = etree.SubElement(parent_node, ns["cbc"] + "IssueDate")
|
||||
issue_date.text = self.invoice_date.strftime("%Y-%m-%d")
|
||||
if (
|
||||
self.invoice_date_due
|
||||
and version >= "2.1"
|
||||
and self.move_type == "out_invoice"
|
||||
):
|
||||
due_date = etree.SubElement(parent_node, ns["cbc"] + "DueDate")
|
||||
due_date.text = fields.Date.to_string(self.invoice_date_due)
|
||||
if self.move_type == "out_invoice":
|
||||
type_code = etree.SubElement(parent_node, ns["cbc"] + "InvoiceTypeCode")
|
||||
elif self.move_type == "out_refund":
|
||||
type_code = etree.SubElement(parent_node, ns["cbc"] + "CreditNoteTypeCode")
|
||||
type_code.text = self._ubl_get_invoice_type_code()
|
||||
if self.narration:
|
||||
note = etree.SubElement(parent_node, ns["cbc"] + "Note")
|
||||
note.text = self.narration
|
||||
doc_currency = etree.SubElement(parent_node, ns["cbc"] + "DocumentCurrencyCode")
|
||||
doc_currency.text = self.currency_id.name
|
||||
# TODO: enable when below commit of 15.0 is back ported to 14.0
|
||||
# [IMP] account_edi(_*): Standalone UBL format + edi.format inheritance
|
||||
# https://github.com/odoo/odoo/commit/b58810a77bb4c432a6aef18413659b1ea7b25c71
|
||||
# or when migrating to 15.0
|
||||
# buyer_reference = etree.SubElement(parent_node, ns["cbc"] + "BuyerReference")
|
||||
# buyer_reference.text = self.ref or ""
|
||||
|
||||
def _ubl_get_invoice_type_code(self):
|
||||
if self.move_type == "out_invoice":
|
||||
return "380"
|
||||
elif self.move_type == "out_refund":
|
||||
return "381"
|
||||
|
||||
def _ubl_get_order_reference(self):
|
||||
"""An identifier of a referenced purchase order, issued by the Buyer"""
|
||||
return self.ref or "/"
|
||||
|
||||
def _ubl_get_salesorder_reference(self):
|
||||
"""An identifier of a referenced sales order, issued by the Seller"""
|
||||
return self.invoice_origin
|
||||
|
||||
def _ubl_add_order_reference(self, parent_node, ns, version="2.1"):
|
||||
self.ensure_one()
|
||||
buyer_ref = self._ubl_get_order_reference()
|
||||
seller_ref = self._ubl_get_salesorder_reference()
|
||||
if buyer_ref or seller_ref:
|
||||
node = etree.SubElement(parent_node, ns["cac"] + "OrderReference")
|
||||
if buyer_ref:
|
||||
node_id = etree.SubElement(node, ns["cbc"] + "ID")
|
||||
node_id.text = buyer_ref
|
||||
if seller_ref:
|
||||
node_salesorderid = etree.SubElement(node, ns["cbc"] + "SalesOrderID")
|
||||
node_salesorderid.text = seller_ref
|
||||
|
||||
def _ubl_get_buyer_reference(self):
|
||||
return self.partner_id.name
|
||||
|
||||
def _ubl_add_buyer_reference(self, parent_node, ns, version="2.1"):
|
||||
self.ensure_one()
|
||||
buyer_ref = self._ubl_get_buyer_reference()
|
||||
if buyer_ref:
|
||||
buyer_order_ref = etree.SubElement(
|
||||
parent_node, ns["cbc"] + "BuyerReference"
|
||||
)
|
||||
buyer_order_ref.text = buyer_ref
|
||||
|
||||
def _ubl_get_contract_document_reference_dict(self):
|
||||
"""Result: dict with key = Doc Type Code, value = ID"""
|
||||
self.ensure_one()
|
||||
return {}
|
||||
|
||||
def _ubl_add_contract_document_reference(self, parent_node, ns, version="2.1"):
|
||||
self.ensure_one()
|
||||
cdr_dict = self._ubl_get_contract_document_reference_dict()
|
||||
for doc_type_code, doc_id in cdr_dict.items():
|
||||
cdr = etree.SubElement(parent_node, ns["cac"] + "ContractDocumentReference")
|
||||
cdr_id = etree.SubElement(cdr, ns["cbc"] + "ID")
|
||||
cdr_id.text = doc_id
|
||||
cdr_type_code = etree.SubElement(cdr, ns["cbc"] + "DocumentTypeCode")
|
||||
cdr_type_code.text = doc_type_code
|
||||
|
||||
def _ubl_add_attachments(self, parent_node, ns, version="2.1"):
|
||||
self.ensure_one()
|
||||
if self.company_id.embed_pdf_in_ubl_xml_invoice and not self.env.context.get(
|
||||
"no_embedded_pdf"
|
||||
):
|
||||
filename = "Invoice-" + self.name + ".pdf"
|
||||
docu_reference = etree.SubElement(
|
||||
parent_node, ns["cac"] + "AdditionalDocumentReference"
|
||||
)
|
||||
docu_reference_id = etree.SubElement(docu_reference, ns["cbc"] + "ID")
|
||||
docu_reference_id.text = filename
|
||||
attach_node = etree.SubElement(docu_reference, ns["cac"] + "Attachment")
|
||||
binary_node = etree.SubElement(
|
||||
attach_node,
|
||||
ns["cbc"] + "EmbeddedDocumentBinaryObject",
|
||||
mimeCode="application/pdf",
|
||||
filename=filename,
|
||||
)
|
||||
ctx = dict()
|
||||
ctx["no_embedded_ubl_xml"] = True
|
||||
ctx["force_report_rendering"] = True
|
||||
pdf_inv = (
|
||||
self.env["ir.actions.report"]
|
||||
.with_context(**ctx)
|
||||
._render_qweb_pdf("account.account_invoices", [self.id])[0]
|
||||
)
|
||||
binary_node.text = base64.b64encode(pdf_inv)
|
||||
|
||||
def _ubl_get_invoice_vat_exclusive_amount(self):
|
||||
amount = self.amount_untaxed
|
||||
# Add also non-VAT taxes that are not subjected to VAT
|
||||
for tline in self.line_ids:
|
||||
if not tline.tax_line_id:
|
||||
continue
|
||||
if tline.tax_line_id.unece_type_id.code != "VAT":
|
||||
sign = 1 if tline.is_refund else -1
|
||||
amount += sign * tline.balance
|
||||
return amount
|
||||
|
||||
def _ubl_get_invoice_vat_amount(self):
|
||||
amount = self.amount_tax
|
||||
# Remove non-VAT taxes that are not subjected to VAT
|
||||
for tline in self.line_ids:
|
||||
if not tline.tax_line_id:
|
||||
continue
|
||||
if tline.tax_line_id.unece_type_id.code != "VAT":
|
||||
sign = 1 if tline.is_refund else -1
|
||||
amount -= sign * tline.balance
|
||||
return amount
|
||||
|
||||
def _ubl_get_charge_total_amount(self):
|
||||
amount = 0.0
|
||||
for tline in self.line_ids:
|
||||
if not tline.tax_line_id:
|
||||
continue
|
||||
if tline.tax_line_id.unece_type_id.code != "VAT":
|
||||
if not tline.tax_line_id.include_base_amount:
|
||||
# For non-VAT taxes, not subject to VAT, they are declared
|
||||
# as AllowanceCharge
|
||||
sign = 1 if tline.is_refund else -1
|
||||
amount += sign * tline.balance
|
||||
return amount
|
||||
|
||||
def _ubl_add_legal_monetary_total(self, parent_node, ns, version="2.1"):
|
||||
self.ensure_one()
|
||||
monetary_total = etree.SubElement(parent_node, ns["cac"] + "LegalMonetaryTotal")
|
||||
cur_name = self.currency_id.name
|
||||
prec = self.currency_id.decimal_places
|
||||
line_total = etree.SubElement(
|
||||
monetary_total, ns["cbc"] + "LineExtensionAmount", currencyID=cur_name
|
||||
)
|
||||
line_total.text = "%0.*f" % (prec, self.amount_untaxed)
|
||||
tax_excl_total = etree.SubElement(
|
||||
monetary_total, ns["cbc"] + "TaxExclusiveAmount", currencyID=cur_name
|
||||
)
|
||||
tax_excl_total.text = "%0.*f" % (
|
||||
prec,
|
||||
self._ubl_get_invoice_vat_exclusive_amount(),
|
||||
)
|
||||
tax_incl_total = etree.SubElement(
|
||||
monetary_total, ns["cbc"] + "TaxInclusiveAmount", currencyID=cur_name
|
||||
)
|
||||
tax_incl_total.text = "%0.*f" % (prec, self.amount_total)
|
||||
charge_total_amount = self._ubl_get_charge_total_amount()
|
||||
if charge_total_amount:
|
||||
el_charge_total_amount = etree.SubElement(
|
||||
monetary_total, ns["cbc"] + "ChargeTotalAmount", currencyID=cur_name
|
||||
)
|
||||
el_charge_total_amount.text = "%0.*f" % (prec, charge_total_amount)
|
||||
prepaid_amount = etree.SubElement(
|
||||
monetary_total, ns["cbc"] + "PrepaidAmount", currencyID=cur_name
|
||||
)
|
||||
prepaid_value = self.amount_total - self.amount_residual
|
||||
prepaid_amount.text = "%0.*f" % (prec, prepaid_value)
|
||||
payable_amount = etree.SubElement(
|
||||
monetary_total, ns["cbc"] + "PayableAmount", currencyID=cur_name
|
||||
)
|
||||
payable_amount.text = "%0.*f" % (prec, self.amount_residual)
|
||||
|
||||
def _ubl_get_invoice_line_price_unit(self, iline):
|
||||
"""Compute the base unit price without taxes"""
|
||||
price = iline.price_unit
|
||||
qty = 1.0
|
||||
if iline.tax_ids:
|
||||
tax_incl = any(t.price_include for t in iline.tax_ids)
|
||||
if tax_incl:
|
||||
# To prevent rounding issue, we must declare tax excluded price
|
||||
# for the total quantity
|
||||
qty = iline.quantity
|
||||
taxes = iline.tax_ids.compute_all(
|
||||
price,
|
||||
self.currency_id,
|
||||
qty,
|
||||
product=iline.product_id,
|
||||
partner=self.partner_id,
|
||||
)
|
||||
if taxes:
|
||||
price = taxes["total_excluded"]
|
||||
dpo = self.env["decimal.precision"]
|
||||
price_precision = dpo.precision_get("Product Price")
|
||||
return price, price_precision, qty
|
||||
|
||||
def _ubl_get_invoice_line_discount(self, iline, base_price, base_qty):
|
||||
# Formula: Net amount = Invoiced quantity * (Item net price/item price
|
||||
# base quantity) + Sum of invoice line charge amount - sum of invoice
|
||||
# line allowance amount
|
||||
discount = iline.quantity / base_qty * base_price - iline.price_subtotal
|
||||
dpo = self.env["decimal.precision"]
|
||||
price_precision = dpo.precision_get("Product Price")
|
||||
discount = float_round(discount, precision_digits=price_precision)
|
||||
return discount, price_precision
|
||||
|
||||
def _ubl_add_invoice_line_discount(
|
||||
self, xml_root, iline, base_price, base_qty, ns, version="2.1"
|
||||
):
|
||||
discount, prec = self._ubl_get_invoice_line_discount(
|
||||
iline, base_price, base_qty
|
||||
)
|
||||
if float_is_zero(discount, precision_digits=prec):
|
||||
return
|
||||
charge_node = etree.SubElement(xml_root, ns["cac"] + "AllowanceCharge")
|
||||
charge_indicator_node = etree.SubElement(
|
||||
charge_node, ns["cbc"] + "ChargeIndicator"
|
||||
)
|
||||
charge_indicator_node.text = "false"
|
||||
charge_reason_code_node = etree.SubElement(
|
||||
charge_node, ns["cbc"] + "AllowanceChargeReasonCode"
|
||||
)
|
||||
charge_reason_code_node.text = "95"
|
||||
charge_reason_node = etree.SubElement(
|
||||
charge_node, ns["cbc"] + "AllowanceChargeReason"
|
||||
)
|
||||
charge_reason_node.text = "Discount"
|
||||
charge_amount_node = etree.SubElement(
|
||||
charge_node, ns["cbc"] + "Amount", currencyID=self.currency_id.name
|
||||
)
|
||||
charge_amount_node.text = "%0.*f" % (prec, discount)
|
||||
|
||||
def _ubl_add_invoice_line(self, parent_node, iline, line_number, ns, version="2.1"):
|
||||
self.ensure_one()
|
||||
cur_name = self.currency_id.name
|
||||
if self.move_type == "out_invoice":
|
||||
line_root = etree.SubElement(parent_node, ns["cac"] + "InvoiceLine")
|
||||
elif self.move_type == "out_refund":
|
||||
line_root = etree.SubElement(parent_node, ns["cac"] + "CreditNoteLine")
|
||||
dpo = self.env["decimal.precision"]
|
||||
qty_precision = dpo.precision_get("Product Unit of Measure")
|
||||
account_precision = self.currency_id.decimal_places
|
||||
line_id = etree.SubElement(line_root, ns["cbc"] + "ID")
|
||||
line_id.text = str(line_number)
|
||||
uom_unece_code = False
|
||||
# product_uom_id is not a required field on account.move.line
|
||||
if self.move_type == "out_invoice":
|
||||
qty_element_name = "InvoicedQuantity"
|
||||
elif self.move_type == "out_refund":
|
||||
qty_element_name = "CreditedQuantity"
|
||||
if iline.product_uom_id.unece_code:
|
||||
uom_unece_code = iline.product_uom_id.unece_code
|
||||
quantity = etree.SubElement(
|
||||
line_root, ns["cbc"] + qty_element_name, unitCode=uom_unece_code
|
||||
)
|
||||
else:
|
||||
quantity = etree.SubElement(line_root, ns["cbc"] + qty_element_name)
|
||||
qty = iline.quantity
|
||||
quantity.text = "%0.*f" % (qty_precision, qty)
|
||||
base_price, price_precision, base_qty = self._ubl_get_invoice_line_price_unit(
|
||||
iline
|
||||
)
|
||||
line_amount = etree.SubElement(
|
||||
line_root, ns["cbc"] + "LineExtensionAmount", currencyID=cur_name
|
||||
)
|
||||
line_amount.text = "%0.*f" % (account_precision, iline.price_subtotal)
|
||||
self._ubl_add_invoice_line_discount(
|
||||
line_root, iline, base_price, base_qty, ns, version=version
|
||||
)
|
||||
self._ubl_add_item(
|
||||
iline.name,
|
||||
iline.product_id,
|
||||
line_root,
|
||||
ns,
|
||||
type_="sale",
|
||||
version=version,
|
||||
)
|
||||
price_node = etree.SubElement(line_root, ns["cac"] + "Price")
|
||||
price_amount = etree.SubElement(
|
||||
price_node, ns["cbc"] + "PriceAmount", currencyID=cur_name
|
||||
)
|
||||
price_amount.text = "%0.*f" % (price_precision, base_price)
|
||||
if uom_unece_code:
|
||||
base_qty_node = etree.SubElement(
|
||||
price_node, ns["cbc"] + "BaseQuantity", unitCode=uom_unece_code
|
||||
)
|
||||
else:
|
||||
base_qty_node = etree.SubElement(price_node, ns["cbc"] + "BaseQuantity")
|
||||
base_qty_node.text = "%0.*f" % (qty_precision, base_qty)
|
||||
|
||||
def _ubl_add_tax_total(self, xml_root, ns, version="2.1"):
|
||||
self.ensure_one()
|
||||
cur_name = self.currency_id.name
|
||||
prec = self.currency_id.decimal_places
|
||||
|
||||
tax_lines = {}
|
||||
for tline in self.line_ids:
|
||||
sign = 1 if tline.is_refund else -1
|
||||
if tline.tax_line_id:
|
||||
# There are as many tax line as there are repartition lines
|
||||
tax_lines.setdefault(
|
||||
tline.tax_line_id,
|
||||
{"base": 0.0, "amount": 0.0},
|
||||
)
|
||||
tax_lines[tline.tax_line_id]["base"] += tline.tax_base_amount
|
||||
tax_lines[tline.tax_line_id]["amount"] += sign * tline.balance
|
||||
elif tline.tax_ids:
|
||||
# In case there are no repartition lines
|
||||
for tax in tline.tax_ids:
|
||||
if not tline.is_refund and tax.invoice_repartition_line_ids:
|
||||
continue
|
||||
if tline.is_refund and tax.refund_repartition_line_ids:
|
||||
continue
|
||||
tax_lines.setdefault(
|
||||
tax,
|
||||
{"base": 0.0, "amount": 0.0},
|
||||
)
|
||||
tax_lines[tax]["base"] += sign * tline.balance
|
||||
|
||||
exempt = 0.0
|
||||
exempt_taxes = self.line_ids.tax_line_id.browse()
|
||||
for tax, amounts in tax_lines.items():
|
||||
if tax.unece_type_id.code != "VAT":
|
||||
if tax.include_base_amount:
|
||||
continue
|
||||
exempt += amounts["amount"]
|
||||
exempt_taxes |= tax
|
||||
# For non-VAT taxes, not subject to VAT, declare as AllowanceCharge
|
||||
charge_node = etree.SubElement(xml_root, ns["cac"] + "AllowanceCharge")
|
||||
charge_indicator_node = etree.SubElement(
|
||||
charge_node, ns["cbc"] + "ChargeIndicator"
|
||||
)
|
||||
charge_indicator_node.text = "true"
|
||||
charge_reason_code_node = etree.SubElement(
|
||||
charge_node, ns["cbc"] + "AllowanceChargeReasonCode"
|
||||
)
|
||||
charge_reason_code_node.text = "ABK"
|
||||
charge_reason_node = etree.SubElement(
|
||||
charge_node, ns["cbc"] + "AllowanceChargeReason"
|
||||
)
|
||||
charge_reason_node.text = "Miscellaneous"
|
||||
charge_amount_node = etree.SubElement(
|
||||
charge_node, ns["cbc"] + "Amount", currencyID=cur_name
|
||||
)
|
||||
charge_amount_node.text = "%0.*f" % (prec, amounts["amount"])
|
||||
self._ubl_add_tax_category(tax, charge_node, ns, version=version)
|
||||
|
||||
tax_total_node = etree.SubElement(xml_root, ns["cac"] + "TaxTotal")
|
||||
tax_amount_node = etree.SubElement(
|
||||
tax_total_node, ns["cbc"] + "TaxAmount", currencyID=cur_name
|
||||
)
|
||||
tax_amount_node.text = "%0.*f" % (prec, self._ubl_get_invoice_vat_amount())
|
||||
for tax, amounts in tax_lines.items():
|
||||
if tax.unece_type_id.code == "VAT":
|
||||
self._ubl_add_tax_subtotal(
|
||||
amounts["base"],
|
||||
amounts["amount"],
|
||||
tax,
|
||||
cur_name,
|
||||
tax_total_node,
|
||||
ns,
|
||||
version=version,
|
||||
)
|
||||
|
||||
if not float_is_zero(exempt, precision_digits=prec):
|
||||
self._ubl_add_tax_subtotal(
|
||||
exempt,
|
||||
0,
|
||||
exempt_taxes[0],
|
||||
cur_name,
|
||||
tax_total_node,
|
||||
ns,
|
||||
version=version,
|
||||
)
|
||||
if len(exempt_taxes) > 1:
|
||||
# xpath cac:TaxCategory/cbc:Name
|
||||
exempt_node = tax_total_node[-1]
|
||||
exempt_node = [
|
||||
e for e in list(exempt_node) if e.tag == ns["cac"] + "TaxCategory"
|
||||
][0]
|
||||
exempt_node = [
|
||||
e for e in list(exempt_node) if e.tag == ns["cbc"] + "Name"
|
||||
][0]
|
||||
exempt_node.text = " + ".join([e.name for e in exempt_taxes])
|
||||
|
||||
def generate_invoice_ubl_xml_etree(self, version="2.1"):
|
||||
self.ensure_one()
|
||||
if self.move_type == "out_invoice":
|
||||
nsmap, ns = self._ubl_get_nsmap_namespace("Invoice-2", version=version)
|
||||
xml_root = etree.Element("Invoice", nsmap=nsmap)
|
||||
elif self.move_type == "out_refund":
|
||||
nsmap, ns = self._ubl_get_nsmap_namespace("CreditNote-2", version=version)
|
||||
xml_root = etree.Element("CreditNote", nsmap=nsmap)
|
||||
self._ubl_add_header(xml_root, ns, version=version)
|
||||
if version == "2.1":
|
||||
self._ubl_add_buyer_reference(xml_root, ns, version=version)
|
||||
self._ubl_add_order_reference(xml_root, ns, version=version)
|
||||
self._ubl_add_contract_document_reference(xml_root, ns, version=version)
|
||||
self._ubl_add_attachments(xml_root, ns, version=version)
|
||||
self._ubl_add_supplier_party(
|
||||
False,
|
||||
self.company_id,
|
||||
"AccountingSupplierParty",
|
||||
xml_root,
|
||||
ns,
|
||||
version=version,
|
||||
)
|
||||
self._ubl_add_customer_party(
|
||||
self.partner_id,
|
||||
False,
|
||||
"AccountingCustomerParty",
|
||||
xml_root,
|
||||
ns,
|
||||
version=version,
|
||||
)
|
||||
# the field 'partner_shipping_id' is defined in the 'sale' module
|
||||
if hasattr(self, "partner_shipping_id") and self.partner_shipping_id:
|
||||
self._ubl_add_delivery(self.partner_shipping_id, xml_root, ns)
|
||||
if self.move_type == "out_invoice":
|
||||
# Put paymentmeans block even when invoice is paid ?
|
||||
payment_identifier = self.get_payment_identifier()
|
||||
self._ubl_add_payment_means(
|
||||
self.partner_bank_id,
|
||||
self.payment_mode_id,
|
||||
self.invoice_date_due,
|
||||
xml_root,
|
||||
ns,
|
||||
payment_identifier=payment_identifier,
|
||||
version=version,
|
||||
)
|
||||
if self.invoice_payment_term_id:
|
||||
self._ubl_add_payment_terms(
|
||||
self.invoice_payment_term_id, xml_root, ns, version=version
|
||||
)
|
||||
self._ubl_add_tax_total(xml_root, ns, version=version)
|
||||
self._ubl_add_legal_monetary_total(xml_root, ns, version=version)
|
||||
|
||||
line_number = 0
|
||||
invoice_lines = self.invoice_line_ids.filtered(
|
||||
lambda line: line.display_type not in ("line_note", "line_section")
|
||||
)
|
||||
for iline in invoice_lines:
|
||||
line_number += 1
|
||||
self._ubl_add_invoice_line(
|
||||
xml_root, iline, line_number, ns, version=version
|
||||
)
|
||||
return xml_root
|
||||
|
||||
def generate_ubl_xml_string(self, version="2.1"):
|
||||
self.ensure_one()
|
||||
assert self.state == "posted"
|
||||
assert self.move_type in ("out_invoice", "out_refund")
|
||||
logger.debug("Starting to generate UBL XML Invoice file")
|
||||
lang = self.get_ubl_lang()
|
||||
# The aim of injecting lang in context
|
||||
# is to have the content of the XML in the partner's lang
|
||||
# but the problem is that the error messages will also be in
|
||||
# that lang. But the error messages should almost never
|
||||
# happen except the first days of use, so it's probably
|
||||
# not worth the additional code to handle the 2 langs
|
||||
xml_root = self.with_context(lang=lang).generate_invoice_ubl_xml_etree(
|
||||
version=version
|
||||
)
|
||||
xml_string = etree.tostring(
|
||||
xml_root, pretty_print=True, encoding="UTF-8", xml_declaration=True
|
||||
)
|
||||
if self.move_type == "out_invoice":
|
||||
self._ubl_check_xml_schema(xml_string, "Invoice", version=version)
|
||||
elif self.move_type == "out_refund":
|
||||
self._ubl_check_xml_schema(xml_string, "CreditNote", version=version)
|
||||
logger.debug(
|
||||
"Invoice UBL XML file generated for account invoice ID %d " "(state %s)",
|
||||
self.id,
|
||||
self.state,
|
||||
)
|
||||
logger.debug(xml_string.decode("utf-8"))
|
||||
return xml_string
|
||||
|
||||
def get_ubl_filename(self, version="2.1"):
|
||||
"""This method is designed to be inherited"""
|
||||
if self.move_type == "out_invoice":
|
||||
return "UBL-Invoice-%s.xml" % version
|
||||
elif self.move_type == "out_refund":
|
||||
return "UBL-CreditNote-%s.xml" % version
|
||||
|
||||
def get_ubl_version(self):
|
||||
return self.env.context.get("ubl_version") or "2.1"
|
||||
|
||||
def get_ubl_lang(self):
|
||||
self.ensure_one()
|
||||
return self.partner_id.lang or "en_US"
|
||||
|
||||
def _embed_ubl_xml_in_pdf(self, pdf_stream):
|
||||
self.ensure_one()
|
||||
if self.is_ubl_sale_invoice_posted():
|
||||
version = self.get_ubl_version()
|
||||
xml_filename = self.get_ubl_filename(version=version)
|
||||
xml_string = self.generate_ubl_xml_string(version=version)
|
||||
pdf_content = pdf_stream["stream"].getvalue()
|
||||
new_content = self.env["pdf.helper"].pdf_embed_xml(
|
||||
pdf_content,
|
||||
xml_filename,
|
||||
xml_string,
|
||||
)
|
||||
# Replace the current content.
|
||||
pdf_stream["stream"].close()
|
||||
pdf_stream["stream"] = io.BytesIO(new_content)
|
||||
|
||||
def attach_ubl_xml_file_button(self):
|
||||
self.ensure_one()
|
||||
assert self.move_type in ("out_invoice", "out_refund")
|
||||
assert self.state == "posted"
|
||||
version = self.get_ubl_version()
|
||||
xml_string = self.generate_ubl_xml_string(version=version)
|
||||
filename = self.get_ubl_filename(version=version)
|
||||
attach = (
|
||||
self.env["ir.attachment"]
|
||||
.with_context(**{})
|
||||
.create(
|
||||
{
|
||||
"name": filename,
|
||||
"res_id": self.id,
|
||||
"res_model": self._name,
|
||||
"datas": base64.b64encode(xml_string),
|
||||
# If default_type = 'out_invoice' in context, 'type'
|
||||
# would take 'out_invoice' value by default !
|
||||
"type": "binary",
|
||||
}
|
||||
)
|
||||
)
|
||||
action = self.env["ir.attachment"].action_get()
|
||||
action.update({"res_id": attach.id, "views": False, "view_mode": "form,tree"})
|
||||
return action
|
||||
|
||||
def is_ubl_sale_invoice_posted(self):
|
||||
self.ensure_one()
|
||||
is_ubl = self.company_id.xml_format_in_pdf_invoice == "ubl"
|
||||
if is_ubl and self.is_sale_document() and self.state == "posted":
|
||||
return True
|
||||
return False
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# Copyright 2016-2017 Akretion (http://www.akretion.com)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# Copyright 2019 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = "ir.actions.report"
|
||||
|
||||
def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None):
|
||||
# It works, but:
|
||||
# - when you click on the "Print" button or use the "Print" menu,
|
||||
# the XML file is regenerated even when the invoice is read from the attachment.
|
||||
# - when you open the invoice from the attachment, you get the "original" XML
|
||||
# file
|
||||
collected_streams = super()._render_qweb_pdf_prepare_streams(
|
||||
report_ref, data, res_ids=res_ids
|
||||
)
|
||||
amo = self.env["account.move"]
|
||||
invoice_reports = amo._get_invoice_report_names()
|
||||
report_name = self._get_report(report_ref).report_name
|
||||
if (
|
||||
collected_streams
|
||||
and res_ids
|
||||
and len(res_ids) == 1
|
||||
and report_name in invoice_reports
|
||||
and not self.env.context.get("no_embedded_ubl_xml")
|
||||
):
|
||||
move = amo.browse(res_ids)
|
||||
if move._xml_format_in_pdf_invoice() == "ubl":
|
||||
pdf_stream = collected_streams[move.id]
|
||||
move._embed_ubl_xml_in_pdf(pdf_stream)
|
||||
return collected_streams
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright 2016-2018 Akretion (http://www.akretion.com)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
xml_format_in_pdf_invoice = fields.Selection(
|
||||
selection_add=[("ubl", "Universal Business Language (UBL)")], default="ubl"
|
||||
)
|
||||
embed_pdf_in_ubl_xml_invoice = fields.Boolean(
|
||||
string="Embed PDF in UBL XML Invoice",
|
||||
help="If active, the standalone UBL Invoice XML file will include the "
|
||||
"PDF of the invoice in base64 under the node "
|
||||
"'AdditionalDocumentReference'. For example, to be compliant with the "
|
||||
"e-fff standard used in Belgium, you should activate this option.",
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
embed_pdf_in_ubl_xml_invoice = fields.Boolean(
|
||||
related="company_id.embed_pdf_in_ubl_xml_invoice",
|
||||
readonly=False,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue