diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/account_edi_facturx/models/account_edi_format.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.py | 327 |
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() |
