From 0226901ec74368532b0518034845849a9b54df88 Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Tue, 26 May 2020 07:45:33 +0000 Subject: [PATCH 1/5] [IMP] base, tools: manage multiple attachments on PDF files --- odoo/tools/pdf.py | 92 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/odoo/tools/pdf.py b/odoo/tools/pdf.py index 61105ed1c90..9003ad64789 100644 --- a/odoo/tools/pdf.py +++ b/odoo/tools/pdf.py @@ -1,25 +1,37 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import io -import PyPDF2 +from PyPDF2 import PdfFileWriter, PdfFileReader +from PyPDF2.generic import DictionaryObject, DecodedStreamObject, NameObject, createStringObject, ArrayObject +from PyPDF2.utils import b_ +from datetime import datetime -class BrandedFileWriter(PyPDF2.PdfFileWriter): +import io +import hashlib + + +DEFAULT_PDF_DATETIME_FORMAT = "D:%Y%m%d%H%M%S+00'00'" + + +class BrandedFileWriter(PdfFileWriter): def __init__(self): super().__init__() self.addMetadata({ '/Creator': "Odoo", '/Producer': "Odoo", }) -PyPDF2.PdfFileWriter = BrandedFileWriter + + +PdfFileWriter = BrandedFileWriter + def merge_pdf(pdf_data): ''' Merge a collection of PDF documents in one :param list pdf_data: a list of PDF datastrings :return: a unique merged PDF datastring ''' - writer = PyPDF2.PdfFileWriter() + writer = PdfFileWriter() for document in pdf_data: - reader = PyPDF2.PdfFileReader(io.BytesIO(document), strict=False) + reader = PdfFileReader(io.BytesIO(document), strict=False) for page in range(0, reader.getNumPages()): writer.addPage(reader.getPage(page)) with io.BytesIO() as _buffer: @@ -32,8 +44,8 @@ def rotate_pdf(pdf): :param pdf: a PDF to rotate :return: a PDF rotated ''' - writer = PyPDF2.PdfFileWriter() - reader = PyPDF2.PdfFileReader(io.BytesIO(pdf), strict=False) + writer = PdfFileWriter() + reader = PdfFileReader(io.BytesIO(pdf), strict=False) for page in range(0, reader.getNumPages()): page = reader.getPage(page) page.rotateClockwise(90) @@ -41,3 +53,67 @@ def rotate_pdf(pdf): with io.BytesIO() as _buffer: writer.write(_buffer) return _buffer.getvalue() + + +class OdooPdfFileReader(PdfFileReader): + # OVERRIDE of PdfFileReader to add the management of multiple embedded files. + + def getAttachments(self): + if "/Names" not in self.trailer["/Root"]: + return [] + for i in range(0, len(self.trailer["/Root"]["/Names"]["/EmbeddedFiles"]["/Names"]), 2): + attachment = self.trailer["/Root"]["/Names"]["/EmbeddedFiles"]["/Names"][i+1].getObject() + yield (attachment["/F"], attachment["/EF"]["/F"].getObject().getData()) + + +class OdooPdfFileWriter(PdfFileWriter): + # OVERRIDE of PdfFileWriter to add the management of multiple embedded files. + + def _create_attachment_object(self, attachment): + ''' Create a PyPdf2.generic object representing an embedded file. + + :param attachment: A dictionary containing: + * filename: The name of the file to embed (require). + * content: The content of the file encoded in base64 (require). + :return: + ''' + file_entry = DecodedStreamObject() + file_entry.setData(attachment['content']) + file_entry.update({ + NameObject("/Type"): NameObject("/EmbeddedFile"), + NameObject("/Params"): + DictionaryObject({ + NameObject('/CheckSum'): createStringObject(hashlib.md5(attachment['content']).hexdigest()), + NameObject('/ModDate'): createStringObject(datetime.now().strftime(DEFAULT_PDF_DATETIME_FORMAT)), + NameObject('/Size'): NameObject(str(len(attachment['content']))), + }), + }) + if attachment.get('subtype'): + file_entry.update({ + NameObject("/Subtype"): NameObject(attachment['subtype']), + }) + file_entry_object = self._addObject(file_entry) + filename_object = createStringObject(attachment['filename']) + filespec_object = DictionaryObject({ + NameObject("/AFRelationship"): NameObject("/Data"), + NameObject("/Type"): NameObject("/Filespec"), + NameObject("/F"): filename_object, + NameObject("/EF"): + DictionaryObject({ + NameObject("/F"): file_entry_object, + NameObject('/UF'): file_entry_object, + }), + NameObject("/UF"): filename_object, + }) + if attachment.get('description'): + filespec_object.update({NameObject("/Desc"): createStringObject(attachment['description'])}) + return self._addObject(filespec_object) + + def addAttachment(self, fname, fdata): + # OVERRIDE of the AddAttachment method to allow appending attachemnts when some already exist + if self._root_object.get('/Names') and self._root_object['/Names'].get('/EmbeddedFiles'): + attachments = self._root_object["/Names"]["/EmbeddedFiles"]["/Names"] + new_attachment = self._create_attachment_object({'filename': fname, 'content': fdata}) + attachments.extend([new_attachment.getObject()['/F'], new_attachment]) + else: + super().addAttachment(fname, fdata) From 913f22f73f208b0fa8ca947cf410e32c65adfc93 Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Tue, 26 May 2020 07:46:18 +0000 Subject: [PATCH 2/5] [ADD] account_edi: generic module to manage EDI (factur-x) - account_edi is the base module to manage EDIs. It manages the generic worflow of import/export and defines the methods for import/export that should be overridden by each types of EDI (in different modules). - On export, the files are added as attachment and can be embeded into pdf (format specific). - On export, the exported format are set on the journal to allow different settings based on journal (different companies, countries, etc). - On import, depending on the import method (from message_post or upload button in invoice tree view) a new invoice is created or an existing one is updated (only allowed draft invoices with no invoice lines). - Adapted factur-x --- addons/account/models/account_journal.py | 14 +- addons/account/wizard/account_invoice_send.py | 3 +- .../__init__.py | 1 + addons/account_edi/__manifest__.py | 24 ++ addons/account_edi/models/__init__.py | 8 + .../account_edi/models/account_edi_format.py | 300 ++++++++++++++++++ addons/account_edi/models/account_journal.py | 21 ++ addons/account_edi/models/account_move.py | 53 ++++ .../account_edi/models/ir_actions_report.py | 16 + addons/account_edi/models/ir_attachment.py | 10 + addons/account_edi/models/mail_template.py | 32 ++ .../account_edi/security/ir.model.access.csv | 2 + .../views/account_journal_views.xml | 17 + addons/account_edi/wizard/__init__.py | 1 + .../wizard/account_invoice_send.py | 13 + .../wizard/account_invoice_send_views.xml | 29 ++ addons/account_edi_facturx/__init__.py | 1 + .../__manifest__.py | 5 +- .../data/account_edi_data.xml | 11 + .../data/facturx_templates.xml | 8 +- addons/account_edi_facturx/models/__init__.py | 1 + .../models/account_edi_format.py} | 184 ++++------- .../account_facturx/i18n/account_facturx.pot | 37 --- addons/account_facturx/i18n/ar.po | 55 ---- addons/account_facturx/i18n/cs.po | 55 ---- addons/account_facturx/i18n/da.po | 55 ---- addons/account_facturx/i18n/de.po | 60 ---- addons/account_facturx/i18n/es.po | 59 ---- addons/account_facturx/i18n/fr.po | 59 ---- addons/account_facturx/i18n/he.po | 55 ---- addons/account_facturx/i18n/hr.po | 55 ---- addons/account_facturx/i18n/hu.po | 55 ---- addons/account_facturx/i18n/it.po | 55 ---- addons/account_facturx/i18n/lb.po | 51 --- addons/account_facturx/i18n/lt.po | 55 ---- addons/account_facturx/i18n/mn.po | 55 ---- addons/account_facturx/i18n/nb.po | 55 ---- addons/account_facturx/i18n/nl.po | 59 ---- addons/account_facturx/i18n/pl.po | 55 ---- addons/account_facturx/i18n/pt.po | 51 --- addons/account_facturx/i18n/pt_BR.po | 55 ---- addons/account_facturx/i18n/ro.po | 51 --- addons/account_facturx/i18n/ru.po | 55 ---- addons/account_facturx/i18n/tr.po | 55 ---- addons/account_facturx/i18n/uk.po | 58 ---- addons/account_facturx/i18n/vi.po | 56 ---- addons/account_facturx/i18n/zh_CN.po | 56 ---- addons/account_facturx/i18n/zh_TW.po | 56 ---- addons/account_facturx/models/__init__.py | 4 - .../models/ir_actions_report.py | 32 -- 50 files changed, 616 insertions(+), 1597 deletions(-) rename addons/{account_facturx => account_edi}/__init__.py (69%) create mode 100644 addons/account_edi/__manifest__.py create mode 100644 addons/account_edi/models/__init__.py create mode 100644 addons/account_edi/models/account_edi_format.py create mode 100644 addons/account_edi/models/account_journal.py create mode 100644 addons/account_edi/models/account_move.py create mode 100644 addons/account_edi/models/ir_actions_report.py create mode 100644 addons/account_edi/models/ir_attachment.py create mode 100644 addons/account_edi/models/mail_template.py create mode 100644 addons/account_edi/security/ir.model.access.csv create mode 100644 addons/account_edi/views/account_journal_views.xml create mode 100644 addons/account_edi/wizard/__init__.py create mode 100644 addons/account_edi/wizard/account_invoice_send.py create mode 100644 addons/account_edi/wizard/account_invoice_send_views.xml create mode 100644 addons/account_edi_facturx/__init__.py rename addons/{account_facturx => account_edi_facturx}/__manifest__.py (64%) create mode 100644 addons/account_edi_facturx/data/account_edi_data.xml rename addons/{account_facturx => account_edi_facturx}/data/facturx_templates.xml (97%) create mode 100644 addons/account_edi_facturx/models/__init__.py rename addons/{account_facturx/models/account_move.py => account_edi_facturx/models/account_edi_format.py} (60%) delete mode 100644 addons/account_facturx/i18n/account_facturx.pot delete mode 100644 addons/account_facturx/i18n/ar.po delete mode 100644 addons/account_facturx/i18n/cs.po delete mode 100644 addons/account_facturx/i18n/da.po delete mode 100644 addons/account_facturx/i18n/de.po delete mode 100644 addons/account_facturx/i18n/es.po delete mode 100644 addons/account_facturx/i18n/fr.po delete mode 100644 addons/account_facturx/i18n/he.po delete mode 100644 addons/account_facturx/i18n/hr.po delete mode 100644 addons/account_facturx/i18n/hu.po delete mode 100644 addons/account_facturx/i18n/it.po delete mode 100644 addons/account_facturx/i18n/lb.po delete mode 100644 addons/account_facturx/i18n/lt.po delete mode 100644 addons/account_facturx/i18n/mn.po delete mode 100644 addons/account_facturx/i18n/nb.po delete mode 100644 addons/account_facturx/i18n/nl.po delete mode 100644 addons/account_facturx/i18n/pl.po delete mode 100644 addons/account_facturx/i18n/pt.po delete mode 100644 addons/account_facturx/i18n/pt_BR.po delete mode 100644 addons/account_facturx/i18n/ro.po delete mode 100644 addons/account_facturx/i18n/ru.po delete mode 100644 addons/account_facturx/i18n/tr.po delete mode 100644 addons/account_facturx/i18n/uk.po delete mode 100644 addons/account_facturx/i18n/vi.po delete mode 100644 addons/account_facturx/i18n/zh_CN.po delete mode 100644 addons/account_facturx/i18n/zh_TW.po delete mode 100644 addons/account_facturx/models/__init__.py delete mode 100644 addons/account_facturx/models/ir_actions_report.py diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index 354e43bb0d7..db72d23f5f0 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -546,10 +546,8 @@ class AccountJournal(models.Model): invoices = self.env['account.move'] for attachment in attachments: - invoice = self.env['account.move'].create({}) attachment.write({'res_model': 'mail.compose.message'}) - invoice.message_post(attachment_ids=[attachment.id]) - invoices += invoice + invoices += self._create_invoice_from_single_attachment(attachment) action_vals = { 'name': _('Generated Documents'), @@ -565,6 +563,16 @@ class AccountJournal(models.Model): action_vals['view_mode'] = 'tree,form' return action_vals + def _create_invoice_from_single_attachment(self, attachment): + """ Creates an invoice and post the attachment. If the related modules + are installed, it will trigger OCR or the import from the EDI. + + :returns: the created invoice. + """ + invoice = self.env['account.move'].create({}) + invoice.message_post(attachment_ids=[attachment.id]) + return invoice + def _create_secure_sequence(self, sequence_fields): """This function creates a no_gap sequence on each journal in self that will ensure a unique number is given to all posted account.move in such a way that we can always diff --git a/addons/account/wizard/account_invoice_send.py b/addons/account/wizard/account_invoice_send.py index 90861b0c91e..56faf87fdd4 100644 --- a/addons/account/wizard/account_invoice_send.py +++ b/addons/account/wizard/account_invoice_send.py @@ -84,7 +84,8 @@ class AccountInvoiceSend(models.TransientModel): def _send_email(self): if self.is_email: - self.composer_id.send_mail() + # with_context : we don't want to reimport the file we just exported. + self.composer_id.with_context(no_new_invoice=True).send_mail() if self.env.context.get('mark_invoice_as_sent'): #Salesman send posted invoice, without the right to write #but they should have the right to change this flag diff --git a/addons/account_facturx/__init__.py b/addons/account_edi/__init__.py similarity index 69% rename from addons/account_facturx/__init__.py rename to addons/account_edi/__init__.py index 21ff7c22f3c..7749576f92c 100644 --- a/addons/account_facturx/__init__.py +++ b/addons/account_edi/__init__.py @@ -1,3 +1,4 @@ # -*- encoding: utf-8 -*- from . import models +from . import wizard diff --git a/addons/account_edi/__manifest__.py b/addons/account_edi/__manifest__.py new file mode 100644 index 00000000000..5234e79dc12 --- /dev/null +++ b/addons/account_edi/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +{ + 'name' : 'Import/Export Invoices From XML/PDF', + 'description':""" +Electronic Data Interchange +======================================= +EDI is the electronic interchange of business information using a standardized format. + +This is the base module for import and export of invoices in various EDI formats, and the +the transmission of said documents to various parties involved in the exchange (other company, +governements, etc.) + """, + 'version' : '1.0', + 'category': 'Accounting/Accounting', + 'depends' : ['account'], + 'data': [ + 'security/ir.model.access.csv', + 'wizard/account_invoice_send_views.xml', + 'views/account_journal_views.xml' + ], + 'installable': True, + 'application': False, + 'auto_install': True, +} diff --git a/addons/account_edi/models/__init__.py b/addons/account_edi/models/__init__.py new file mode 100644 index 00000000000..dea47dee7b9 --- /dev/null +++ b/addons/account_edi/models/__init__.py @@ -0,0 +1,8 @@ +# -*- encoding: utf-8 -*- + +from . import account_move +from . import account_journal +from . import account_edi_format +from . import ir_actions_report +from . import ir_attachment +from . import mail_template diff --git a/addons/account_edi/models/account_edi_format.py b/addons/account_edi/models/account_edi_format.py new file mode 100644 index 00000000000..c36f22e16a3 --- /dev/null +++ b/addons/account_edi/models/account_edi_format.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api +from odoo.exceptions import UserError +from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter + +from lxml import etree +import base64 +import io +import logging + +_logger = logging.getLogger(__name__) + + +class AccountEdiFormat(models.Model): + _name = 'account.edi.format' + _description = 'EDI format' + + name = fields.Char() + code = fields.Char() + hide_on_journal = fields.Selection([('import_export', 'Import/Export'), ('import', 'Import Only')], default='import_export', help='used to hide this EDI format on journals') + + _sql_constraints = [ + ('unique_code', 'unique (code)', 'This code already exists') + ] + + #################################################### + # Export method to override based on EDI Format + #################################################### + + def _export_invoice_to_attachment(self, invoice): + """ Create the file content representing the invoice. + + :param invoice: the invoice to encode. + :returns: a dictionary (values are compatible to create an ir.attachment) + * name : the name of the file + * datas : the content of the file, + * res_model : 'account.move', + * res_id: the id of invoice + * mimetype : the mimetype of the attachment + """ + # TO OVERRIDE + self.ensure_one() + return False + + def _export_invoice_to_embed_to_pdf(self, pdf_content, invoice): + """ Create the file content representing the invoice when it's destined + to be embed into a pdf. + - default: creates the default EDI document (_export_invoice_to_attachment). + - Should return False if this EDI format should not be embedded. + - Should be overriden only if a specific behavior (for example, + include the pdf content inside the file). + + :param pdf_content: the pdf before any EDI format was added. + :param invoice: the invoice to add. + :returns: a dictionary or False if this EDI format must not be embedded to pdf. + * name : the name of the file + * datas : the content of the file, + * res_model : 'account.move', + * res_id: the id of invoice + * mimetype : the mimetype of the attachment + """ + # TO OVERRIDE + self.ensure_one() + return self._export_invoice_to_attachment(invoice) + + #################################################### + # Import methods to override based on EDI Format + #################################################### + + def _create_invoice_from_xml_tree(self, filename, tree): + """ Create a new invoice with the data inside the xml. + + :param filename: The name of the xml. + :param tree: The tree of the xml to import. + :returns: The created invoice. + """ + # TO OVERRIDE + self.ensure_one() + return self.env['account.move'] + + def _update_invoice_from_xml_tree(self, filename, tree, invoice): + """ Update an existing invoice with the data inside the xml. + + :param filename: The name of the xml. + :param tree: The tree of the xml to import. + :param invoice: The invoice to update. + :returns: The updated invoice. + """ + # TO OVERRIDE + self.ensure_one() + return self.env['account.move'] + + def _create_invoice_from_pdf_reader(self, filename, reader): + """ Create a new invoice with the data inside a pdf. + + :param filename: The name of the pdf. + :param reader: The OdooPdfFileReader of the pdf to import. + :returns: The created invoice. + """ + # TO OVERRIDE + self.ensure_one() + + return self.env['account.move'] + + def _update_invoice_from_pdf_reader(self, filename, reader, invoice): + """ Update an existing invoice with the data inside the pdf. + + :param filename: The name of the pdf. + :param reader: The OdooPdfFileReader of the pdf to import. + :param invoice: The invoice to update. + :returns: The updated invoice. + """ + # TO OVERRIDE + self.ensure_one() + return self.env['account.move'] + + #################################################### + # Export Internal methods (not meant to be overridden) + #################################################### + + def _embed_edis_to_pdf(self, pdf_content, invoice): + """ Create the EDI document of the invoice and embed it in the pdf_content. + + :param pdf_content: the bytes representing the pdf to add the EDIs to. + :param invoice: the invoice to generate the EDI from. + :returns: the same pdf_content with the EDI of the invoice embed in it. + """ + attachments = [] + for edi_format in self: + try: + vals = edi_format._export_invoice_to_embed_to_pdf(pdf_content, invoice) + except: + continue + if vals: + attachments.append(vals) + + if attachments: + # Add the attachments to the pdf file + reader_buffer = io.BytesIO(pdf_content) + reader = OdooPdfFileReader(reader_buffer) + writer = OdooPdfFileWriter() + writer.cloneReaderDocumentRoot(reader) + for vals in attachments: + writer.addAttachment(vals['name'], vals['datas']) + buffer = io.BytesIO() + writer.write(buffer) + pdf_content = buffer.getvalue() + reader_buffer.close() + buffer.close() + return pdf_content + + def _create_ir_attachments(self, invoice): + """ Create ir.attachment for the EDIs from invoice. + + :param invoice: the invoice to generate the EDI from. + :returns: the newly created attachments. + """ + attachment_vals_list = [] + for edi_format in self: + vals = edi_format._export_invoice_to_attachment(invoice) + if vals: + vals['datas'] = base64.encodebytes(vals['datas']) + vals['edi_format_id'] = edi_format._origin.id + attachment_vals_list.append(vals) + res = self.env['ir.attachment'].create(attachment_vals_list) + invoice.edi_document_ids |= res + return res + + #################################################### + # Import Internal methods (not meant to be overridden) + #################################################### + + def _decode_xml(self, filename, content): + """Decodes an xml into a list of one dictionary representing an attachment. + + :param filename: The name of the xml. + :param attachment: The xml as a string. + :returns: A list with a dictionary. + * filename: The name of the attachment. + * content: The content of the attachment. + * type: The type of the attachment. + * xml_tree: The tree of the xml if type is xml. + * pdf_reader: The pdf_reader if type is pdf. + """ + to_process = [] + try: + xml_tree = etree.fromstring(content) + except Exception as e: + _logger.exception("Error when converting the xml content to etree: %s" % e) + return to_process + if len(xml_tree): + to_process.append({ + 'filename': filename, + 'content': content, + 'type': 'xml', + 'xml_tree': xml_tree, + }) + return to_process + + def _decode_pdf(self, filename, content): + """Decodes a pdf and unwrap sub-attachment into a list of dictionary each representing an attachment. + + :param filename: The name of the pdf. + :param content: The bytes representing the pdf. + :returns: A list of dictionary for each attachment. + * filename: The name of the attachment. + * content: The content of the attachment. + * type: The type of the attachment. + * xml_tree: The tree of the xml if type is xml. + * pdf_reader: The pdf_reader if type is pdf. + """ + to_process = [] + try: + buffer = io.BytesIO(content) + pdf_reader = OdooPdfFileReader(buffer) + except Exception as e: + # Malformed pdf + _logger.exception("Error when reading the pdf: %s" % e) + return to_process + + # Process embedded files. + for xml_name, content in pdf_reader.getAttachments(): + to_process.extend(self._decode_xml(xml_name, content)) + + # Process the pdf itself. + to_process.append({ + 'filename': filename, + 'content': content, + 'type': 'pdf', + 'pdf_reader': pdf_reader, + }) + + return to_process + + def _decode_attachment(self, attachment): + """Decodes an ir.attachment and unwrap sub-attachment into a list of dictionary each representing an attachment. + + :param attachment: An ir.attachment record. + :returns: A list of dictionary for each attachment. + * filename: The name of the attachment. + * content: The content of the attachment. + * type: The type of the attachment. + * xml_tree: The tree of the xml if type is xml. + * pdf_reader: The pdf_reader if type is pdf. + """ + content = base64.b64decode(attachment.with_context(bin_size=False).datas) + to_process = [] + + if 'pdf' in attachment.mimetype: + to_process.extend(self._decode_pdf(attachment.name, content)) + elif 'xml' in attachment.mimetype: + to_process.extend(self._decode_xml(attachment.name, content)) + + return to_process + + def _create_invoice_from_attachment(self, attachment): + """Decodes an ir.attachment to create an invoice. + + :param attachment: An ir.attachment record. + :returns: The invoice where to import data. + """ + for file_data in self._decode_attachment(attachment): + for edi_format in self: + res = False + if file_data['type'] == 'xml': + res = edi_format._create_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree']) + elif file_data['type'] == 'pdf': + res = edi_format._create_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader']) + file_data['pdf_reader'].stream.close() + if res: + if 'extract_state' in res: + # Bypass the OCR to prevent overwriting data when an EDI was succesfully imported. + # TODO : remove when we integrate the OCR to the EDI flow. + res.write({'extract_state': 'done'}) + return res + return self.env['account.move'] + + def _update_invoice_from_attachment(self, attachment, invoice): + """Decodes an ir.attachment to update an invoice. + + :param attachment: An ir.attachment record. + :returns: The invoice where to import data. + """ + for file_data in self._decode_attachment(attachment): + for edi_format in self: + res = False + if file_data['type'] == 'xml': + res = edi_format._update_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], invoice) + elif file_data['type'] == 'pdf': + res = edi_format._update_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], invoice) + file_data['pdf_reader'].stream.close() + if res: + if 'extract_state' in res: + # Bypass the OCR to prevent overwriting data when an EDI was succesfully imported. + # TODO : remove when we integrate the OCR to the EDI flow. + res.write({'extract_state': 'done'}) + return res + return self.env['account.move'] diff --git a/addons/account_edi/models/account_journal.py b/addons/account_edi/models/account_journal.py new file mode 100644 index 00000000000..c160d8da249 --- /dev/null +++ b/addons/account_edi/models/account_journal.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + edi_format_ids = fields.Many2many(comodel_name='account.edi.format', + string='Electronic invoicing', + help='Send XML/EDI invoices', + domain="[('hide_on_journal', '=', 'import_export')]") + + def _create_invoice_from_single_attachment(self, attachment): + invoice = self.env['account.edi.format'].search([])._create_invoice_from_attachment(attachment) + if invoice: + # with_context: we don't want to import the attachment since the invoice was just created from it. + invoice.with_context(no_new_invoice=True).message_post(attachment_ids=[attachment.id]) + return invoice + return super()._create_invoice_from_single_attachment(attachment) diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py new file mode 100644 index 00000000000..0ee7e37328c --- /dev/null +++ b/addons/account_edi/models/account_move.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields + + +class AccountMove(models.Model): + _inherit = 'account.move' + + edi_document_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'account.move'), ('edi_format_id', '!=', False)]) + + def post(self): + # OVERRIDE + # Generate the electronic documents for the move. + existing_attachments = self.env['ir.attachment'].search([ + ('res_model', '=', 'account.move'), + ('res_id', 'in', self.ids), + ('edi_format_id', 'in', self.journal_id.edi_format_ids.ids)]) + existing_attachments.unlink() + res = super(AccountMove, self).post() + for move in self: + move.journal_id.edi_format_ids._create_ir_attachments(move) + + @api.returns('mail.message', lambda value: value.id) + def message_post(self, **kwargs): + # OVERRIDE + # When posting a message, analyse the attachment to check if it is an EDI document and update the invoice + # with the data. + res = super(AccountMove, self).message_post(**kwargs) + + if len(self) != 1 or self.env.context.get('no_new_invoice') or not self.is_invoice(include_receipts=True): + return res + + attachments = self.env['ir.attachment'].browse(kwargs.get('attachment_ids', [])) + odoobot = self.env.ref('base.partner_root') + if attachments and self.state != 'draft': + self.message_post(body='The invoice is not a draft, it was not updated from the attachment.', + message_type='comment', + subtype_xmlid='mail.mt_note', + author_id=odoobot.id) + return res + if attachments and self.line_ids: + self.message_post(body='The invoice already contains lines, it was not updated from the attachment.', + message_type='comment', + subtype_xmlid='mail.mt_note', + author_id=odoobot.id) + return res + + for attachment in attachments: + invoice = self.env['account.edi.format'].search([])._update_invoice_from_attachment(attachment, self) + break + + return res diff --git a/addons/account_edi/models/ir_actions_report.py b/addons/account_edi/models/ir_actions_report.py new file mode 100644 index 00000000000..9dcba74099c --- /dev/null +++ b/addons/account_edi/models/ir_actions_report.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ + + +class IrActionsReport(models.Model): + _inherit = 'ir.actions.report' + + def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None): + # OVERRIDE to embed some EDI documents inside the PDF. + if self.model == 'account.move' and res_ids and len(res_ids) == 1 and pdf_content: + invoice = self.env['account.move'].browse(res_ids) + if invoice.is_sale_document() and invoice.state != 'draft': + pdf_content = invoice.journal_id.edi_format_ids._embed_edis_to_pdf(pdf_content, invoice) + + return super(IrActionsReport, self)._post_pdf(save_in_attachment, pdf_content=pdf_content, res_ids=res_ids) diff --git a/addons/account_edi/models/ir_attachment.py b/addons/account_edi/models/ir_attachment.py new file mode 100644 index 00000000000..146411bc28e --- /dev/null +++ b/addons/account_edi/models/ir_attachment.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + + +class IrAttachment(models.Model): + _inherit = 'ir.attachment' + + edi_format_id = fields.Many2one('account.edi.format') diff --git a/addons/account_edi/models/mail_template.py b/addons/account_edi/models/mail_template.py new file mode 100644 index 00000000000..209b9160e0e --- /dev/null +++ b/addons/account_edi/models/mail_template.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +from odoo import api, models + + +class MailTemplate(models.Model): + _inherit = "mail.template" + + def generate_email(self, res_ids, fields): + res = super().generate_email(res_ids, fields) + + multi_mode = True + if isinstance(res_ids, int): + res_ids = [res_ids] + multi_mode = False + + if self.model != 'account.move': + return res + + existing_attachments = self.env['ir.attachment'].search([ + ('res_model', '=', 'account.move'), + ('res_id', 'in', res_ids), + ('edi_format_id', '!=', False)]) + + for record in self.env[self.model].browse(res_ids): + available_formats = existing_attachments.filtered(lambda a: a.res_id == record.id) + missing_formats = record.journal_id.edi_format_ids.filtered(lambda f: f._origin.id not in available_formats.edi_format_id.ids) + new_attachments = missing_formats._create_ir_attachments(record) + available_formats |= new_attachments + (res[record.id] if multi_mode else res).setdefault('attachments', []).extend([(a.name, a.datas) for a in available_formats]) + + return res diff --git a/addons/account_edi/security/ir.model.access.csv b/addons/account_edi/security/ir.model.access.csv new file mode 100644 index 00000000000..71c1f8bda06 --- /dev/null +++ b/addons/account_edi/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_edi_format_readonly,account.edi.format,model_account_edi_format,,1,0,0,0 diff --git a/addons/account_edi/views/account_journal_views.xml b/addons/account_edi/views/account_journal_views.xml new file mode 100644 index 00000000000..cd466c6ee2f --- /dev/null +++ b/addons/account_edi/views/account_journal_views.xml @@ -0,0 +1,17 @@ + + + + + account.journal.form.inherited + account.journal + + + + + + + + + + + diff --git a/addons/account_edi/wizard/__init__.py b/addons/account_edi/wizard/__init__.py new file mode 100644 index 00000000000..edaf0442f27 --- /dev/null +++ b/addons/account_edi/wizard/__init__.py @@ -0,0 +1 @@ +from . import account_invoice_send diff --git a/addons/account_edi/wizard/account_invoice_send.py b/addons/account_edi/wizard/account_invoice_send.py new file mode 100644 index 00000000000..3cf5e2cef75 --- /dev/null +++ b/addons/account_edi/wizard/account_invoice_send.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +import base64 + + +class AccountInvoiceSend(models.TransientModel): + _inherit = 'account.invoice.send' + _description = 'Account Invoice Send' + + edi_format_ids = fields.Many2many(related='invoice_ids.journal_id.edi_format_ids', string="Electronic invoicing") diff --git a/addons/account_edi/wizard/account_invoice_send_views.xml b/addons/account_edi/wizard/account_invoice_send_views.xml new file mode 100644 index 00000000000..023a56ef709 --- /dev/null +++ b/addons/account_edi/wizard/account_invoice_send_views.xml @@ -0,0 +1,29 @@ + + + + + account.invoice.send.form.inherited.edi + account.invoice.send + + + +
+ + +
+
+
+
+
+
diff --git a/addons/account_edi_facturx/__init__.py b/addons/account_edi_facturx/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/addons/account_edi_facturx/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/account_facturx/__manifest__.py b/addons/account_edi_facturx/__manifest__.py similarity index 64% rename from addons/account_facturx/__manifest__.py rename to addons/account_edi_facturx/__manifest__.py index a580e617a4c..3e7e16ffb11 100644 --- a/addons/account_facturx/__manifest__.py +++ b/addons/account_edi_facturx/__manifest__.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- { - 'name' : 'Import Bills/Invoices From XML', + 'name' : 'Import/Export invoices with Factur-X', 'version' : '1.0', 'category': 'Accounting/Accounting', - 'depends' : ['account'], + 'depends' : ['account_edi'], 'data': [ + 'data/account_edi_data.xml', 'data/facturx_templates.xml', ], 'installable': True, diff --git a/addons/account_edi_facturx/data/account_edi_data.xml b/addons/account_edi_facturx/data/account_edi_data.xml new file mode 100644 index 00000000000..d581c468a5c --- /dev/null +++ b/addons/account_edi_facturx/data/account_edi_data.xml @@ -0,0 +1,11 @@ + + + + + + Factur-X (FR) + facturx_1_0_05 + + + + \ No newline at end of file diff --git a/addons/account_facturx/data/facturx_templates.xml b/addons/account_edi_facturx/data/facturx_templates.xml similarity index 97% rename from addons/account_facturx/data/facturx_templates.xml rename to addons/account_edi_facturx/data/facturx_templates.xml index 98b49ee71b9..1ead8f38f46 100644 --- a/addons/account_facturx/data/facturx_templates.xml +++ b/addons/account_edi_facturx/data/facturx_templates.xml @@ -133,7 +133,7 @@ - + @@ -141,7 +141,7 @@ - + @@ -154,7 +154,7 @@ - + @@ -174,7 +174,7 @@ - + diff --git a/addons/account_edi_facturx/models/__init__.py b/addons/account_edi_facturx/models/__init__.py new file mode 100644 index 00000000000..9961df8933c --- /dev/null +++ b/addons/account_edi_facturx/models/__init__.py @@ -0,0 +1 @@ +from . import account_edi_format diff --git a/addons/account_facturx/models/account_move.py b/addons/account_edi_facturx/models/account_edi_format.py similarity index 60% rename from addons/account_facturx/models/account_move.py rename to addons/account_edi_facturx/models/account_edi_format.py index abab584a0b9..032675fc132 100644 --- a/addons/account_facturx/models/account_move.py +++ b/addons/account_edi_facturx/models/account_edi_format.py @@ -10,23 +10,22 @@ from lxml import etree from PyPDF2 import PdfFileReader import io -import base64 import logging + _logger = logging.getLogger(__name__) DEFAULT_FACTURX_DATE_FORMAT = '%Y%m%d' -class AccountMove(models.Model): - _inherit = 'account.move' +class AccountEdiFormat(models.Model): + _inherit = 'account.edi.format' - def _export_as_facturx_xml(self): - ''' Create the Factur-x xml file content. - :return: The XML content as str. - ''' + def _export_invoice_to_attachment(self, invoice): self.ensure_one() + if self.code != 'facturx_1_0_05': + return super()._export_invoice_to_attachment(invoice) def format_date(dt): # Format the date in the Factur-x standard. @@ -39,35 +38,59 @@ class AccountMove(models.Model): # Create file content. template_values = { - 'record': self, + 'record': invoice, 'format_date': format_date, 'format_monetary': format_monetary, } - content = self.env.ref('account_facturx.account_invoice_facturx_export')._render(template_values) - return b"" + content + xml_content = b"" + xml_content += self.env.ref('account_edi_facturx.account_invoice_facturx_export')._render(template_values) + xml_name = '%s_facturx.xml' % (invoice.name.replace('/', '_')) + return {'name': xml_name, + 'datas': xml_content, + 'res_model': 'account.move', + 'res_id': invoice._origin.id, + 'mimetype': 'application/xml' + } - def _import_facturx_invoice(self, tree): - ''' Extract invoice values from the Factur-x xml tree passed as parameter. + 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. + """ - :param tree: The tree of the Factur-x xml file. - :return: A dictionary containing account.invoice values to create/update it. - ''' amount_total_import = None - default_type = False - if self._context.get('default_journal_id'): + default_move_type = False + if invoice._context.get('default_journal_id'): journal = self.env['account.journal'].browse(self.env.context['default_journal_id']) - default_type = 'out_invoice' if journal.type == 'sale' else 'in_invoice' - elif self._context.get('default_move_type'): - default_type = self._context['default_move_type'] - elif self.move_type in self.env['account.move'].get_invoice_types(include_receipts=True): + 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_type = self.move_type + default_move_type = invoice.move_type - if not default_type: + if not default_move_type: raise UserError(_("No information about the journal or the type of invoice is passed")) - if default_type == 'entry': + if default_move_type == 'entry': return # Total amount. @@ -82,31 +105,31 @@ class AccountMove(models.Model): elements = tree.xpath('//rsm:ExchangedDocument/ram:TypeCode', namespaces=tree.nsmap) type_code = elements[0].text - default_type.replace('_refund', '_invoice') + default_move_type.replace('_refund', '_invoice') if type_code == '381': - default_type = 'out_refund' if default_type == 'out_invoice' else 'in_refund' + 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_type = 'out_refund' if default_type == 'out_invoice' else 'in_refund' - refund_sign = -1 if 'refund' in default_type else 1 + 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. - self.move_type = default_type + invoice.move_type = default_move_type # self could be a single record (editing) or be empty (new). - with Form(self.with_context(default_move_type=default_type)) as invoice_form: + with Form(invoice.with_context(default_move_type=default_move_type)) 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' - elements = tree.xpath('//ram:'+partner_type+'/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap) + elements = tree.xpath('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap) partner = elements and self.env['res.partner'].search([('vat', '=', elements[0].text)], limit=1) if not partner: - elements = tree.xpath('//ram:'+partner_type+'/ram:Name', namespaces=tree.nsmap) + elements = tree.xpath('//ram:' + partner_type + '/ram:Name', namespaces=tree.nsmap) partner_name = elements and elements[0].text partner = elements and self.env['res.partner'].search([('name', 'ilike', partner_name)], limit=1) if not partner: - elements = tree.xpath('//ram:'+partner_type+'//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap) + elements = tree.xpath('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap) partner = elements and self.env['res.partner'].search([('email', '=', elements[0].text)], limit=1) if partner: invoice_form.partner_id = partner @@ -223,98 +246,3 @@ class AccountMove(models.Model): invoice_line_form.price_unit = amount_total_import return invoice_form.save() - - @api.returns('mail.message', lambda value: value.id) - def message_post(self, **kwargs): - # OVERRIDE - # /!\ 'default_res_id' in self._context is used to don't process attachment when using a form view. - res = super(AccountMove, self).message_post(**kwargs) - - if not self.env.context.get('no_new_invoice') and len(self) == 1 and self.state == 'draft' and ( - self.env.context.get('default_move_type', self.move_type) in self.env['account.move'].get_invoice_types(include_receipts=True) - or self.env['account.journal'].browse(self.env.context.get('default_journal_id')).type in ('sale', 'purchase') - ): - for attachment in self.env['ir.attachment'].browse(kwargs.get('attachment_ids', [])): - self._create_invoice_from_attachment(attachment) - return res - - def _create_invoice_from_attachment(self, attachment): - if 'pdf' in attachment.mimetype: - for move in self: - move._create_invoice_from_pdf(attachment) - if 'xml' in attachment.mimetype: - for move in self: - move._create_invoice_from_xml(attachment) - - def _create_invoice_from_pdf(self, attachment): - content = base64.b64decode(attachment.datas) - - with io.BytesIO(content) as buffer: - try: - reader = PdfFileReader(buffer) - - # Search for Factur-x embedded file. - if reader.trailer['/Root'].get('/Names') and reader.trailer['/Root']['/Names'].get('/EmbeddedFiles'): - # N.B: embedded_files looks like: - # ['file.xml', {'/Type': '/Filespec', '/F': 'file.xml', '/EF': {'/F': IndirectObject(22, 0)}}] - embedded_files = reader.trailer['/Root']['/Names']['/EmbeddedFiles']['/Names'] - # '[::2]' because it's a list [fn_1, content_1, fn_2, content_2, ..., fn_n, content_2] - for filename_obj, content_obj in list(zip(embedded_files, embedded_files[1:]))[::2]: - content = content_obj.getObject()['/EF']['/F'].getData() - - if filename_obj == 'factur-x.xml': - try: - tree = etree.fromstring(content) - except Exception: - continue - - self._import_facturx_invoice(tree) - self._remove_ocr_option() - buffer.close() - except UserError as e: - raise e - except Exception as e: - # Malformed pdf - _logger.exception(e) - - @api.model - def _get_xml_decoders(self): - ''' List of usable decoders to extract invoice from attachments. - - :return: a list of triplet (xml_type, check_func, decode_func) - * xml_type: The format name, e.g 'UBL 2.1' - * check_func: A function taking an etree and a file name as parameter and returning a dict: - * flag: The etree is part of this format. - * error: Error message. - * decode_func: A function taking an etree as parameter and returning an invoice record. - ''' - # TO BE OVERWRITTEN - return [] - - def _create_invoice_from_xml(self, attachment): - decoders = self._get_xml_decoders() - - # Convert attachment -> etree - content = base64.b64decode(attachment.datas) - try: - tree = etree.fromstring(content) - except Exception: - _logger.exception('The xml file is badly formatted : {}'.format(attachment.name)) - - for xml_type, check_func, decode_func in decoders: - check_res = check_func(tree, attachment.name) - - if check_res.get('flag') and not check_res.get('error'): - invoice_ids = decode_func(tree) - if invoice_ids: - invoice_ids._remove_ocr_option() - break - - try: - return invoice_ids - except UnboundLocalError: - _logger.exception('No decoder was found for the xml file: {}. The file is badly formatted, not supported or the decoder is not installed'.format(attachment.name)) - - def _remove_ocr_option(self): - if 'extract_state' in self: - self.write({'extract_state': 'done'}) diff --git a/addons/account_facturx/i18n/account_facturx.pot b/addons/account_facturx/i18n/account_facturx.pot deleted file mode 100644 index 6b0ced13208..00000000000 --- a/addons/account_facturx/i18n/account_facturx.pot +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~13.2\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-02-13 07:24+0000\n" -"PO-Revision-Date: 2020-02-13 07:24+0000\n" -"Last-Translator: \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:0 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/ar.po b/addons/account_facturx/i18n/ar.po deleted file mode 100644 index 878b8f8f7e7..00000000000 --- a/addons/account_facturx/i18n/ar.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Mustafa Rawi , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Mustafa Rawi , 2019\n" -"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ar\n" -"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "القيود اليومية" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "إجراء التقرير" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/cs.po b/addons/account_facturx/i18n/cs.po deleted file mode 100644 index 7cd9b660196..00000000000 --- a/addons/account_facturx/i18n/cs.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" -"Language-Team: Czech (https://www.transifex.com/odoo/teams/41243/cs/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: cs\n" -"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Položky deníku" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/da.po b/addons/account_facturx/i18n/da.po deleted file mode 100644 index 3108da823a5..00000000000 --- a/addons/account_facturx/i18n/da.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Ejner Sønniksen , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Ejner Sønniksen , 2019\n" -"Language-Team: Danish (https://www.transifex.com/odoo/teams/41243/da/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: da\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Journalposter" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/de.po b/addons/account_facturx/i18n/de.po deleted file mode 100644 index 506f53b6839..00000000000 --- a/addons/account_facturx/i18n/de.po +++ /dev/null @@ -1,60 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# Leon Grill , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Leon Grill , 2019\n" -"Language-Team: German (https://www.transifex.com/odoo/teams/41243/de/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: de\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Buchungssätze" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" -"Es wurde kein Decoder für die Xml-Datei gefunden: {}. Die Datei ist schlecht" -" formatiert, nicht unterstützt oder der Decoder ist nicht installiert" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" -"Es werden keine Informationen über das Journal oder die Art der Rechnung " -"übergeben" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "Die Xml-Datei ist schlecht formatiert : {}" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "urn:cen.eu:en16931:2017" diff --git a/addons/account_facturx/i18n/es.po b/addons/account_facturx/i18n/es.po deleted file mode 100644 index c5eb9ad3c42..00000000000 --- a/addons/account_facturx/i18n/es.po +++ /dev/null @@ -1,59 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# Pedro M. Baeza , 2019 -# John Guardado , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: John Guardado , 2019\n" -"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Asientos contables" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" -"No se encontró ningún decodificador para el archivo xml: {}. El archivo está" -" mal formateado, no es compatible o el decodificador no está instalado." - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "No hay información sobre el diario ni sobre el tipo de factura." - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "Acción de informe" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "El archivo xml está mal formateado : {}" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "urn:cen.eu:en16931:2017" diff --git a/addons/account_facturx/i18n/fr.po b/addons/account_facturx/i18n/fr.po deleted file mode 100644 index bf314b325a3..00000000000 --- a/addons/account_facturx/i18n/fr.po +++ /dev/null @@ -1,59 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# Cécile Collart , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Cécile Collart , 2019\n" -"Language-Team: French (https://www.transifex.com/odoo/teams/41243/fr/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: fr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Pièces comptables" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" -"Aucun décodeur n'a été trouvé pour le fichier XML: {}. Le fichier est mal " -"formaté, non supporté ou le décodeur n'est pas installé" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" -"Aucune information sur le journal ou le type de facture n'est transmise" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "Signaler l'action" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "Le fichier XML est mal formaté: {}" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "urn:cen.eu:en16931:2017" diff --git a/addons/account_facturx/i18n/he.po b/addons/account_facturx/i18n/he.po deleted file mode 100644 index 1fa3cedf917..00000000000 --- a/addons/account_facturx/i18n/he.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# דודי מלכה , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: דודי מלכה , 2019\n" -"Language-Team: Hebrew (https://www.transifex.com/odoo/teams/41243/he/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: he\n" -"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "רשומות יומן" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "דוח פעולה" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/hr.po b/addons/account_facturx/i18n/hr.po deleted file mode 100644 index 42800d08811..00000000000 --- a/addons/account_facturx/i18n/hr.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" -"Language-Team: Croatian (https://www.transifex.com/odoo/teams/41243/hr/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: hr\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Temeljnice" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/hu.po b/addons/account_facturx/i18n/hu.po deleted file mode 100644 index 38d04785980..00000000000 --- a/addons/account_facturx/i18n/hu.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" -"Language-Team: Hungarian (https://www.transifex.com/odoo/teams/41243/hu/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: hu\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Könyvelési tételek" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/it.po b/addons/account_facturx/i18n/it.po deleted file mode 100644 index 060fd0c68f4..00000000000 --- a/addons/account_facturx/i18n/it.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Sergio Zanchetta , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Sergio Zanchetta , 2019\n" -"Language-Team: Italian (https://www.transifex.com/odoo/teams/41243/it/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: it\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Registrazioni contabili" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "Azione resoconto" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "urn:cen.eu:en16931:2017" diff --git a/addons/account_facturx/i18n/lb.po b/addons/account_facturx/i18n/lb.po deleted file mode 100644 index 4bfa09c7d32..00000000000 --- a/addons/account_facturx/i18n/lb.po +++ /dev/null @@ -1,51 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Language-Team: Luxembourgish (https://www.transifex.com/odoo/teams/41243/lb/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: lb\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/lt.po b/addons/account_facturx/i18n/lt.po deleted file mode 100644 index 9d3bbe48a69..00000000000 --- a/addons/account_facturx/i18n/lt.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Linas Versada , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Linas Versada , 2019\n" -"Language-Team: Lithuanian (https://www.transifex.com/odoo/teams/41243/lt/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: lt\n" -"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Žurnalo įrašai" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/mn.po b/addons/account_facturx/i18n/mn.po deleted file mode 100644 index 7cc7afe3efa..00000000000 --- a/addons/account_facturx/i18n/mn.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Baskhuu Lodoikhuu , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Baskhuu Lodoikhuu , 2019\n" -"Language-Team: Mongolian (https://www.transifex.com/odoo/teams/41243/mn/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: mn\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Ажил гүйлгээ" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/nb.po b/addons/account_facturx/i18n/nb.po deleted file mode 100644 index aa0abb10169..00000000000 --- a/addons/account_facturx/i18n/nb.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" -"Language-Team: Norwegian Bokmål (https://www.transifex.com/odoo/teams/41243/nb/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: nb\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Journalregistreringer" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/nl.po b/addons/account_facturx/i18n/nl.po deleted file mode 100644 index a0bb8abac93..00000000000 --- a/addons/account_facturx/i18n/nl.po +++ /dev/null @@ -1,59 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# Yenthe Van Ginneken , 2019 -# Gunther Clauwaert , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Gunther Clauwaert , 2019\n" -"Language-Team: Dutch (https://www.transifex.com/odoo/teams/41243/nl/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: nl\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Boekingen" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" -"Er werd geen decoder voor de xml file: {} gevonden. De file is slecht " -"geformatteerd, niet ondersteunt of de decoder is niet geïnstalleerd" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "Rapport actie" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "Het XML bestand is slecht geformatteerd : {}" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "urn:cen.eu:en16931:2017" diff --git a/addons/account_facturx/i18n/pl.po b/addons/account_facturx/i18n/pl.po deleted file mode 100644 index 201fa57d803..00000000000 --- a/addons/account_facturx/i18n/pl.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" -"Language-Team: Polish (https://www.transifex.com/odoo/teams/41243/pl/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: pl\n" -"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Zapisy księgowe" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/pt.po b/addons/account_facturx/i18n/pt.po deleted file mode 100644 index 5490c0a19fe..00000000000 --- a/addons/account_facturx/i18n/pt.po +++ /dev/null @@ -1,51 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Language-Team: Portuguese (https://www.transifex.com/odoo/teams/41243/pt/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: pt\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/pt_BR.po b/addons/account_facturx/i18n/pt_BR.po deleted file mode 100644 index ccd929f4f23..00000000000 --- a/addons/account_facturx/i18n/pt_BR.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" -"Language-Team: Portuguese (Brazil) (https://www.transifex.com/odoo/teams/41243/pt_BR/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: pt_BR\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Lançamentos de Diário" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/ro.po b/addons/account_facturx/i18n/ro.po deleted file mode 100644 index 3915785e87d..00000000000 --- a/addons/account_facturx/i18n/ro.po +++ /dev/null @@ -1,51 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Language-Team: Romanian (https://www.transifex.com/odoo/teams/41243/ro/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ro\n" -"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/ru.po b/addons/account_facturx/i18n/ru.po deleted file mode 100644 index 6e267ff2124..00000000000 --- a/addons/account_facturx/i18n/ru.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Martin Trigaux, 2019\n" -"Language-Team: Russian (https://www.transifex.com/odoo/teams/41243/ru/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: ru\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Записи журнала" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/tr.po b/addons/account_facturx/i18n/tr.po deleted file mode 100644 index 1fd82aee36f..00000000000 --- a/addons/account_facturx/i18n/tr.po +++ /dev/null @@ -1,55 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Murat Kaplan , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Murat Kaplan , 2019\n" -"Language-Team: Turkish (https://www.transifex.com/odoo/teams/41243/tr/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: tr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Yevmiye Kayıtları" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/uk.po b/addons/account_facturx/i18n/uk.po deleted file mode 100644 index 92b2a03f3c5..00000000000 --- a/addons/account_facturx/i18n/uk.po +++ /dev/null @@ -1,58 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# Alina Lisnenko , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Alina Lisnenko , 2019\n" -"Language-Team: Ukrainian (https://www.transifex.com/odoo/teams/41243/uk/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: uk\n" -"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Записи в журналі" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" -"Не знайдено декодер для xml фалу: {}. Файл погано відформатований, не " -"підтримується або декодер не встановлено" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "Пропущено інформацію про журнал або тип рахунку" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "Дія звіту" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "Файл xml погано відформатовано : {}" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "urn:cen.eu:en16931:2017" diff --git a/addons/account_facturx/i18n/vi.po b/addons/account_facturx/i18n/vi.po deleted file mode 100644 index 5046ebf5529..00000000000 --- a/addons/account_facturx/i18n/vi.po +++ /dev/null @@ -1,56 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Nancy Momoland , 2019 -# Chinh Chinh , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: Chinh Chinh , 2019\n" -"Language-Team: Vietnamese (https://www.transifex.com/odoo/teams/41243/vi/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: vi\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "Bút toán phát sinh" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "Báo cáo hành động" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/zh_CN.po b/addons/account_facturx/i18n/zh_CN.po deleted file mode 100644 index 0f73bed2afc..00000000000 --- a/addons/account_facturx/i18n/zh_CN.po +++ /dev/null @@ -1,56 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Jeanphy , 2019 -# niulin lnc. , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: niulin lnc. , 2019\n" -"Language-Team: Chinese (China) (https://www.transifex.com/odoo/teams/41243/zh_CN/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: zh_CN\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "日记账分录" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "报告动作" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/i18n/zh_TW.po b/addons/account_facturx/i18n/zh_TW.po deleted file mode 100644 index d82ab1bd4ef..00000000000 --- a/addons/account_facturx/i18n/zh_TW.po +++ /dev/null @@ -1,56 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * account_facturx -# -# Translators: -# Martin Trigaux, 2019 -# 敬雲 林 , 2019 -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~12.4\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-12 11:32+0000\n" -"PO-Revision-Date: 2019-08-26 09:08+0000\n" -"Last-Translator: 敬雲 林 , 2019\n" -"Language-Team: Chinese (Taiwan) (https://www.transifex.com/odoo/teams/41243/zh_TW/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: zh_TW\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_account_move -msgid "Journal Entries" -msgstr "日記帳分錄" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:328 -#, python-format -msgid "" -"No decoder was found for the xml file: {}. The file is badly formatted, not " -"supported or the decoder is not installed" -msgstr "" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:65 -#, python-format -msgid "No information about the journal or the type of invoice is passed" -msgstr "" - -#. module: account_facturx -#: model:ir.model,name:account_facturx.model_ir_actions_report -msgid "Report Action" -msgstr "報告動作" - -#. module: account_facturx -#: code:addons/account_facturx/models/account_move.py:314 -#, python-format -msgid "The xml file is badly formatted : {}" -msgstr "" - -#. module: account_facturx -#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export -msgid "urn:cen.eu:en16931:2017" -msgstr "" diff --git a/addons/account_facturx/models/__init__.py b/addons/account_facturx/models/__init__.py deleted file mode 100644 index d1070b6c5f1..00000000000 --- a/addons/account_facturx/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- encoding: utf-8 -*- - -from . import account_move -from . import ir_actions_report diff --git a/addons/account_facturx/models/ir_actions_report.py b/addons/account_facturx/models/ir_actions_report.py deleted file mode 100644 index 72244d8e241..00000000000 --- a/addons/account_facturx/models/ir_actions_report.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- - -from odoo import models, fields, api, _ - -from PyPDF2 import PdfFileWriter, PdfFileReader - -import io - - -class IrActionsReport(models.Model): - _inherit = 'ir.actions.report' - - def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None): - # OVERRIDE - if self.model == 'account.move' and res_ids and len(res_ids) == 1: - invoice = self.env['account.move'].browse(res_ids) - if invoice.is_sale_document() and invoice.state != 'draft': - xml_content = invoice._export_as_facturx_xml() - - # Add attachment. - reader_buffer = io.BytesIO(pdf_content) - reader = PdfFileReader(reader_buffer) - writer = PdfFileWriter() - writer.cloneReaderDocumentRoot(reader) - writer.addAttachment('factur-x.xml', xml_content) - buffer = io.BytesIO() - writer.write(buffer) - pdf_content = buffer.getvalue() - - reader_buffer.close() - buffer.close() - return super(IrActionsReport, self)._post_pdf(save_in_attachment, pdf_content=pdf_content, res_ids=res_ids) From 9e3d70c3fadd57e7a7588a3749a3a79b29f17bec Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Tue, 26 May 2020 07:47:02 +0000 Subject: [PATCH 3/5] [ADD] account_edi_ubl: module to manage EDI in generic UBL. - E-FFF import moved from l10n_be_edi into a generic UBL import. - [FIX] when importing if LineExtensionAmount is set but not PriceUnit on a move line, the price was not imported. - Added export to generic UBL 2.1. --- addons/account_edi_ubl/__init__.py | 1 + addons/account_edi_ubl/__manifest__.py | 13 ++ addons/account_edi_ubl/data/ubl_templates.xml | 167 ++++++++++++++++ addons/account_edi_ubl/models/__init__.py | 2 + .../models/account_edi_format.py | 187 ++++++++++++++++++ addons/account_edi_ubl/models/account_move.py | 21 ++ 6 files changed, 391 insertions(+) create mode 100644 addons/account_edi_ubl/__init__.py create mode 100644 addons/account_edi_ubl/__manifest__.py create mode 100644 addons/account_edi_ubl/data/ubl_templates.xml create mode 100644 addons/account_edi_ubl/models/__init__.py create mode 100644 addons/account_edi_ubl/models/account_edi_format.py create mode 100644 addons/account_edi_ubl/models/account_move.py diff --git a/addons/account_edi_ubl/__init__.py b/addons/account_edi_ubl/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/addons/account_edi_ubl/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/account_edi_ubl/__manifest__.py b/addons/account_edi_ubl/__manifest__.py new file mode 100644 index 00000000000..afa10b999c1 --- /dev/null +++ b/addons/account_edi_ubl/__manifest__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +{ + 'name' : 'Import/Export invoices with generic UBL', + 'version' : '1.0', + 'category': 'Accounting/Accounting', + 'depends' : ['account_edi'], + 'data': [ + 'data/ubl_templates.xml', + ], + 'installable': True, + 'application': False, + 'auto_install': True, +} diff --git a/addons/account_edi_ubl/data/ubl_templates.xml b/addons/account_edi_ubl/data/ubl_templates.xml new file mode 100644 index 00000000000..e591603b070 --- /dev/null +++ b/addons/account_edi_ubl/data/ubl_templates.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + diff --git a/addons/account_edi_ubl/models/__init__.py b/addons/account_edi_ubl/models/__init__.py new file mode 100644 index 00000000000..8c67bc9d5f4 --- /dev/null +++ b/addons/account_edi_ubl/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_move +from . import account_edi_format diff --git a/addons/account_edi_ubl/models/account_edi_format.py b/addons/account_edi_ubl/models/account_edi_format.py new file mode 100644 index 00000000000..7aa98dc9806 --- /dev/null +++ b/addons/account_edi_ubl/models/account_edi_format.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +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 odoo.osv import expression + +from datetime import datetime + +import logging + +_logger = logging.getLogger(__name__) + + +class AccountEdiFormat(models.Model): + _inherit = 'account.edi.format' + + def _import_ubl(self, tree, invoice): + """ Decodes an UBL invoice into an invoice. + + :param tree: the UBL tree to decode. + :param invoice: the invoice to update or an empty recordset. + :returns: the invoice where the UBL data was imported. + """ + + def _get_ubl_namespaces(): + ''' If the namespace is declared with xmlns='...', the namespaces map contains the 'None' key that causes an + TypeError: empty namespace prefix is not supported in XPath + Then, we need to remap arbitrarily this key. + + :param tree: An instance of etree. + :return: The namespaces map without 'None' key. + ''' + namespaces = tree.nsmap + namespaces['inv'] = namespaces.pop(None) + return namespaces + + namespaces = _get_ubl_namespaces() + if not invoice: + invoice = self.env['account.move'].create({}) + + elements = tree.xpath('//cbc:InvoiceTypeCode', namespaces=namespaces) + if elements: + type_code = elements[0].text + move_type = 'in_refund' if type_code == '381' else 'in_invoice' + else: + move_type = 'in_invoice' + + default_journal = invoice.with_context(default_move_type=move_type)._get_default_journal() + + with Form(invoice.with_context(default_move_type=move_type, default_journal_id=default_journal.id)) as invoice_form: + # Reference + elements = tree.xpath('//cbc:ID', namespaces=namespaces) + if elements: + invoice_form.ref = elements[0].text + elements = tree.xpath('//cbc:InstructionID', namespaces=namespaces) + if elements: + invoice_form.payment_reference = elements[0].text + + # Dates + elements = tree.xpath('//cbc:IssueDate', namespaces=namespaces) + if elements: + invoice_form.invoice_date = elements[0].text + elements = tree.xpath('//cbc:PaymentDueDate', namespaces=namespaces) + if elements: + invoice_form.invoice_date_due = elements[0].text + # allow both cbc:PaymentDueDate and cbc:DueDate + elements = tree.xpath('//cbc:DueDate', namespaces=namespaces) + invoice_form.invoice_date_due = invoice_form.invoice_date_due or elements and elements[0].text + + # Currency + elements = tree.xpath('//cbc:DocumentCurrencyCode', namespaces=namespaces) + currency_code = elements and elements[0].text or '' + currency = self.env['res.currency'].search([('name', '=', currency_code.upper())], limit=1) + if elements: + invoice_form.currency_id = currency + + # Incoterm + elements = tree.xpath('//cbc:TransportExecutionTerms/cac:DeliveryTerms/cbc:ID', namespaces=namespaces) + if elements: + invoice_form.invoice_incoterm_id = self.env['account.incoterms'].search([('code', '=', elements[0].text)], limit=1) + + # Partner + partner_element = tree.xpath('//cac:AccountingSupplierParty/cac:Party', namespaces=namespaces) + if partner_element: + domains = [] + partner_element = partner_element[0] + elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:Name', namespaces=namespaces) + if elements: + partner_name = elements[0].text + domains.append([('name', 'ilike', partner_name)]) + else: + partner_name = '' + elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:Telephone', namespaces=namespaces) + if elements: + partner_telephone = elements[0].text + domains.append([('phone', '=', partner_telephone), ('mobile', '=', partner_telephone)]) + elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:ElectronicMail', namespaces=namespaces) + if elements: + partner_mail = elements[0].text + domains.append([('email', '=', partner_mail)]) + elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:ID', namespaces=namespaces) + if elements: + partner_id = elements[0].text + domains.append([('vat', 'like', partner_id)]) + + if domains: + partner = self.env['res.partner'].search(expression.OR(domains), limit=1) + if partner: + invoice_form.partner_id = partner + partner_name = partner.name + else: + invoice_form.partner_id = self.env['res.partner'] + + # Regenerate PDF + attachments = self.env['ir.attachment'] + elements = tree.xpath('//cac:AdditionalDocumentReference', namespaces=namespaces) + for element in elements: + attachment_name = element.xpath('cbc:ID', namespaces=namespaces) + attachment_data = element.xpath('cac:Attachment//cbc:EmbeddedDocumentBinaryObject', namespaces=namespaces) + if attachment_name and attachment_data: + attachments |= self.env['ir.attachment'].create({ + 'name': attachment_name[0].text, + 'res_id': invoice.id, + 'res_model': 'account.move', + 'datas': attachment_data[0].text, + 'type': 'binary', + }) + if attachments: + invoice.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids) + + # Lines + lines_elements = tree.xpath('//cac:InvoiceLine', namespaces=namespaces) + for eline in lines_elements: + with invoice_form.invoice_line_ids.new() as invoice_line_form: + # Product + elements = eline.xpath('cac:Item/cac:SellersItemIdentification/cbc:ID', namespaces=namespaces) + domains = [] + if elements: + product_code = elements[0].text + domains.append([('default_code', '=', product_code)]) + elements = eline.xpath('cac:Item/cac:StandardItemIdentification/cbc:ID[@schemeID=\'GTIN\']', namespaces=namespaces) + if elements: + product_ean13 = elements[0].text + domains.append([('ean13', '=', product_ean13)]) + if domains: + product = self.env['product.product'].search(expression.OR(domains), limit=1) + if product: + invoice_line_form.product_id = product + + # Quantity + elements = eline.xpath('cbc:InvoicedQuantity', namespaces=namespaces) + quantity = elements and float(elements[0].text) or 1.0 + invoice_line_form.quantity = quantity + + # Price Unit + elements = eline.xpath('cac:Price/cbc:PriceAmount', namespaces=namespaces) + price_unit = elements and float(elements[0].text) or 0.0 + elements = eline.xpath('cbc:LineExtensionAmount', namespaces=namespaces) + line_extension_amount = elements and float(elements[0].text) or 0.0 + invoice_line_form.price_unit = price_unit or line_extension_amount / invoice_line_form.quantity or 0.0 + + # Name + elements = eline.xpath('cac:Item/cbc:Description', namespaces=namespaces) + if elements and elements[0].text: + invoice_line_form.name = elements[0].text + invoice_line_form.name = invoice_line_form.name.replace('%month%', str(fields.Date.to_date(invoice_form.invoice_date).month)) # TODO: full name in locale + invoice_line_form.name = invoice_line_form.name.replace('%year%', str(fields.Date.to_date(invoice_form.invoice_date).year)) + else: + invoice_line_form.name = "%s (%s)" % (partner_name or '', invoice_form.invoice_date) + + # Taxes + taxes_elements = eline.xpath('cac:TaxTotal/cac:TaxSubtotal', namespaces=namespaces) + invoice_line_form.tax_ids.clear() + for etax in taxes_elements: + elements = etax.xpath('cbc:Percent', namespaces=namespaces) + if elements: + tax = self.env['account.tax'].search([ + ('company_id', '=', self.env.company.id), + ('amount', '=', float(elements[0].text)), + ('type_tax_use', '=', invoice_form.journal_id.type), + ], order='sequence ASC', limit=1) + if tax: + invoice_line_form.tax_ids.add(tax) + + return invoice_form.save() diff --git a/addons/account_edi_ubl/models/account_move.py b/addons/account_edi_ubl/models/account_move.py new file mode 100644 index 00000000000..ce845c29028 --- /dev/null +++ b/addons/account_edi_ubl/models/account_move.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import models +from odoo.tools import float_repr + + +class AccountMove(models.Model): + _inherit = 'account.move' + + def _get_ubl_values(self): + def format_monetary(amount): + # Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001). + return float_repr(amount, self.currency_id.decimal_places) + + return { + 'invoice': self, + 'ubl_version': 2.1, + 'type_code': 380 if self.move_type == 'out_invoice' else 381, + 'payment_means_code': 42 if self.journal_id.bank_account_id else 31, + 'format_monetary': format_monetary, + } From 5b5ee381398090d6af9ce68041babb9953cd9e73 Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Tue, 26 May 2020 07:47:30 +0000 Subject: [PATCH 4/5] [IMP] l10n_be_edi: adaptation to account_edi + export in EFFF - Support for account_edi workflows + efff as an account.edi.format - Added export to EFFF. - Based on UBL (account_edi_ubl). TASK ID : 2154703 --- addons/l10n_be_edi/__manifest__.py | 3 +- addons/l10n_be_edi/data/account_edi_data.xml | 11 ++ addons/l10n_be_edi/models/__init__.py | 5 +- .../l10n_be_edi/models/account_edi_format.py | 46 +++++ addons/l10n_be_edi/models/account_invoice.py | 181 ------------------ addons/l10n_be_edi/models/account_move.py | 16 ++ addons/l10n_be_edi/tests/test_ubl.py | 3 + 7 files changed, 81 insertions(+), 184 deletions(-) create mode 100644 addons/l10n_be_edi/data/account_edi_data.xml create mode 100644 addons/l10n_be_edi/models/account_edi_format.py delete mode 100644 addons/l10n_be_edi/models/account_invoice.py create mode 100644 addons/l10n_be_edi/models/account_move.py diff --git a/addons/l10n_be_edi/__manifest__.py b/addons/l10n_be_edi/__manifest__.py index eb4be7f9998..c9bf6a9b841 100644 --- a/addons/l10n_be_edi/__manifest__.py +++ b/addons/l10n_be_edi/__manifest__.py @@ -11,8 +11,9 @@ invoices. The UBL standard became the `ISO/IEC 19845 (cf the `official announce `_). Belgian e-invoicing uses the UBL 2.0 using the e-fff protocol. """, - 'depends': ['account', 'l10n_be', 'account_facturx'], + 'depends': ['l10n_be', 'account_edi'], 'data': [ + 'data/account_edi_data.xml' ], 'installable': True, 'auto_install': True, diff --git a/addons/l10n_be_edi/data/account_edi_data.xml b/addons/l10n_be_edi/data/account_edi_data.xml new file mode 100644 index 00000000000..de4c58d4ba9 --- /dev/null +++ b/addons/l10n_be_edi/data/account_edi_data.xml @@ -0,0 +1,11 @@ + + + + + + E-FFF (BE) + efff_1 + + + + \ No newline at end of file diff --git a/addons/l10n_be_edi/models/__init__.py b/addons/l10n_be_edi/models/__init__.py index ab922af0d1e..047fc3ef4d9 100644 --- a/addons/l10n_be_edi/models/__init__.py +++ b/addons/l10n_be_edi/models/__init__.py @@ -1,3 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- encoding: utf-8 -*- -from . import account_invoice +from . import account_edi_format +from . import account_move diff --git a/addons/l10n_be_edi/models/account_edi_format.py b/addons/l10n_be_edi/models/account_edi_format.py new file mode 100644 index 00000000000..23bdf93c953 --- /dev/null +++ b/addons/l10n_be_edi/models/account_edi_format.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +from odoo import api, models, fields +from odoo.tools import float_repr + + +class AccountEdiFormat(models.Model): + _inherit = 'account.edi.format' + + def _is_efff(self, filename, tree): + return self.code == 'efff_1' and tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice' + + def _create_invoice_from_xml_tree(self, filename, tree): + self.ensure_one() + if self._is_efff(filename, tree): + return self._import_ubl(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_efff(filename, tree): + return self._import_ubl(tree, invoice) + return super()._update_invoice_from_xml_tree(filename, tree, invoice) + + def _export_invoice_to_attachment(self, invoice): + self.ensure_one() + if self.code != 'efff_1': + return super()._export_invoice_to_attachment(invoice) + + # Create file content. + xml_content = b"" + xml_content += self.env.ref('account_edi_ubl.export_ubl_invoice')._render(invoice._get_ubl_values()) + vat = invoice.company_id.partner_id.commercial_partner_id.vat + xml_name = 'efff-%s%s%s.xml' % (vat or '', '-' if vat else '', invoice.name.replace('/', '_')) # official naming convention + return {'name': xml_name, + 'datas': xml_content, + 'res_model': 'account.move', + 'res_id': invoice._origin.id, + 'mimetype': 'application/xml' + } + + def _export_invoice_to_embed_to_pdf(self, pdf_content, invoice): + self.ensure_one() + if self.code != 'efff_1': + return super()._export_invoice_to_embed_to_pdf(pdf_content, invoice) + return False # ubl must not be embedded to PDF. diff --git a/addons/l10n_be_edi/models/account_invoice.py b/addons/l10n_be_edi/models/account_invoice.py deleted file mode 100644 index f0acf9ecb9c..00000000000 --- a/addons/l10n_be_edi/models/account_invoice.py +++ /dev/null @@ -1,181 +0,0 @@ -# -*- coding: utf-8 -*- - -from odoo import api, fields, models, tools, _ -from odoo.tests.common import Form -from odoo.osv import expression - -import logging -_logger = logging.getLogger(__name__) - - -class AccountMove(models.Model): - _inherit = 'account.move' - - @api.model - def _get_ubl_namespaces(self, tree): - ''' If the namespace is declared with xmlns='...', the namespaces map contains the 'None' key that causes an - TypeError: empty namespace prefix is not supported in XPath - Then, we need to remap arbitrarily this key. - - :param tree: An instance of etree. - :return: The namespaces map without 'None' key. - ''' - namespaces = tree.nsmap - namespaces['inv'] = namespaces.pop(None) - return namespaces - - @api.model - def _detect_ubl_2_1(self, tree, file_name): - # Quick check the tree looks like an UBL 2.1 file. - flag = tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice' - error = None - - return {'flag': flag, 'error': error} - - @api.model - def _decode_ubl_2_1(self, tree): - self.ensure_one() - namespaces = self._get_ubl_namespaces(tree) - - elements = tree.xpath('//cbc:InvoiceTypeCode', namespaces=namespaces) - if elements: - type_code = elements[0].text - type = 'in_refund' if type_code == '381' else 'in_invoice' - else: - type = 'in_invoice' - - default_journal = self.with_context(default_move_type=type)._get_default_journal() - - with Form(self.with_context(default_move_type=type, default_journal_id=default_journal.id)) as invoice_form: - # Reference - elements = tree.xpath('//cbc:ID', namespaces=namespaces) - if elements: - invoice_form.ref = elements[0].text - elements = tree.xpath('//cbc:InstructionID', namespaces=namespaces) - if elements: - invoice_form.payment_reference = elements[0].text - - # Dates - elements = tree.xpath('//cbc:IssueDate', namespaces=namespaces) - if elements: - invoice_form.invoice_date = elements[0].text - elements = tree.xpath('//cbc:PaymentDueDate', namespaces=namespaces) - if elements: - invoice_form.invoice_date_due = elements[0].text - # allow both cbc:PaymentDueDate and cbc:DueDate - elements = tree.xpath('//cbc:DueDate', namespaces=namespaces) - invoice_form.invoice_date_due = invoice_form.invoice_date_due or elements and elements[0].text - - # Currency - elements = tree.xpath('//cbc:DocumentCurrencyCode', namespaces=namespaces) - currency_code = elements and elements[0].text or '' - currency = self.env['res.currency'].search([('name', '=', currency_code.upper())], limit=1) - if elements: - invoice_form.currency_id = currency - - # Incoterm - elements = tree.xpath('//cbc:TransportExecutionTerms/cac:DeliveryTerms/cbc:ID', namespaces=namespaces) - if elements: - invoice_form.invoice_incoterm_id = self.env['account.incoterms'].search([('code', '=', elements[0].text)], limit=1) - - # Partner - partner_element = tree.xpath('//cac:AccountingSupplierParty/cac:Party', namespaces=namespaces) - if partner_element: - domains = [] - partner_element = partner_element[0] - elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:Name', namespaces=namespaces) - if elements: - partner_name = elements[0].text - domains.append([('name', 'ilike', partner_name)]) - elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:Telephone', namespaces=namespaces) - if elements: - partner_telephone = elements[0].text - domains.append([('phone', '=', partner_telephone), ('mobile', '=', partner_telephone)]) - elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:ElectronicMail', namespaces=namespaces) - if elements: - partner_mail = elements[0].text - domains.append([('email', '=', partner_mail)]) - elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:ID', namespaces=namespaces) - if elements: - partner_id = elements[0].text - domains.append([('vat', 'like', partner_id)]) - - if domains: - partner = self.env['res.partner'].search(expression.OR(domains), limit=1) - if partner: - invoice_form.partner_id = partner - else: - invoice_form.partner_id = self.env['res.partner'] - - # Regenerate PDF - attachments = self.env['ir.attachment'] - elements = tree.xpath('//cac:AdditionalDocumentReference', namespaces=namespaces) - for element in elements: - attachment_name = element.xpath('cbc:ID', namespaces=namespaces) - attachment_data = element.xpath('cac:Attachment//cbc:EmbeddedDocumentBinaryObject', namespaces=namespaces) - if attachment_name and attachment_data: - attachments |= self.env['ir.attachment'].create({ - 'name': attachment_name[0].text, - 'res_id': self.id, - 'res_model': 'account.move', - 'datas': attachment_data[0].text, - 'type': 'binary', - }) - if attachments: - self.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids) - - # Lines - lines_elements = tree.xpath('//cac:InvoiceLine', namespaces=namespaces) - for eline in lines_elements: - with invoice_form.invoice_line_ids.new() as invoice_line_form: - # Quantity - elements = eline.xpath('cbc:InvoicedQuantity', namespaces=namespaces) - quantity = elements and float(elements[0].text) or 1.0 - invoice_line_form.quantity = quantity - - # Price Unit - elements = eline.xpath('cac:Price/cbc:PriceAmount', namespaces=namespaces) - invoice_line_form.price_unit = elements and float(elements[0].text) or 0.0 - - # Name - elements = eline.xpath('cac:Item/cbc:Description', namespaces=namespaces) - invoice_line_form.name = elements and elements[0].text or '' - invoice_line_form.name = invoice_line_form.name.replace('%month%', str(fields.Date.to_date(invoice_form.invoice_date).month)) # TODO: full name in locale - invoice_line_form.name = invoice_line_form.name.replace('%year%', str(fields.Date.to_date(invoice_form.invoice_date).year)) - - # Product - elements = eline.xpath('cac:Item/cac:SellersItemIdentification/cbc:ID', namespaces=namespaces) - domains = [] - if elements: - product_code = elements[0].text - domains.append([('default_code', '=', product_code)]) - elements = eline.xpath('cac:Item/cac:StandardItemIdentification/cbc:ID[@schemeID=\'GTIN\']', namespaces=namespaces) - if elements: - product_ean13 = elements[0].text - domains.append([('ean13', '=', product_ean13)]) - if domains: - product = self.env['product.product'].search(expression.OR(domains), limit=1) - if product: - invoice_line_form.product_id = product - - # Taxes - taxes_elements = eline.xpath('cac:TaxTotal/cac:TaxSubtotal', namespaces=namespaces) - invoice_line_form.tax_ids.clear() - for etax in taxes_elements: - elements = etax.xpath('cbc:Percent', namespaces=namespaces) - if elements: - tax = self.env['account.tax'].search([ - ('company_id', '=', self.env.company.id), - ('amount', '=', float(elements[0].text)), - ('type_tax_use', '=', invoice_form.journal_id.type), - ], order='sequence ASC', limit=1) - if tax: - invoice_line_form.tax_ids.add(tax) - - return invoice_form.save() - - @api.model - def _get_xml_decoders(self): - # Override - ubl_decoders = [('UBL 2.1', self._detect_ubl_2_1, self._decode_ubl_2_1)] - return super(AccountMove, self)._get_xml_decoders() + ubl_decoders diff --git a/addons/l10n_be_edi/models/account_move.py b/addons/l10n_be_edi/models/account_move.py new file mode 100644 index 00000000000..50ce265a54a --- /dev/null +++ b/addons/l10n_be_edi/models/account_move.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from odoo import models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + def _get_ubl_values(self): + values = super(AccountMove, self)._get_ubl_values() + + # E-fff uses ubl_version 2.0, account_edi_ubl supports ubl_version 2.1 but generates 2.0 UBL + # so we only need to override the version to be compatible with E-FFF + values['ubl_version'] = 2.0 + + return values diff --git a/addons/l10n_be_edi/tests/test_ubl.py b/addons/l10n_be_edi/tests/test_ubl.py index cbbf385b459..1c4348a2aa7 100644 --- a/addons/l10n_be_edi/tests/test_ubl.py +++ b/addons/l10n_be_edi/tests/test_ubl.py @@ -25,8 +25,11 @@ class TestUBL(common.TransactionCase): 'res_model': 'account.move', }) + invoice_count = len(self.env['account.move'].search([])) + invoice.message_post(attachment_ids=[attachment_id.id]) + self.assertEqual(len(self.env['account.move'].search([])), invoice_count) self.assertEqual(invoice.amount_total, 666.50) self.assertEqual(invoice.amount_tax, 115.67) self.assertEqual(invoice.partner_id, self.partner_id) From ac0381d216c7b24b1fe5d9091d655d25d074cc81 Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Tue, 26 May 2020 07:47:54 +0000 Subject: [PATCH 5/5] [IMP] l10n_it_edi: adaptation to account_edi - Support for account_edi workflows (only import) + fattura_pa as an account.edi.format. - XML containing multiple invoices cannot be imported from the chatter. --- addons/account_edi/models/account_move.py | 2 + addons/l10n_it_edi/__manifest__.py | 2 + addons/l10n_it_edi/data/account_edi_data.xml | 12 + addons/l10n_it_edi/models/__init__.py | 1 + .../l10n_it_edi/models/account_edi_format.py | 421 ++++++++++++++++++ addons/l10n_it_edi/models/account_invoice.py | 410 +---------------- addons/l10n_it_edi/models/ir_mail_server.py | 2 +- 7 files changed, 453 insertions(+), 397 deletions(-) create mode 100644 addons/l10n_it_edi/data/account_edi_data.xml create mode 100644 addons/l10n_it_edi/models/account_edi_format.py diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py index 0ee7e37328c..b44a375673b 100644 --- a/addons/account_edi/models/account_move.py +++ b/addons/account_edi/models/account_move.py @@ -21,6 +21,8 @@ class AccountMove(models.Model): for move in self: move.journal_id.edi_format_ids._create_ir_attachments(move) + return res + @api.returns('mail.message', lambda value: value.id) def message_post(self, **kwargs): # OVERRIDE diff --git a/addons/l10n_it_edi/__manifest__.py b/addons/l10n_it_edi/__manifest__.py index 917dc15c231..a119942594c 100644 --- a/addons/l10n_it_edi/__manifest__.py +++ b/addons/l10n_it_edi/__manifest__.py @@ -7,6 +7,7 @@ 'depends': [ 'l10n_it', 'fetchmail', + 'account_edi' ], 'author': 'Odoo', 'description': """ @@ -16,6 +17,7 @@ E-invoice implementation 'website': 'http://www.odoo.com/', 'data': [ 'security/ir.model.access.csv', + 'data/account_edi_data.xml', 'data/invoice_it_template.xml', 'views/l10n_it_view.xml', ], diff --git a/addons/l10n_it_edi/data/account_edi_data.xml b/addons/l10n_it_edi/data/account_edi_data.xml new file mode 100644 index 00000000000..9dbd1dec98d --- /dev/null +++ b/addons/l10n_it_edi/data/account_edi_data.xml @@ -0,0 +1,12 @@ + + + + + + Fattura PA (IT) + fattura_pa + import + + + + \ No newline at end of file diff --git a/addons/l10n_it_edi/models/__init__.py b/addons/l10n_it_edi/models/__init__.py index a7b2d212437..e2f3f69717a 100644 --- a/addons/l10n_it_edi/models/__init__.py +++ b/addons/l10n_it_edi/models/__init__.py @@ -4,5 +4,6 @@ from . import res_partner from . import res_company from . import account_invoice +from . import account_edi_format from . import ir_mail_server from . import ddt diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py new file mode 100644 index 00000000000..ab2d7230407 --- /dev/null +++ b/addons/l10n_it_edi/models/account_edi_format.py @@ -0,0 +1,421 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields, _ +from odoo.tests.common import Form +from odoo.exceptions import UserError +from odoo.tools import float_repr + +import re +from datetime import date, datetime +import logging +import base64 + + +_logger = logging.getLogger(__name__) + +DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d' + + +class AccountEdiFormat(models.Model): + _inherit = 'account.edi.format' + + def _check_filename_is_fattura_pa(self, filename): + return re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", filename) + + def _is_fattura_pa(self, filename, tree): + return self.code == 'fattura_pa' and self._check_filename_is_fattura_pa(filename) + + def _create_invoice_from_xml_tree(self, filename, tree): + self.ensure_one() + if self._is_fattura_pa(filename, tree): + return self._import_fattura_pa(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_fattura_pa(filename, tree): + if len(tree.xpath('//FatturaElettronicaBody')) > 1: + invoice.message_post(body='The attachment contains multiple invoices, this invoice was not updated from it.', + message_type='comment', + subtype_xmlid='mail.mt_note', + author_id=self.env.ref('base.partner_root').id) + else: + return self._import_fattura_pa(tree, invoice) + return super()._update_invoice_from_xml_tree(filename, tree, invoice) + + def _import_fattura_pa(self, tree, invoice): + """ Decodes a fattura_pa invoice into an invoice. + + :param tree: the fattura_pa tree to decode. + :param invoice: the invoice to update or an empty recordset. + :returns: the invoice where the fattura_pa data was imported. + """ + invoices = self.env['account.move'] + first_run = True + + # possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch + for body_tree in tree.xpath('//FatturaElettronicaBody'): + if not first_run or not invoice: + # make sure all the iterations create a new invoice record (except the first which could have already created one) + invoice = self.env['account.move'] + first_run = False + + elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento') + if elements and elements[0].text and elements[0].text == 'TD01': + self_ctx = invoice.with_context(default_move_type='in_invoice') + elif elements and elements[0].text and elements[0].text == 'TD04': + self_ctx = invoice.with_context(default_move_type='in_refund') + else: + _logger.info(_('Document type not managed: %s.') % (elements[0].text)) + + # type must be present in the context to get the right behavior of the _default_journal method (account.move). + # journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line). + + elements = tree.xpath('//CessionarioCommittente//IdCodice') + company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1) + if not company: + elements = tree.xpath('//CessionarioCommittente//CodiceFiscale') + company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1) + + if company: + self_ctx = self_ctx.with_context(company_id=company.id) + else: + company = self.env.company + if elements: + _logger.info(_('Company not found with codice fiscale: %s. The company\'s user is set by default.') % elements[0].text) + else: + _logger.info(_('Company not found. The company\'s user is set by default.')) + + if not self.env.is_superuser(): + if self.env.company != company: + raise UserError(_("You can only import invoice concern your current company: %s") % self.env.company.display_name) + + # Refund type. + # TD01 == invoice + # TD02 == advance/down payment on invoice + # TD03 == advance/down payment on fee + # TD04 == credit note + # TD05 == debit note + # TD06 == fee + elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento') + if elements and elements[0].text and elements[0].text == 'TD01': + move_type = 'in_invoice' + elif elements and elements[0].text and elements[0].text == 'TD04': + move_type = 'in_refund' + # move could be a single record (editing) or be empty (new). + with Form(invoice.with_context(default_move_type=move_type)) as invoice_form: + message_to_log = [] + + # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2> + elements = tree.xpath('//CedentePrestatore//IdCodice') + partner = elements and self.env['res.partner'].search(['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if not partner: + elements = tree.xpath('//CedentePrestatore//CodiceFiscale') + partner = elements and self.env['res.partner'].search(['&', ('l10n_it_codice_fiscale', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if not partner: + elements = tree.xpath('//DatiTrasmissione//Email') + partner = elements and self.env['res.partner'].search(['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) + if partner: + invoice_form.partner_id = partner + else: + message_to_log.append("%s
%s" % ( + _("Vendor not found, useful informations from XML file:"), + invoice._compose_info_message( + tree, './/CedentePrestatore'))) + + # Numbering attributed by the transmitter. <1.1.2> + elements = tree.xpath('//ProgressivoInvio') + if elements: + invoice_form.payment_reference = elements[0].text + + elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero') + if elements: + invoice_form.ref = elements[0].text + + # Currency. <2.1.1.2> + elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa') + if elements: + currency_str = elements[0].text + 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 + + # Date. <2.1.1.3> + elements = body_tree.xpath('.//DatiGeneraliDocumento/Data') + if elements: + date_str = elements[0].text + date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + invoice_form.invoice_date = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + + # Dati Bollo. <2.1.1.6> + elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo') + if elements: + invoice_form.l10n_it_stamp_duty = float(elements[0].text) + + # List of all amount discount (will be add after all article to avoid to have a negative sum) + discount_list = [] + percentage_global_discount = 1.0 + + # Global discount. <2.1.1.8> + discount_elements = body_tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione') + total_discount_amount = 0.0 + if discount_elements: + for discount_element in discount_elements: + discount_line = discount_element.xpath('.//Tipo') + discount_sign = -1 + if discount_line and discount_line[0].text == 'SC': + discount_sign = 1 + discount_percentage = discount_element.xpath('.//Percentuale') + if discount_percentage and discount_percentage[0].text: + percentage_global_discount *= 1 - float(discount_percentage[0].text)/100 * discount_sign + + discount_amount_text = discount_element.xpath('.//Importo') + if discount_amount_text and discount_amount_text[0].text: + discount_amount = float(discount_amount_text[0].text) * discount_sign * -1 + discount = {} + discount["seq"] = 0 + + if discount_amount < 0: + discount["name"] = _('GLOBAL DISCOUNT') + else: + discount["name"] = _('GLOBAL EXTRA CHARGE') + discount["amount"] = discount_amount + discount["tax"] = [] + discount_list.append(discount) + + # Comment. <2.1.1.11> + elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale') + for element in elements: + invoice_form.narration = '%s%s\n' % (invoice_form.narration or '', element.text) + + # Informations relative to the purchase order, the contract, the agreement, + # the reception phase or invoices previously transmitted + # <2.1.2> - <2.1.6> + for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']: + elements = body_tree.xpath('.//DatiGenerali/' + document_type) + if elements: + for element in elements: + message_to_log.append("%s %s
%s" % (document_type, _("from XML file:"), + invoice._compose_info_message(element, '.'))) + + # Dati DDT. <2.1.8> + elements = body_tree.xpath('.//DatiGenerali/DatiDDT') + if elements: + message_to_log.append("%s
%s" % ( + _("Transport informations from XML file:"), + invoice._compose_info_message(body_tree, './/DatiGenerali/DatiDDT'))) + + # Due date. <2.4.2.5> + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento') + if elements: + date_str = elements[0].text + date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + invoice_form.invoice_date_due = fields.Date.to_string(date_obj) + + # Total amount. <2.4.2.6> + elements = body_tree.xpath('.//ImportoPagamento') + amount_total_import = 0 + for element in elements: + amount_total_import += float(element.text) + if amount_total_import: + message_to_log.append(_("Total amount from the XML File: %s") % ( + amount_total_import)) + + # Bank account. <2.4.2.13> + if invoice_form.move_type not in ('out_invoice', 'in_refund'): + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN') + if elements: + if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id: + bank = self.env['res.partner.bank'].search([ + ('acc_number', '=', elements[0].text), + ('partner_id.id', '=', invoice_form.partner_id.commercial_partner_id.id) + ]) + else: + bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)]) + if bank: + invoice_form.partner_bank_id = bank + else: + message_to_log.append("%s
%s" % ( + _("Bank account not found, useful informations from XML file:"), + invoice._compose_multi_info_message( + body_tree, ['.//DatiPagamento//Beneficiario', + './/DatiPagamento//IstitutoFinanziario', + './/DatiPagamento//IBAN', + './/DatiPagamento//ABI', + './/DatiPagamento//CAB', + './/DatiPagamento//BIC', + './/DatiPagamento//ModalitaPagamento']))) + else: + elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento') + if elements: + message_to_log.append("%s
%s" % ( + _("Bank account not found, useful informations from XML file:"), + self._compose_info_message(body_tree, './/DatiPagamento'))) + + # Invoice lines. <2.2.1> + elements = body_tree.xpath('.//DettaglioLinee') + if elements: + for element in elements: + with invoice_form.invoice_line_ids.new() as invoice_line_form: + + # Sequence. + line_elements = element.xpath('.//NumeroLinea') + if line_elements: + invoice_line_form.sequence = int(line_elements[0].text) * 2 + + # Product. + line_elements = element.xpath('.//Descrizione') + if line_elements: + invoice_line_form.name = " ".join(line_elements[0].text.split()) + + elements_code = element.xpath('.//CodiceArticolo') + if elements_code: + for element_code in elements_code: + type_code = element_code.xpath('.//CodiceTipo')[0] + code = element_code.xpath('.//CodiceValore')[0] + if type_code.text == 'EAN': + product = self.env['product.product'].search([('barcode', '=', code.text)]) + if product: + invoice_line_form.product_id = product + break + if partner: + product_supplier = self.env['product.supplierinfo'].search([('name', '=', partner.id), ('product_code', '=', code.text)]) + if product_supplier and product_supplier.product_id: + invoice_line_form.product_id = product_supplier.product_id + break + if not invoice_line_form.product_id: + for element_code in elements_code: + code = element_code.xpath('.//CodiceValore')[0] + product = self.env['product.product'].search([('default_code', '=', code.text)]) + if product: + invoice_line_form.product_id = product + break + + # Price Unit. + line_elements = element.xpath('.//PrezzoUnitario') + if line_elements: + invoice_line_form.price_unit = float(line_elements[0].text) + + # Quantity. + line_elements = element.xpath('.//Quantita') + if line_elements: + invoice_line_form.quantity = float(line_elements[0].text) + else: + invoice_line_form.quantity = 1 + + # Taxes + tax_element = element.xpath('.//AliquotaIVA') + natura_element = element.xpath('.//Natura') + invoice_line_form.tax_ids.clear() + if tax_element and tax_element[0].text: + percentage = float(tax_element[0].text) + if natura_element and natura_element[0].text: + l10n_it_kind_exoneration = natura_element[0].text + tax = self.env['account.tax'].search([ + ('company_id', '=', invoice_form.company_id.id), + ('amount_type', '=', 'percent'), + ('type_tax_use', '=', 'purchase'), + ('amount', '=', percentage), + ('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration), + ], limit=1) + else: + tax = self.env['account.tax'].search([ + ('company_id', '=', invoice_form.company_id.id), + ('amount_type', '=', 'percent'), + ('type_tax_use', '=', 'purchase'), + ('amount', '=', percentage), + ], limit=1) + l10n_it_kind_exoneration = '' + + if tax: + invoice_line_form.tax_ids.add(tax) + else: + if l10n_it_kind_exoneration: + message_to_log.append(_("Tax not found with percentage: %s and exoneration %s for the article: %s") % ( + percentage, + l10n_it_kind_exoneration, + invoice_line_form.name)) + else: + message_to_log.append(_("Tax not found with percentage: %s for the article: %s") % ( + percentage, + invoice_line_form.name)) + + # Discount in cascade mode. + # if 3 discounts : -10% -50€ -20% + # the result must be : (((price -10%)-50€) -20%) + # Generic form : (((price -P1%)-A1€) -P2%) + # It will be split in two parts: fix amount and pourcent amount + # example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€) + # pourcent: 1-(1-P2)*(1-P5) + # fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all + # percentage present after the fix amount) + line_elements = element.xpath('.//ScontoMaggiorazione') + total_discount_amount = 0.0 + total_discount_percentage = percentage_global_discount + if line_elements: + for line_element in line_elements: + discount_line = line_element.xpath('.//Tipo') + discount_sign = -1 + if discount_line and discount_line[0].text == 'SC': + discount_sign = 1 + discount_percentage = line_element.xpath('.//Percentuale') + if discount_percentage and discount_percentage[0].text: + pourcentage_actual = 1 - float(discount_percentage[0].text)/100 * discount_sign + total_discount_percentage *= pourcentage_actual + total_discount_amount *= pourcentage_actual + + discount_amount = line_element.xpath('.//Importo') + if discount_amount and discount_amount[0].text: + total_discount_amount += float(discount_amount[0].text) * discount_sign * -1 + + # Save amount discount. + if total_discount_amount != 0: + discount = {} + discount["seq"] = invoice_line_form.sequence + 1 + + if total_discount_amount < 0: + discount["name"] = _('DISCOUNT: ') + invoice_line_form.name + else: + discount["name"] = _('EXTRA CHARGE: ') + invoice_line_form.name + discount["amount"] = total_discount_amount + discount["tax"] = [] + for tax in invoice_line_form.tax_ids: + discount["tax"].append(tax) + discount_list.append(discount) + invoice_line_form.discount = (1 - total_discount_percentage) * 100 + + # Apply amount discount. + for discount in discount_list: + with invoice_form.invoice_line_ids.new() as invoice_line_form_discount: + invoice_line_form_discount.tax_ids.clear() + invoice_line_form_discount.sequence = discount["seq"] + invoice_line_form_discount.name = discount["name"] + invoice_line_form_discount.price_unit = discount["amount"] + + new_invoice = invoice_form.save() + new_invoice.l10n_it_send_state = "other" + + elements = body_tree.xpath('.//Allegati') + if elements: + for element in elements: + name_attachment = element.xpath('.//NomeAttachment')[0].text + attachment_64 = str.encode(element.xpath('.//Attachment')[0].text) + attachment_64 = self.env['ir.attachment'].create({ + 'name': name_attachment, + 'datas': attachment_64, + 'type': 'binary', + }) + + # default_res_id is had to context to avoid facturx to import his content + # no_new_invoice to prevent from looping on the message_post that would create a new invoice without it + new_invoice.with_context(no_new_invoice=True, default_res_id=new_invoice.id).message_post( + body=(_("Attachment from XML")), + attachment_ids=[attachment_64.id] + ) + + for message in message_to_log: + new_invoice.message_post(body=message) + + invoices += new_invoice + return invoices diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py index c511de477d8..62e40420247 100644 --- a/addons/l10n_it_edi/models/account_invoice.py +++ b/addons/l10n_it_edi/models/account_invoice.py @@ -43,9 +43,17 @@ class AccountMove(models.Model): l10n_it_ddt_id = fields.Many2one('l10n_it.ddt', string='DDT', readonly=True, states={'draft': [('readonly', False)]}, copy=False) - l10n_it_einvoice_name = fields.Char(readonly=True, copy=False) + l10n_it_einvoice_name = fields.Char(compute='_compute_l10n_it_einvoice') - l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", copy=False) + l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", compute='_compute_l10n_it_einvoice') + + @api.depends('edi_document_ids') + def _compute_l10n_it_einvoice(self): + fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA') + for invoice in self: + einvoice = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == fattura_pa) + invoice.l10n_it_einvoice_id = einvoice + invoice.l10n_it_einvoice_name = einvoice.name def post(self): # OVERRIDE @@ -158,17 +166,17 @@ class AccountMove(models.Model): 'codice': invoice.company_id.l10n_it_codice_fiscale, 'progressive_number': progressive_number.zfill(5), } - invoice.l10n_it_einvoice_name = report_name data = b"" + invoice._export_as_xml() description = _('Italian invoice: %s') % invoice.move_type - invoice.l10n_it_einvoice_id = self.env['ir.attachment'].create({ + invoice.edi_document_ids = self.env['ir.attachment'].create({ 'name': report_name, 'res_id': invoice.id, 'res_model': invoice._name, 'datas': base64.encodebytes(data), 'description': description, 'type': 'binary', + 'edi_format_id': self.env.ref('l10n_it_edi.edi_fatturaPA').id }) invoice.message_post( @@ -276,8 +284,8 @@ class AccountMove(models.Model): raise UserError(_("%s isn't in a right state. It must be in a 'Not yet send' or 'Invalid' state.") % (self.display_name)) message = self.env['mail.message'].create({ - 'subject': _('Sending file: %s') % (self.l10n_it_einvoice_id.name), - 'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_id.name, self.env.company.l10n_it_address_recipient_fatturapa), + 'subject': _('Sending file: %s') % (self.l10n_it_einvoice_name), + 'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_name, self.env.company.l10n_it_address_recipient_fatturapa), 'author_id': self.env.user.partner_id.id, 'email_from': self.env.company.l10n_it_address_send_fatturapa, 'reply_to': self.env.company.l10n_it_address_send_fatturapa, @@ -301,382 +309,6 @@ class AccountMove(models.Model): ) self.l10n_it_send_state = 'invalid' - def _import_xml_invoice(self, tree): - ''' Extract invoice values from the E-Invoice xml tree passed as parameter. - - :param content: The tree of the xml file. - :return: A dictionary containing account.invoice values to create/update it. - ''' - - invoices = self.env['account.move'] - multi = False - - # possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch - for body_tree in tree.xpath('//FatturaElettronicaBody'): - if multi: - # make sure all the iterations create a new invoice record (except the first which could have already created one) - self = self.env['account.move'] - multi = True - - elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento') - if elements and elements[0].text and elements[0].text == 'TD01': - self_ctx = self.with_context(default_move_type='in_invoice') - elif elements and elements[0].text and elements[0].text == 'TD04': - self_ctx = self.with_context(default_move_type='in_refund') - else: - _logger.info(_('Document type not managed: %s.') % (elements[0].text)) - - # type must be present in the context to get the right behavior of the _default_journal method (account.move). - # journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line). - - elements = tree.xpath('//CessionarioCommittente//IdCodice') - company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1) - if not company: - elements = tree.xpath('//CessionarioCommittente//CodiceFiscale') - company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1) - - if company: - self_ctx = self_ctx.with_context(company_id=company.id) - else: - company = self.env.company - if elements: - _logger.info(_('Company not found with codice fiscale: %s. The company\'s user is set by default.') % elements[0].text) - else: - _logger.info(_('Company not found. The company\'s user is set by default.')) - - if not self.env.is_superuser(): - if self.env.company != company: - raise UserError(_("You can only import invoice concern your current company: %s") % self.env.company.display_name) - - # Refund type. - # TD01 == invoice - # TD02 == advance/down payment on invoice - # TD03 == advance/down payment on fee - # TD04 == credit note - # TD05 == debit note - # TD06 == fee - elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento') - if elements and elements[0].text and elements[0].text == 'TD01': - type = 'in_invoice' - elif elements and elements[0].text and elements[0].text == 'TD04': - type = 'in_refund' - # self could be a single record (editing) or be empty (new). - with Form(self.with_context(default_move_type=type)) as invoice_form: - message_to_log = [] - - # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2> - elements = tree.xpath('//CedentePrestatore//IdCodice') - partner = elements and self.env['res.partner'].search(['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) - if not partner: - elements = tree.xpath('//CedentePrestatore//CodiceFiscale') - partner = elements and self.env['res.partner'].search(['&', ('l10n_it_codice_fiscale', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) - if not partner: - elements = tree.xpath('//DatiTrasmissione//Email') - partner = elements and self.env['res.partner'].search(['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1) - if partner: - invoice_form.partner_id = partner - else: - message_to_log.append("%s
%s" % ( - _("Vendor not found, useful informations from XML file:"), - self._compose_info_message( - tree, './/CedentePrestatore'))) - - # Numbering attributed by the transmitter. <1.1.2> - elements = tree.xpath('//ProgressivoInvio') - if elements: - invoice_form.payment_reference = elements[0].text - - elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero') - if elements: - invoice_form.ref = elements[0].text - - # Currency. <2.1.1.2> - elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa') - if elements: - currency_str = elements[0].text - 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 - - # Date. <2.1.1.3> - elements = body_tree.xpath('.//DatiGeneraliDocumento/Data') - if elements: - date_str = elements[0].text - date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) - invoice_form.invoice_date = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) - - # Dati Bollo. <2.1.1.6> - elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo') - if elements: - invoice_form.l10n_it_stamp_duty = float(elements[0].text) - - # List of all amount discount (will be add after all article to avoid to have a negative sum) - discount_list = [] - percentage_global_discount = 1.0 - - # Global discount. <2.1.1.8> - discount_elements = body_tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione') - total_discount_amount = 0.0 - if discount_elements: - for discount_element in discount_elements: - discount_line = discount_element.xpath('.//Tipo') - discount_sign = -1 - if discount_line and discount_line[0].text == 'SC': - discount_sign = 1 - discount_percentage = discount_element.xpath('.//Percentuale') - if discount_percentage and discount_percentage[0].text: - percentage_global_discount *= 1 - float(discount_percentage[0].text)/100 * discount_sign - - discount_amount_text = discount_element.xpath('.//Importo') - if discount_amount_text and discount_amount_text[0].text: - discount_amount = float(discount_amount_text[0].text) * discount_sign * -1 - discount = {} - discount["seq"] = 0 - - if discount_amount < 0: - discount["name"] = _('GLOBAL DISCOUNT') - else: - discount["name"] = _('GLOBAL EXTRA CHARGE') - discount["amount"] = discount_amount - discount["tax"] = [] - discount_list.append(discount) - - # Comment. <2.1.1.11> - elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale') - for element in elements: - invoice_form.narration = '%s%s\n' % (invoice_form.narration or '', element.text) - - # Informations relative to the purchase order, the contract, the agreement, - # the reception phase or invoices previously transmitted - # <2.1.2> - <2.1.6> - for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']: - elements = body_tree.xpath('.//DatiGenerali/' + document_type) - if elements: - for element in elements: - message_to_log.append("%s %s
%s" % (document_type, _("from XML file:"), - self._compose_info_message(element, '.'))) - - # Dati DDT. <2.1.8> - elements = body_tree.xpath('.//DatiGenerali/DatiDDT') - if elements: - message_to_log.append("%s
%s" % ( - _("Transport informations from XML file:"), - self._compose_info_message(body_tree, './/DatiGenerali/DatiDDT'))) - - # Due date. <2.4.2.5> - elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento') - if elements: - date_str = elements[0].text - date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) - invoice_form.invoice_date_due = fields.Date.to_string(date_obj) - - # Total amount. <2.4.2.6> - elements = body_tree.xpath('.//ImportoPagamento') - amount_total_import = 0 - for element in elements: - amount_total_import += float(element.text) - if amount_total_import: - message_to_log.append(_("Total amount from the XML File: %s") % ( - amount_total_import)) - - # Bank account. <2.4.2.13> - if invoice_form.type not in ('out_invoice', 'in_refund'): - elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN') - if elements: - if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id: - bank = self.env['res.partner.bank'].search([ - ('acc_number', '=', elements[0].text), - ('partner_id.id', '=', invoice_form.partner_id.commercial_partner_id.id) - ]) - else: - bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)]) - if bank: - invoice_form.partner_bank_id = bank - else: - message_to_log.append("%s
%s" % ( - _("Bank account not found, useful informations from XML file:"), - self._compose_multi_info_message( - body_tree, ['.//DatiPagamento//Beneficiario', - './/DatiPagamento//IstitutoFinanziario', - './/DatiPagamento//IBAN', - './/DatiPagamento//ABI', - './/DatiPagamento//CAB', - './/DatiPagamento//BIC', - './/DatiPagamento//ModalitaPagamento']))) - else: - elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento') - if elements: - message_to_log.append("%s
%s" % ( - _("Bank account not found, useful informations from XML file:"), - self._compose_info_message(body_tree, './/DatiPagamento'))) - - # Invoice lines. <2.2.1> - elements = body_tree.xpath('.//DettaglioLinee') - if elements: - for element in elements: - with invoice_form.invoice_line_ids.new() as invoice_line_form: - - # Sequence. - line_elements = element.xpath('.//NumeroLinea') - if line_elements: - invoice_line_form.sequence = int(line_elements[0].text) * 2 - - # Product. - line_elements = element.xpath('.//Descrizione') - if line_elements: - invoice_line_form.name = " ".join(line_elements[0].text.split()) - - elements_code = element.xpath('.//CodiceArticolo') - if elements_code: - for element_code in elements_code: - type_code = element_code.xpath('.//CodiceTipo')[0] - code = element_code.xpath('.//CodiceValore')[0] - if type_code.text == 'EAN': - product = self.env['product.product'].search([('barcode', '=', code.text)]) - if product: - invoice_line_form.product_id = product - break - if partner: - product_supplier = self.env['product.supplierinfo'].search([('name', '=', partner.id), ('product_code', '=', code.text)]) - if product_supplier and product_supplier.product_id: - invoice_line_form.product_id = product_supplier.product_id - break - if not invoice_line_form.product_id: - for element_code in elements_code: - code = element_code.xpath('.//CodiceValore')[0] - product = self.env['product.product'].search([('default_code', '=', code.text)]) - if product: - invoice_line_form.product_id = product - break - - # Price Unit. - line_elements = element.xpath('.//PrezzoUnitario') - if line_elements: - invoice_line_form.price_unit = float(line_elements[0].text) - - # Quantity. - line_elements = element.xpath('.//Quantita') - if line_elements: - invoice_line_form.quantity = float(line_elements[0].text) - else: - invoice_line_form.quantity = 1 - - # Taxes - tax_element = element.xpath('.//AliquotaIVA') - natura_element = element.xpath('.//Natura') - invoice_line_form.tax_ids.clear() - if tax_element and tax_element[0].text: - percentage = float(tax_element[0].text) - if natura_element and natura_element[0].text: - l10n_it_kind_exoneration = natura_element[0].text - tax = self.env['account.tax'].search([ - ('company_id', '=', invoice_form.company_id.id), - ('amount_type', '=', 'percent'), - ('type_tax_use', '=', 'purchase'), - ('amount', '=', percentage), - ('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration), - ], limit=1) - else: - tax = self.env['account.tax'].search([ - ('company_id', '=', invoice_form.company_id.id), - ('amount_type', '=', 'percent'), - ('type_tax_use', '=', 'purchase'), - ('amount', '=', percentage), - ], limit=1) - l10n_it_kind_exoneration = '' - - if tax: - invoice_line_form.tax_ids.add(tax) - else: - if l10n_it_kind_exoneration: - message_to_log.append(_("Tax not found with percentage: %s and exoneration %s for the article: %s") % ( - percentage, - l10n_it_kind_exoneration, - invoice_line_form.name)) - else: - message_to_log.append(_("Tax not found with percentage: %s for the article: %s") % ( - percentage, - invoice_line_form.name)) - - # Discount in cascade mode. - # if 3 discounts : -10% -50€ -20% - # the result must be : (((price -10%)-50€) -20%) - # Generic form : (((price -P1%)-A1€) -P2%) - # It will be split in two parts: fix amount and pourcent amount - # example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€) - # pourcent: 1-(1-P2)*(1-P5) - # fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all - # percentage present after the fix amount) - line_elements = element.xpath('.//ScontoMaggiorazione') - total_discount_amount = 0.0 - total_discount_percentage = percentage_global_discount - if line_elements: - for line_element in line_elements: - discount_line = line_element.xpath('.//Tipo') - discount_sign = -1 - if discount_line and discount_line[0].text == 'SC': - discount_sign = 1 - discount_percentage = line_element.xpath('.//Percentuale') - if discount_percentage and discount_percentage[0].text: - pourcentage_actual = 1 - float(discount_percentage[0].text)/100 * discount_sign - total_discount_percentage *= pourcentage_actual - total_discount_amount *= pourcentage_actual - - discount_amount = line_element.xpath('.//Importo') - if discount_amount and discount_amount[0].text: - total_discount_amount += float(discount_amount[0].text) * discount_sign * -1 - - # Save amount discount. - if total_discount_amount != 0: - discount = {} - discount["seq"] = invoice_line_form.sequence + 1 - - if total_discount_amount < 0: - discount["name"] = _('DISCOUNT: ') + invoice_line_form.name - else: - discount["name"] = _('EXTRA CHARGE: ') + invoice_line_form.name - discount["amount"] = total_discount_amount - discount["tax"] = [] - for tax in invoice_line_form.tax_ids: - discount["tax"].append(tax) - discount_list.append(discount) - invoice_line_form.discount = (1 - total_discount_percentage) * 100 - - # Apply amount discount. - for discount in discount_list: - with invoice_form.invoice_line_ids.new() as invoice_line_form_discount: - invoice_line_form_discount.tax_ids.clear() - invoice_line_form_discount.sequence = discount["seq"] - invoice_line_form_discount.name = discount["name"] - invoice_line_form_discount.price_unit = discount["amount"] - - new_invoice = invoice_form.save() - new_invoice.l10n_it_send_state = "other" - - elements = body_tree.xpath('.//Allegati') - if elements: - for element in elements: - name_attachment = element.xpath('.//NomeAttachment')[0].text - attachment_64 = str.encode(element.xpath('.//Attachment')[0].text) - attachment_64 = self.env['ir.attachment'].create({ - 'name': name_attachment, - 'datas': attachment_64, - 'type': 'binary', - }) - - # default_res_id is had to context to avoid facturx to import his content - # no_new_invoice to prevent from looping on the message_post that would create a new invoice without it - new_invoice.with_context(no_new_invoice=True, default_res_id=new_invoice.id).message_post( - body=(_("Attachment from XML")), - attachment_ids=[attachment_64.id] - ) - - for message in message_to_log: - new_invoice.message_post(body=message) - - invoices += new_invoice - return invoices - def _compose_info_message(self, tree, element_tags): output_str = "" elements = tree.xpath(element_tags) @@ -703,20 +335,6 @@ class AccountMove(models.Model): output_str += "
  • %s: %s
  • " % (element.tag, text) return output_str + "" - @api.model - def _get_xml_decoders(self): - # Override - ubl_decoders = [('Italy', self._detect_italy_edi, self._import_xml_invoice)] - return super(AccountMove, self)._get_xml_decoders() + ubl_decoders - - @api.model - def _detect_italy_edi(self, tree, file_name): - # Quick check the file name looks like the one of an Italian EDI XML. - flag = re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", file_name) - error = None - - return {'flag': flag, 'error': error} - class AccountTax(models.Model): _name = "account.tax" _inherit = "account.tax" diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py index 24ccd392a8e..8830af16b93 100644 --- a/addons/l10n_it_edi/models/ir_mail_server.py +++ b/addons/l10n_it_edi/models/ir_mail_server.py @@ -140,7 +140,7 @@ class FetchmailServer(models.Model): except Exception: raise UserError(_('The xml file is badly formatted : {}').format(att_name)) - invoice = self.env['account.move']._import_xml_invoice(tree) + invoice = self.env.ref('l10n_it_edi.edi_fatturaPA')._create_invoice_from_xml_tree(att_name, tree) invoice.l10n_it_send_state = "new" invoice.source_email = from_address self._cr.commit()