summaryrefslogtreecommitdiff
path: root/addons/account_edi_facturx/models/account_edi_format.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/account_edi_facturx/models/account_edi_format.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/account_edi_facturx/models/account_edi_format.py')
-rw-r--r--addons/account_edi_facturx/models/account_edi_format.py327
1 files changed, 327 insertions, 0 deletions
diff --git a/addons/account_edi_facturx/models/account_edi_format.py b/addons/account_edi_facturx/models/account_edi_format.py
new file mode 100644
index 00000000..422606f0
--- /dev/null
+++ b/addons/account_edi_facturx/models/account_edi_format.py
@@ -0,0 +1,327 @@
+# -*- coding: utf-8 -*-
+
+from odoo import api, models, fields, tools, _
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, float_repr
+from odoo.tests.common import Form
+from odoo.exceptions import UserError
+
+from datetime import datetime
+from lxml import etree
+from PyPDF2 import PdfFileReader
+import base64
+
+import io
+
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+DEFAULT_FACTURX_DATE_FORMAT = '%Y%m%d'
+
+
+class AccountEdiFormat(models.Model):
+ _inherit = 'account.edi.format'
+
+ def _is_compatible_with_journal(self, journal):
+ self.ensure_one()
+ res = super()._is_compatible_with_journal(journal)
+ if self.code != 'facturx_1_0_05':
+ return res
+ return journal.type == 'sale'
+
+ def _post_invoice_edi(self, invoices, test_mode=False):
+ self.ensure_one()
+ if self.code != 'facturx_1_0_05':
+ return super()._post_invoice_edi(invoices, test_mode=test_mode)
+ res = {}
+ for invoice in invoices:
+ attachment = self._export_facturx(invoice)
+ res[invoice] = {'attachment': attachment}
+ return res
+
+ def _is_embedding_to_invoice_pdf_needed(self):
+ # OVERRIDE
+ self.ensure_one()
+ return True if self.code == 'facturx_1_0_05' else super()._is_embedding_to_invoice_pdf_needed()
+
+ def _get_embedding_to_invoice_pdf_values(self, invoice):
+ values = super()._get_embedding_to_invoice_pdf_values(invoice)
+ if values and self.code == 'facturx_1_0_05':
+ values['name'] = 'factur-x.xml'
+ return values
+
+ def _export_facturx(self, invoice):
+
+ def format_date(dt):
+ # Format the date in the Factur-x standard.
+ dt = dt or datetime.now()
+ return dt.strftime(DEFAULT_FACTURX_DATE_FORMAT)
+
+ def format_monetary(number, currency):
+ # Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001).
+ return float_repr(number, currency.decimal_places)
+
+ self.ensure_one()
+ # Create file content.
+ template_values = {
+ 'record': invoice,
+ 'format_date': format_date,
+ 'format_monetary': format_monetary,
+ 'invoice_line_values': [],
+ }
+
+ # Tax lines.
+ aggregated_taxes_details = {line.tax_line_id.id: {
+ 'line': line,
+ 'tax_amount': -line.amount_currency if line.currency_id else -line.balance,
+ 'tax_base_amount': 0.0,
+ } for line in invoice.line_ids.filtered('tax_line_id')}
+
+ # Invoice lines.
+ for i, line in enumerate(invoice.invoice_line_ids.filtered(lambda l: not l.display_type)):
+ price_unit_with_discount = line.price_unit * (1 - (line.discount / 100.0))
+ taxes_res = line.tax_ids.with_context(force_sign=line.move_id._get_tax_force_sign()).compute_all(
+ price_unit_with_discount,
+ currency=line.currency_id,
+ quantity=line.quantity,
+ product=line.product_id,
+ partner=invoice.partner_id,
+ is_refund=line.move_id.move_type in ('in_refund', 'out_refund'),
+ )
+
+ line_template_values = {
+ 'line': line,
+ 'index': i + 1,
+ 'tax_details': [],
+ 'net_price_subtotal': taxes_res['total_excluded'],
+ }
+
+ for tax_res in taxes_res['taxes']:
+ tax = self.env['account.tax'].browse(tax_res['id'])
+ line_template_values['tax_details'].append({
+ 'tax': tax,
+ 'tax_amount': tax_res['amount'],
+ 'tax_base_amount': tax_res['base'],
+ })
+
+ if tax.id in aggregated_taxes_details:
+ aggregated_taxes_details[tax.id]['tax_base_amount'] += tax_res['base']
+
+ template_values['invoice_line_values'].append(line_template_values)
+
+ template_values['tax_details'] = list(aggregated_taxes_details.values())
+
+ xml_content = b"<?xml version='1.0' encoding='UTF-8'?>"
+ xml_content += self.env.ref('account_edi_facturx.account_invoice_facturx_export')._render(template_values)
+ xml_name = '%s_facturx.xml' % (invoice.name.replace('/', '_'))
+ return self.env['ir.attachment'].create({
+ 'name': xml_name,
+ 'datas': base64.encodebytes(xml_content),
+ 'mimetype': 'application/xml'
+ })
+
+ def _is_facturx(self, filename, tree):
+ return self.code == 'facturx_1_0_05' and tree.tag == '{urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100}CrossIndustryInvoice'
+
+ def _create_invoice_from_xml_tree(self, filename, tree):
+ self.ensure_one()
+ if self._is_facturx(filename, tree):
+ return self._import_facturx(tree, self.env['account.move'])
+ return super()._create_invoice_from_xml_tree(filename, tree)
+
+ def _update_invoice_from_xml_tree(self, filename, tree, invoice):
+ self.ensure_one()
+ if self._is_facturx(filename, tree):
+ return self._import_facturx(tree, invoice)
+ return super()._update_invoice_from_xml_tree(filename, tree, invoice)
+
+ def _import_facturx(self, tree, invoice):
+ """ Decodes a factur-x invoice into an invoice.
+
+ :param tree: the factur-x tree to decode.
+ :param invoice: the invoice to update or an empty recordset.
+ :returns: the invoice where the factur-x data was imported.
+ """
+
+ amount_total_import = None
+
+ default_move_type = False
+ if invoice._context.get('default_journal_id'):
+ journal = self.env['account.journal'].browse(self.env.context['default_journal_id'])
+ default_move_type = 'out_invoice' if journal.type == 'sale' else 'in_invoice'
+ elif invoice._context.get('default_move_type'):
+ default_move_type = self._context['default_move_type']
+ elif invoice.move_type in self.env['account.move'].get_invoice_types(include_receipts=True):
+ # in case an attachment is saved on a draft invoice previously created, we might
+ # have lost the default value in context but the type was already set
+ default_move_type = invoice.move_type
+
+ if not default_move_type:
+ raise UserError(_("No information about the journal or the type of invoice is passed"))
+ if default_move_type == 'entry':
+ return
+
+ # Total amount.
+ elements = tree.xpath('//ram:GrandTotalAmount', namespaces=tree.nsmap)
+ total_amount = elements and float(elements[0].text) or 0.0
+
+ # Refund type.
+ # There is two modes to handle refund in Factur-X:
+ # a) type_code == 380 for invoice, type_code == 381 for refund, all positive amounts.
+ # b) type_code == 380, negative amounts in case of refund.
+ # To handle both, we consider the 'a' mode and switch to 'b' if a negative amount is encountered.
+ elements = tree.xpath('//rsm:ExchangedDocument/ram:TypeCode', namespaces=tree.nsmap)
+ type_code = elements[0].text
+
+ default_move_type.replace('_refund', '_invoice')
+ if type_code == '381':
+ default_move_type = 'out_refund' if default_move_type == 'out_invoice' else 'in_refund'
+ refund_sign = -1
+ else:
+ # Handle 'b' refund mode.
+ if total_amount < 0:
+ default_move_type = 'out_refund' if default_move_type == 'out_invoice' else 'in_refund'
+ refund_sign = -1 if 'refund' in default_move_type else 1
+
+ # Write the type as the journal entry is already created.
+ invoice.move_type = default_move_type
+
+ # self could be a single record (editing) or be empty (new).
+ with Form(invoice.with_context(default_move_type=default_move_type,
+ account_predictive_bills_disable_prediction=True)) as invoice_form:
+ # Partner (first step to avoid warning 'Warning! You must first select a partner.').
+ partner_type = invoice_form.journal_id.type == 'purchase' and 'SellerTradeParty' or 'BuyerTradeParty'
+ invoice_form.partner_id = self._retrieve_partner(
+ name=self._find_value('//ram:' + partner_type + '/ram:Name', tree, namespaces=tree.nsmap),
+ mail=self._find_value('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']', tree, namespaces=tree.nsmap),
+ vat=self._find_value('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', tree, namespaces=tree.nsmap),
+ )
+
+ # Delivery partner
+ if 'partner_shipping_id' in invoice._fields:
+ invoice_form.partner_shipping_id = self._retrieve_partner(
+ name=self._find_value('//ram:ShipToTradeParty/ram:Name', tree, namespaces=tree.nsmap),
+ mail=self._find_value('//ram:ShipToTradeParty//ram:URIID[@schemeID=\'SMTP\']', tree, namespaces=tree.nsmap),
+ vat=self._find_value('//ram:ShipToTradeParty/ram:SpecifiedTaxRegistration/ram:ID', tree, namespaces=tree.nsmap),
+ )
+
+ # Reference.
+ elements = tree.xpath('//rsm:ExchangedDocument/ram:ID', namespaces=tree.nsmap)
+ if elements:
+ invoice_form.ref = elements[0].text
+
+ # Name.
+ elements = tree.xpath('//ram:BuyerOrderReferencedDocument/ram:IssuerAssignedID', namespaces=tree.nsmap)
+ if elements:
+ invoice_form.payment_reference = elements[0].text
+
+ # Comment.
+ elements = tree.xpath('//ram:IncludedNote/ram:Content', namespaces=tree.nsmap)
+ if elements:
+ invoice_form.narration = elements[0].text
+
+ # Total amount.
+ elements = tree.xpath('//ram:GrandTotalAmount', namespaces=tree.nsmap)
+ if elements:
+
+ # Currency.
+ if elements[0].attrib.get('currencyID'):
+ currency_str = elements[0].attrib['currencyID']
+ currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False)
+ if currency != self.env.company.currency_id and currency.active:
+ invoice_form.currency_id = currency
+
+ # Store xml total amount.
+ amount_total_import = total_amount * refund_sign
+
+ # Date.
+ elements = tree.xpath('//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString', namespaces=tree.nsmap)
+ if elements:
+ date_str = elements[0].text
+ date_obj = datetime.strptime(date_str, DEFAULT_FACTURX_DATE_FORMAT)
+ invoice_form.invoice_date = date_obj.strftime(DEFAULT_SERVER_DATE_FORMAT)
+
+ # Due date.
+ elements = tree.xpath('//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString', namespaces=tree.nsmap)
+ if elements:
+ date_str = elements[0].text
+ date_obj = datetime.strptime(date_str, DEFAULT_FACTURX_DATE_FORMAT)
+ invoice_form.invoice_date_due = date_obj.strftime(DEFAULT_SERVER_DATE_FORMAT)
+
+ # Invoice lines.
+ elements = tree.xpath('//ram:IncludedSupplyChainTradeLineItem', namespaces=tree.nsmap)
+ if elements:
+ for element in elements:
+ with invoice_form.invoice_line_ids.new() as invoice_line_form:
+
+ # Sequence.
+ line_elements = element.xpath('.//ram:AssociatedDocumentLineDocument/ram:LineID', namespaces=tree.nsmap)
+ if line_elements:
+ invoice_line_form.sequence = int(line_elements[0].text)
+
+ # Product.
+ line_elements = element.xpath('.//ram:SpecifiedTradeProduct/ram:Name', namespaces=tree.nsmap)
+ if line_elements:
+ invoice_line_form.name = line_elements[0].text
+ line_elements = element.xpath('.//ram:SpecifiedTradeProduct/ram:SellerAssignedID', namespaces=tree.nsmap)
+ if line_elements and line_elements[0].text:
+ product = self.env['product.product'].search([('default_code', '=', line_elements[0].text)])
+ if product:
+ invoice_line_form.product_id = product
+ if not invoice_line_form.product_id:
+ line_elements = element.xpath('.//ram:SpecifiedTradeProduct/ram:GlobalID', namespaces=tree.nsmap)
+ if line_elements and line_elements[0].text:
+ product = self.env['product.product'].search([('barcode', '=', line_elements[0].text)])
+ if product:
+ invoice_line_form.product_id = product
+
+ # Quantity.
+ line_elements = element.xpath('.//ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', namespaces=tree.nsmap)
+ if line_elements:
+ invoice_line_form.quantity = float(line_elements[0].text)
+
+ # Price Unit.
+ line_elements = element.xpath('.//ram:GrossPriceProductTradePrice/ram:ChargeAmount', namespaces=tree.nsmap)
+ if line_elements:
+ quantity_elements = element.xpath('.//ram:GrossPriceProductTradePrice/ram:BasisQuantity', namespaces=tree.nsmap)
+ if quantity_elements:
+ invoice_line_form.price_unit = float(line_elements[0].text) / float(quantity_elements[0].text)
+ else:
+ invoice_line_form.price_unit = float(line_elements[0].text)
+ else:
+ line_elements = element.xpath('.//ram:NetPriceProductTradePrice/ram:ChargeAmount', namespaces=tree.nsmap)
+ if line_elements:
+ quantity_elements = element.xpath('.//ram:NetPriceProductTradePrice/ram:BasisQuantity', namespaces=tree.nsmap)
+ if quantity_elements:
+ invoice_line_form.price_unit = float(line_elements[0].text) / float(quantity_elements[0].text)
+ else:
+ invoice_line_form.price_unit = float(line_elements[0].text)
+ # Discount.
+ line_elements = element.xpath('.//ram:AppliedTradeAllowanceCharge/ram:CalculationPercent', namespaces=tree.nsmap)
+ if line_elements:
+ invoice_line_form.discount = float(line_elements[0].text)
+
+ # Taxes
+ line_elements = element.xpath('.//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', namespaces=tree.nsmap)
+ invoice_line_form.tax_ids.clear()
+ for tax_element in line_elements:
+ percentage = float(tax_element.text)
+
+ tax = self.env['account.tax'].search([
+ ('company_id', '=', invoice_form.company_id.id),
+ ('amount_type', '=', 'percent'),
+ ('type_tax_use', '=', invoice_form.journal_id.type),
+ ('amount', '=', percentage),
+ ], limit=1)
+
+ if tax:
+ invoice_line_form.tax_ids.add(tax)
+ elif amount_total_import:
+ # No lines in BASICWL.
+ with invoice_form.invoice_line_ids.new() as invoice_line_form:
+ invoice_line_form.name = invoice_form.comment or '/'
+ invoice_line_form.quantity = 1
+ invoice_line_form.price_unit = amount_total_import
+
+ return invoice_form.save()