diff --git a/account_invoice_import_invoice2data/README.rst b/account_invoice_import_invoice2data/README.rst new file mode 100644 index 0000000000..d1de026da8 --- /dev/null +++ b/account_invoice_import_invoice2data/README.rst @@ -0,0 +1,297 @@ +=================================== +Account Invoice Import Invoice2data +=================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c7d1d2a38227a9bf81bc6132091b5285a4afe0b1280bea7541ebab212f57f16a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github + :target: https://github.com/OCA/edi/tree/16.0/account_invoice_import_invoice2data + :alt: OCA/edi +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-16-0/edi-16-0-account_invoice_import_invoice2data + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is an extension of the module *account_invoice_import*: it adds support for regular PDF invoices i.e. PDF invoice that don't have an embedded XML file. It uses the `invoice2data library `_ which takes care of extracting the text of the PDF invoice, find an existing invoice template and execute the invoice template to extract the useful information from the invoice. + +To know the full story behind the development of this module, read this `blog post `_. + +More information for creating the templates can be found in `tutorial of the invoice2data library `_. The templates have to be created manually. An graphical template creator for odoo is a work in progress. + +**WARNING**: an alternative module **account_invoice_import_simple_pdf** developped in July 2021 provides similar features but has one big advantage: the accountant can add support for a new vendor by himself, no more invoice templates which require technical skill. The module *account_invoice_import_simple_pdf* provides basic functionality, but does not support line level accounting. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This module requires the Python library *invoice2data* available on `Github `_ with a version >= 0.2.74 (February 2018). + +To install the latest version of this library, run: + +.. code:: + + sudo pip3 install --upgrade invoice2data + +If you use Ubuntu 16.04 LTS or higher, you can use the pdftotext version 0.41.0 that is packaged in the distribution: + +.. code:: + + sudo apt install poppler-utils + +If you want the invoice2data library to support mixed-type pdf's or fallback on OCR if the PDF doesn't contain text (only a very small minority of PDF invoices are image-based and require OCR) like scanned receipts, you should also install `Ocrmypdf `_ + +.. code:: + + pip install -U ocrmypdf + +If you want the invoice2data library to fallback on OCR if the PDF doesn't contain text (only a very small minority of PDF invoices are image-based and require OCR) like scanned receipts, you should also install `Imagemagick `_ (to get the *convert* utility to convert PDF to TIFF) and `Tesseract OCR `_ : + +.. code:: + + sudo apt install imagemagick tesseract-ocr + +If you want to use custom invoice templates for the invoice2data lib (in addition to the templates provided by the invoice2data lib), you should add a line in your Odoo server configuration file such as: + +.. code:: + + invoice2data_templates_dir = /opt/invoice2data_local_templates + +and store your invoice templates in YAML format (*.yml* extension) or json format in the directory that you have configured above. If you add invoice tempates in this directory, you don't have to restart Odoo, they will be used automatically on the next invoice import. + +If you want to use only your custom invoice templates and ignore the templates provided by the invoice2data lib, you should have in your Odoo server configuration file: + +.. code:: + + invoice2data_templates_dir = /opt/invoice2data_local_templates + invoice2data_exclude_built_in_templates = True + +The yaml templates are loaded with [pyyaml](https://github.com/yaml/pyyaml) which is a pure python implementation. (thus rather slow) +As an alternative json templates can be used. Which are natively better supported by python. The performance with yaml templates can be greatly increased **10x** by using [libyaml](https://github.com/yaml/libyaml) +It can be installed on most distributions by: + +.. code:: + + sudo apt-get libyaml-dev + +French users should also install the module *l10n_fr_business_document_import* available in the `French localization `_. +Dutch users should also install the module *l10n_nl_business_document_import* available in the `Netherlands localization `_. + +Dependencies +============ +| Name | Requirement | Installation | Description | +| -------------- | :---------: | :---------: | :-------------------------------------- | +| [invoice2data](https://github.com/invoice-x/invoice2data) | required | `pip install invoice2data` | The main dependency of this invoice import module | +| [poppler-utils](https://poppler.freedesktop.org/) | required | `apt install poppler-utils` | The default Input-reader for the invoice2data library | +| [dateparser](https://dateparser.readthedocs.io/en/latest/#) | required | `pip install dateparser` | Requirement for parsing the invoice dates, this requirement is likely already satisfied by odoo itself | +| [libyaml](https://github.com/yaml/libyaml) | optional | `apt install libyaml-dev` | Template loader, recomended to greatly speedup the loading of yaml templates | +| [imagemagick](https://www.imagemagick.org) | optional | `apt install imagemagick` | inputreader: Pre-processes the pdf before feeding it into tesseract-ocr | +| [tesseract-ocr](https://github.com/tesseract-ocr/tesseract) | optional | `apt install tesseract-ocr` | inputreader: for ocr of image only pdf files | +| [tesseract-ocr- lang](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html) | optional | `apt install tesseract-ocr-` see documentation | inputreader: Language pack for tesseract ocr, greatly improves character detection | +| [ocrmypdf](https://github.com/ocrmypdf/OCRmyPDF) | optional | `apt install ocrmypdf` | inputreader: For image only or mixed type pdf's. It uses tesseract-ocr under the hood, but provides optimalisations which greatly improves results | + +Configuration +============= + +Go to the form view of the supplier and configure it with the following parameters: + +* the *VAT* is set (the VAT number is used by default when searching the supplier in the Odoo partner database) +* in the *Invoicing* tab, create an *Invoice Import Configuration*. + +For the PDF invoice of your supplier that don't have an embedded XML file, you will have to create a `template file `_ in YAML format in the invoice2data Python library. It is quite easy to do ; if you are familiar with `regexp `_, it should not take more than 10 minutes for each supplier. + +Here are some hints to help you add a template for your supplier: + +* There is a `tutorial in the repo of the invoice2data library `_ + +* Take `Free SAS template file `_ as an example. You will find a sample PDF invoice for this supplier under invoice2data/test/pdf/invoice_free_fiber_201507.pdf + +* Try to run the invoice2data library manually on the sample invoice of Free: + +.. code:: + + % python -m invoice2data.main --debug invoice2data/test/pdf/invoice_free_fiber_201507.pdf + +On the output, you will get first the text of the PDF, then some debug info on the parsing of the invoice and the regexps, and, on the last line, you will have the dict that contain the result of the parsing. + +* if the VAT number of the supplier is present in the text of the PDF invoice, I think it's a good idea to use it as the keyword. It is good practice to add 2 other keywords: one for the language (for example, match on the word *Invoice* in the language of the invoice) and one for the currency, to match only the invoices of that supplier in this particular language and currency. + +* the list of *fields* should contain the following entries: + + * 'vat' with the VAT number of the supplier (if the VAT number of the supplier is not in the text of PDF file, add a 'partner_name' key) + * 'amount' ('amount' is the total amount with taxes) + * 'amount_untaxed' or 'amount_tax' (one or the other, no need for both) + * 'date': the date of the invoice + * 'invoice_number' + * 'date_due', if this information is available in the text of the PDF file. + +The invoice2data library is quite powerfull. It supports multiple input methods (pdftotext, ocrmypdf, tesseract ocr, google cloud vision). +Even invoicelines can be imported and mapped to products in the database. +The invoice2data library does not have a strict standard on field names. This makes the module very flexible, but also hard to create re-usable templates. + +If you want to make use of the advanced features, support for the following fields is implemented. + +## Supported fields + +(note: the fieldname column contains the name to be used in the template file.) + +Partner fields +| fieldname | type | Description | +| -------------- | :---------: | :-------------------------------------- | +| vat | char | The vat code is unique for each partner, it has the highest priority for matching the partner | +| partner_name | char | self explaining | +| partner_street | char | self explaining | +| partner_street2 | char | self explaining | +| partner_street3 | char | self explaining | +| partner_city | char | self explaining | +| partner_zip | char | self explaining | +| country_code | char | use iso format fr or nl | +| state_code | char | use iso format NY (for New York) | +| partner_email | char | self explaining | +| partner_website | char | self explaining | +| telephone | char | can be used for matching the partner with the help of support modules | +| mobile | char | can be used for matching the partner contact with the help of support modules | +| partner_ref | char | reference name or number can be used for partner matching | +| siren | char | French business code, can be used for matching the partner | +| partner_coc | char | General business identiefier number, can be used for matching the partner | + +Invoice Fields (on document level) +| fieldname | type | Description | +| -------------- | :---------: | :-------------------------------------- | +| currency | char | The currency of the invoice in iso format (EUR, USD) | +| currency_symbol | char | The currency symbol of the invoice (€, $) | +| bic | char | Bank Identifier Code | +| iban | char | International Bank Account Number | +| amount | float | The total amount of the invoice (including taxes) | +| amount_untaxed | float | The total amount of the invoice (excluding taxes) | +| amount_tax | float | The sum of the tax amount of the invoice | +| date | date | The date of the invoice | +| invoice_number | char | self explaining | +| date_due | date | The duedate of the invoice | +| date_start | date | The start date of the period for the invoice when the services are delivered. | +| date_end | date | The start date of the period for the invoice when the services are delivered. | +| note | char | The contents of this field will be imported in the chatter. | +| narration | char | The contents of this field will be imported in the narration field. (on the bottom of the invoice.) | +| payment_reference | char | If the invoice is pre-paid an reference can be used for payment reconciliation | +| payment_unece_code | char | The unece code of the payment means according to 4461 code list | +| incoterm | char | The Incoterm 2000 abbrevation | +| company_vat | char | The vat number of the company to which the invoice is addressed to. Used to check if the invoice is actually is adressed to the company which wants to process it. (Very useful in multi-company setup) | +| mandate_id | char | A banking mandate is attached to a bank account and represents an authorization that the bank account owner gives to a company for a specific operation (such as direct debit). | + + +Invoice line Fields +| fieldname | type | Description | +| -------------- | :---------: | :-------------------------------------- | +| name | char | The name of the product, can be used for product matching | +| barcode | char | The the barcode of the product or product package, used for product matching | +| code | char | The (internal) product code, used for product matching | +| qty | float | The amount of items/units | +| unece_code | char | The unece code of the products units of measure can be passed | +| uom | char | The name of the unit of measure, internally if will be mapped to the unece code. Example L will be mapped to unece_code LTR | +| price_unit | float | The unit price of the item. (excluding taxes) | +| discount | float | The amount of discount for this line. Eg 20 for 20% discount or 0.0 for no discount | +| price_total | float | The total amount of the invoice line including taxes. It can be used to select the correct tax tag. | +| price_subtotal | float | The total amount of the invoice line excluding taxes. It can be used to create adjustment lines when the decimal precision is insufficient. | +| line_tax_percent | float | The percentage of tax | +| line_tax_amount | float | The fixed amount of tax applied to the line | +| line_note | char | Notes on the invoice can be imported, There is a special view available. | +| sectionheader | char | There is a special view available for section headers. | +| date_start | date | The start date of the period for the invoice when the services are delivered. | +| date_end | date | The start date of the period for the invoice when the services are delivered. | + +Known issues / Roadmap +====================== + +* Implement support for lines with all tax included, used for some localizations like Switzerland or scanned receipts. +* An graphical template builder. +* Once invoice2data 1.0 is published on PyPI: opt into its new APIs — the input-backend cascade (faster pdfium-first default + automatic fallback), runtime ``ai_fallback=True`` for LLM extraction when no template matches, ``raise_on_error=True`` for typed ``NoTemplateFoundError`` / ``RequiredFieldsMissingError`` instead of ``{}``, and ``pre_process_pdf`` returning the cleaned/smaller PDF so the wizard can re-attach it to the Odoo invoice in place of the raw upload. + +Known Issues +* The input module is hard coded to use pdftotext parser and as a fallback to tesseract. +* Creation of the templates is still quite hard. +* The addres and company specific fields are parsed. Meaning it is possible to import an invoice which is issued to another company than yours! + +Changelog +========= + +16.0.1.0.0 (2025-10-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Migration to 16.0. + +14.0.2.2.0 (2023-03-03) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [ADD] Support for invoicelines. + (`#74 `_) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Alexis de Lattre +* Marwan Behillil + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-alexis-via| image:: https://github.com/alexis-via.png?size=40px + :target: https://github.com/alexis-via + :alt: alexis-via +.. |maintainer-bosd| image:: https://github.com/bosd.png?size=40px + :target: https://github.com/bosd + :alt: bosd + +Current `maintainers `__: + +|maintainer-alexis-via| |maintainer-bosd| + +This module is part of the `OCA/edi `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_invoice_import_invoice2data/__init__.py b/account_invoice_import_invoice2data/__init__.py new file mode 100644 index 0000000000..40272379f7 --- /dev/null +++ b/account_invoice_import_invoice2data/__init__.py @@ -0,0 +1 @@ +from . import wizard diff --git a/account_invoice_import_invoice2data/__manifest__.py b/account_invoice_import_invoice2data/__manifest__.py new file mode 100644 index 0000000000..862ba77a66 --- /dev/null +++ b/account_invoice_import_invoice2data/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2015-2021 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Account Invoice Import Invoice2data", + "version": "16.0.1.0.0", + "category": "Accounting/Accounting", + "license": "AGPL-3", + "summary": "Import supplier invoices using the invoice2data lib", + "author": "Akretion,Odoo Community Association (OCA)", + "maintainers": ["alexis-via", "bosd"], + "website": "https://github.com/OCA/edi", + "depends": ["account_invoice_import"], + "external_dependencies": { + "python": [ + "invoice2data", + "dateparser", + ], + "deb": ["poppler-utils"], + }, + "data": ["wizard/account_invoice_import_view.xml"], + "demo": ["demo/demo_data.xml"], + "installable": True, +} diff --git a/account_invoice_import_invoice2data/demo/demo_data.xml b/account_invoice_import_invoice2data/demo/demo_data.xml new file mode 100644 index 0000000000..e9cd35cb93 --- /dev/null +++ b/account_invoice_import_invoice2data/demo/demo_data.xml @@ -0,0 +1,63 @@ + + + + + Internet Access + + + + service + + + + Free SAS + + 1 + 0 + 8 rue de la Ville l’Évèque + 75008 + Paris + + http://www.free.fr + FR60421938861 + + Fiber optic access at the main office + + + + Beeswax XL + Acme beeswax + 17589684 + 42 + consu + + + + + + Olive Oil + + + + consu + + + 987123 + + + + Luxury Truffles + + + + consu + + + LUX_TRF + + + + diff --git a/account_invoice_import_invoice2data/i18n/account_invoice_import_invoice2data.pot b/account_invoice_import_invoice2data/i18n/account_invoice_import_invoice2data.pot new file mode 100644 index 0000000000..7f1e5b3fcf --- /dev/null +++ b/account_invoice_import_invoice2data/i18n/account_invoice_import_invoice2data.pot @@ -0,0 +1,101 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_import_invoice2data +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \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_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "regular PDF file without any embedded XML file via the" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,description_sale:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,description_sale:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Acme beeswax" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Beeswax XL" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__display_name +msgid "Display Name" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__id +msgid "ID" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.internet_access +#: model:product.template,name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Internet Access" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.luxury_truffles +#: model:product.template,name:account_invoice_import_invoice2data.luxury_truffles_product_template +msgid "Luxury Truffles" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.olive_oil +#: model:product.template,name:account_invoice_import_invoice2data.olive_oil_product_template +msgid "Olive Oil" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#, python-format +msgid "PDF Invoice parsing failed. Error message: %s" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.internet_access +#: model:product.template,uom_name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Units" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model,name:account_invoice_import_invoice2data.model_account_invoice_import +msgid "Wizard to import supplier invoices/refunds" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "invoice2data" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,uom_name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "kg" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "" +"library (reads the text of the PDF and get the information). It will work " +"only if the invoice2data library has a template for the invoices of that " +"supplier." +msgstr "" diff --git a/account_invoice_import_invoice2data/i18n/de.po b/account_invoice_import_invoice2data/i18n/de.po new file mode 100644 index 0000000000..42769fbc03 --- /dev/null +++ b/account_invoice_import_invoice2data/i18n/de.po @@ -0,0 +1,115 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_import_invoice2data +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-05-27 09:19+0000\n" +"Last-Translator: Maria Sparenberg \n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 3.10\n" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "regular PDF file without any embedded XML file via the" +msgstr "reguläre PDF Datei ohne eingebettete XML Datei per" + +#. module: account_invoice_import_invoice2data +#: model:product.product,description_sale:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,description_sale:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Acme beeswax" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Beeswax XL" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__display_name +msgid "Display Name" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__id +msgid "ID" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.internet_access +#: model:product.template,name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Internet Access" +msgstr "Internet Zugang" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.luxury_truffles +#: model:product.template,name:account_invoice_import_invoice2data.luxury_truffles_product_template +msgid "Luxury Truffles" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.olive_oil +#: model:product.template,name:account_invoice_import_invoice2data.olive_oil_product_template +msgid "Olive Oil" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#, python-format +msgid "PDF Invoice parsing failed. Error message: %s" +msgstr "PDF Rechnungsverarbeitung ist gescheitert. Fehler Meldung: %s" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.internet_access +#: model:product.template,uom_name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Units" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model,name:account_invoice_import_invoice2data.model_account_invoice_import +msgid "Wizard to import supplier invoices/refunds" +msgstr "Assistent für den Import von Lieferantenrechnungen / -gutschriften" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "invoice2data" +msgstr "invoice2data" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,uom_name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "kg" +msgstr "kg" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "" +"library (reads the text of the PDF and get the information). It will work " +"only if the invoice2data library has a template for the invoices of that " +"supplier." +msgstr "" +"Bibliothek (liest den Text der PDF und erhält die Informationen). Dies " +"funktioniert nur, wenn die Bibliothek invoice2data eine Vorlage für die " +"Rechnungen dieses Lieferanten enthält." + +#, python-format +#~ msgid "" +#~ "This PDF invoice doesn't match a known template of the invoice2data lib." +#~ msgstr "" +#~ "Diese PDF Rechnung entspricht keinem Template der invoice2data Bibliothek." + +#~ msgid "Unit(s)" +#~ msgstr "Einheit(en)" diff --git a/account_invoice_import_invoice2data/i18n/es.po b/account_invoice_import_invoice2data/i18n/es.po new file mode 100644 index 0000000000..f69c6b55f4 --- /dev/null +++ b/account_invoice_import_invoice2data/i18n/es.po @@ -0,0 +1,118 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_import_invoice2data +# +# Translators: +# enjolras , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-12 01:43+0000\n" +"PO-Revision-Date: 2023-11-23 14:34+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "regular PDF file without any embedded XML file via the" +msgstr "" +"archivo PDF normal sin ningún archivo XML incrustado a través de la " +"función" + +#. module: account_invoice_import_invoice2data +#: model:product.product,description_sale:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,description_sale:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Acme beeswax" +msgstr "Cera de abejas Acme" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Beeswax XL" +msgstr "Cera de abejas XL" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__id +msgid "ID" +msgstr "ID" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.internet_access +#: model:product.template,name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Internet Access" +msgstr "Acceso a internet" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.luxury_truffles +#: model:product.template,name:account_invoice_import_invoice2data.luxury_truffles_product_template +msgid "Luxury Truffles" +msgstr "Trufas de Lujo" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.olive_oil +#: model:product.template,name:account_invoice_import_invoice2data.olive_oil_product_template +msgid "Olive Oil" +msgstr "Aceite de Oliva" + +#. module: account_invoice_import_invoice2data +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#, python-format +msgid "PDF Invoice parsing failed. Error message: %s" +msgstr "Error en el análisis de la factura PDF. Mensaje de error: %s" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.internet_access +#: model:product.template,uom_name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Units" +msgstr "Unidades" + +#. module: account_invoice_import_invoice2data +#: model:ir.model,name:account_invoice_import_invoice2data.model_account_invoice_import +msgid "Wizard to import supplier invoices/refunds" +msgstr "Asistente para importar facturas/reembolsos de proveedores" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "invoice2data" +msgstr "invoice2data" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,uom_name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "kg" +msgstr "kg" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "" +"library (reads the text of the PDF and get the information). It will work " +"only if the invoice2data library has a template for the invoices of that " +"supplier." +msgstr "" +"(lee el texto del PDF y obtiene la información). Sólo funcionará si la " +"biblioteca invoice2data tiene una plantilla para las facturas de ese " +"proveedor." + +#, python-format +#~ msgid "" +#~ "This PDF invoice doesn't match a known template of the invoice2data lib." +#~ msgstr "" +#~ "Esta factura en PDF no coincide con una plantilla conocida de la " +#~ "biblioteca de invoice2data." diff --git a/account_invoice_import_invoice2data/i18n/fr.po b/account_invoice_import_invoice2data/i18n/fr.po new file mode 100644 index 0000000000..2c2ceae733 --- /dev/null +++ b/account_invoice_import_invoice2data/i18n/fr.po @@ -0,0 +1,113 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_import_invoice2data +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-05-29 11:08+0000\n" +"Last-Translator: Claude R Perrin \n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "regular PDF file without any embedded XML file via the" +msgstr "fichier PDF standard sans aucun fichier XML intégré à l’aide de" + +#. module: account_invoice_import_invoice2data +#: model:product.product,description_sale:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,description_sale:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Acme beeswax" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Beeswax XL" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__id +msgid "ID" +msgstr "ID" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.internet_access +#: model:product.template,name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Internet Access" +msgstr "Accès internet" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.luxury_truffles +#: model:product.template,name:account_invoice_import_invoice2data.luxury_truffles_product_template +msgid "Luxury Truffles" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.olive_oil +#: model:product.template,name:account_invoice_import_invoice2data.olive_oil_product_template +msgid "Olive Oil" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#, python-format +msgid "PDF Invoice parsing failed. Error message: %s" +msgstr "L'analyse de la facture PDF a échoué. Message d'erreur : %s" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.internet_access +#: model:product.template,uom_name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Units" +msgstr "Unités" + +#. module: account_invoice_import_invoice2data +#: model:ir.model,name:account_invoice_import_invoice2data.model_account_invoice_import +msgid "Wizard to import supplier invoices/refunds" +msgstr "Assistant pour importer les factures/avoirs des fournisseurs" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "invoice2data" +msgstr "invoice2data" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,uom_name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "kg" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "" +"library (reads the text of the PDF and get the information). It will work " +"only if the invoice2data library has a template for the invoices of that " +"supplier." +msgstr "" +"bibliothèque (qui lit le texte du PDF et trouve l'information). Il ne " +"fonctionnera que si la bibliothèque invoice2data dispose d'un modèle pour " +"les factures de ce fournisseur." + +#, python-format +#~ msgid "" +#~ "This PDF invoice doesn't match a known template of the invoice2data lib." +#~ msgstr "" +#~ "Cette facture PDF ne correspond pas à un modèle connu de la bibliothèque " +#~ "invoice2data." diff --git a/account_invoice_import_invoice2data/i18n/it.po b/account_invoice_import_invoice2data/i18n/it.po new file mode 100644 index 0000000000..070d2e5f3c --- /dev/null +++ b/account_invoice_import_invoice2data/i18n/it.po @@ -0,0 +1,104 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_import_invoice2data +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-29 16:26+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "regular PDF file without any embedded XML file via the" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,description_sale:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,description_sale:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Acme beeswax" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Beeswax XL" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__display_name +msgid "Display Name" +msgstr "Nome Visualizzato" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__id +msgid "ID" +msgstr "ID" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.internet_access +#: model:product.template,name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Internet Access" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.luxury_truffles +#: model:product.template,name:account_invoice_import_invoice2data.luxury_truffles_product_template +msgid "Luxury Truffles" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.olive_oil +#: model:product.template,name:account_invoice_import_invoice2data.olive_oil_product_template +msgid "Olive Oil" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#, python-format +msgid "PDF Invoice parsing failed. Error message: %s" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.internet_access +#: model:product.template,uom_name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Units" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model,name:account_invoice_import_invoice2data.model_account_invoice_import +msgid "Wizard to import supplier invoices/refunds" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "invoice2data" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,uom_name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "kg" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "" +"library (reads the text of the PDF and get the information). It will work " +"only if the invoice2data library has a template for the invoices of that " +"supplier." +msgstr "" diff --git a/account_invoice_import_invoice2data/i18n/nl.po b/account_invoice_import_invoice2data/i18n/nl.po new file mode 100644 index 0000000000..83a339fe96 --- /dev/null +++ b/account_invoice_import_invoice2data/i18n/nl.po @@ -0,0 +1,116 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_import_invoice2data +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-18 16:34+0000\n" +"Last-Translator: Bosd \n" +"Language-Team: none\n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "regular PDF file without any embedded XML file via the" +msgstr "normaal PDF bestand zonder een geintegreerd XML bestand via de" + +#. module: account_invoice_import_invoice2data +#: model:product.product,description_sale:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,description_sale:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Acme beeswax" +msgstr "Acme beeswax" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Beeswax XL" +msgstr "Beeswax XL" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__display_name +msgid "Display Name" +msgstr "Weergavenaam" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__id +msgid "ID" +msgstr "ID" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.internet_access +#: model:product.template,name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Internet Access" +msgstr "Internet Toegang" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import____last_update +msgid "Last Modified on" +msgstr "Laatst bijgewerkt op" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.luxury_truffles +#: model:product.template,name:account_invoice_import_invoice2data.luxury_truffles_product_template +msgid "Luxury Truffles" +msgstr "Luxury Truffles" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.olive_oil +#: model:product.template,name:account_invoice_import_invoice2data.olive_oil_product_template +msgid "Olive Oil" +msgstr "Olive Oil" + +#. module: account_invoice_import_invoice2data +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#, python-format +msgid "PDF Invoice parsing failed. Error message: %s" +msgstr "Het is niet gelukt om de PDF factuur te verwerken. Fout melding: %s" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.internet_access +#: model:product.template,uom_name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Units" +msgstr "Stuks" + +#. module: account_invoice_import_invoice2data +#: model:ir.model,name:account_invoice_import_invoice2data.model_account_invoice_import +msgid "Wizard to import supplier invoices/refunds" +msgstr "Hulp voor het importeren van inkoop / credit facturen" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "invoice2data" +msgstr "invoice2data" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,uom_name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "kg" +msgstr "kg" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "" +"library (reads the text of the PDF and get the information). It will work " +"only if the invoice2data library has a template for the invoices of that " +"supplier." +msgstr "" +"bibliotheek (extraheert de teksten in de PDF voor het verkrijgen van de " +"informatie). Het werkt enkel wanneer er een sjabloon beschikbaar is voor de " +"betreffende leverancier." + +#, python-format +#~ msgid "" +#~ "This PDF invoice doesn't match a known template of the invoice2data lib." +#~ msgstr "" +#~ "Deze PDF factuur heeft geen overeenkomstig herkennings sjabloon van de " +#~ "invoice2data library." + +#~ msgid "Unit(s)" +#~ msgstr "Stuk(s)" diff --git a/account_invoice_import_invoice2data/i18n/pt.po b/account_invoice_import_invoice2data/i18n/pt.po new file mode 100644 index 0000000000..7f35cb520e --- /dev/null +++ b/account_invoice_import_invoice2data/i18n/pt.po @@ -0,0 +1,111 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_import_invoice2data +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2018-12-17 16:58+0000\n" +"Last-Translator: Pedro Castro Silva \n" +"Language-Team: none\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 3.3\n" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "regular PDF file without any embedded XML file via the" +msgstr "arquivo PDF regular sem qualquer arquivo XML incorporado via" + +#. module: account_invoice_import_invoice2data +#: model:product.product,description_sale:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,description_sale:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Acme beeswax" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Beeswax XL" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__display_name +msgid "Display Name" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__id +msgid "ID" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.internet_access +#: model:product.template,name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Internet Access" +msgstr "Acesso à Internet" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.luxury_truffles +#: model:product.template,name:account_invoice_import_invoice2data.luxury_truffles_product_template +msgid "Luxury Truffles" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.olive_oil +#: model:product.template,name:account_invoice_import_invoice2data.olive_oil_product_template +msgid "Olive Oil" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#, python-format +msgid "PDF Invoice parsing failed. Error message: %s" +msgstr "Análise de fatura PDF falhou. Mensagem de erro: %s" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.internet_access +#: model:product.template,uom_name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Units" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model,name:account_invoice_import_invoice2data.model_account_invoice_import +msgid "Wizard to import supplier invoices/refunds" +msgstr "Assistente para importar as faturas/créditos do fornecedor" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "invoice2data" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,uom_name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "kg" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "" +"library (reads the text of the PDF and get the information). It will work " +"only if the invoice2data library has a template for the invoices of that " +"supplier." +msgstr "" +"biblioteca (lê o texto do PDF e obtém a informação). Apenas funcionará se a " +"biblioteca de invoice2data tiver um modelo para as faturas deste fornecedor." + +#, python-format +#~ msgid "" +#~ "This PDF invoice doesn't match a known template of the invoice2data lib." +#~ msgstr "" +#~ "Esta fatura PDF não combina com um modelo conhecido do invoice2data lib." diff --git a/account_invoice_import_invoice2data/i18n/sl.po b/account_invoice_import_invoice2data/i18n/sl.po new file mode 100644 index 0000000000..f0486253e7 --- /dev/null +++ b/account_invoice_import_invoice2data/i18n/sl.po @@ -0,0 +1,113 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_import_invoice2data +# +# Translators: +# Matjaž Mozetič , 2016 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-11-12 13:37+0000\n" +"PO-Revision-Date: 2016-11-12 13:37+0000\n" +"Last-Translator: Matjaž Mozetič , 2016\n" +"Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n" +"%100==4 ? 2 : 3);\n" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "regular PDF file without any embedded XML file via the" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,description_sale:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,description_sale:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Acme beeswax" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "Beeswax XL" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__display_name +msgid "Display Name" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import__id +msgid "ID" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.internet_access +#: model:product.template,name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Internet Access" +msgstr "Internetni dostop" + +#. module: account_invoice_import_invoice2data +#: model:ir.model.fields,field_description:account_invoice_import_invoice2data.field_account_invoice_import____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.luxury_truffles +#: model:product.template,name:account_invoice_import_invoice2data.luxury_truffles_product_template +msgid "Luxury Truffles" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,name:account_invoice_import_invoice2data.olive_oil +#: model:product.template,name:account_invoice_import_invoice2data.olive_oil_product_template +msgid "Olive Oil" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: code:addons/account_invoice_import_invoice2data/wizard/account_invoice_import.py:0 +#, python-format +msgid "PDF Invoice parsing failed. Error message: %s" +msgstr "Neuspešna obdelava PDF računa. Napaka: %s" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.internet_access +#: model:product.template,uom_name:account_invoice_import_invoice2data.internet_access_product_template +msgid "Units" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:ir.model,name:account_invoice_import_invoice2data.model_account_invoice_import +msgid "Wizard to import supplier invoices/refunds" +msgstr "Čarovnik za uvoz prejetih računov/dobropisov" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "invoice2data" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model:product.product,uom_name:account_invoice_import_invoice2data.product_beeswax_xl +#: model:product.template,uom_name:account_invoice_import_invoice2data.product_beeswax_xl_product_template +msgid "kg" +msgstr "" + +#. module: account_invoice_import_invoice2data +#: model_terms:ir.ui.view,arch_db:account_invoice_import_invoice2data.account_invoice_import_form +msgid "" +"library (reads the text of the PDF and get the information). It will work " +"only if the invoice2data library has a template for the invoices of that " +"supplier." +msgstr "" + +#, python-format +#~ msgid "" +#~ "This PDF invoice doesn't match a known template of the invoice2data lib." +#~ msgstr "" +#~ "Ta PDF račun se ne ujema z nobeno znano predlogo iz knjižnice " +#~ "invoice2data." diff --git a/account_invoice_import_invoice2data/readme/CONFIGURE.rst b/account_invoice_import_invoice2data/readme/CONFIGURE.rst new file mode 100644 index 0000000000..c61fa8cdb7 --- /dev/null +++ b/account_invoice_import_invoice2data/readme/CONFIGURE.rst @@ -0,0 +1,105 @@ +Go to the form view of the supplier and configure it with the following parameters: + +* the *VAT* is set (the VAT number is used by default when searching the supplier in the Odoo partner database) +* in the *Invoicing* tab, create an *Invoice Import Configuration*. + +For the PDF invoice of your supplier that don't have an embedded XML file, you will have to create a `template file `_ in YAML format in the invoice2data Python library. It is quite easy to do ; if you are familiar with `regexp `_, it should not take more than 10 minutes for each supplier. + +Here are some hints to help you add a template for your supplier: + +* There is a `tutorial in the repo of the invoice2data library `_ + +* Take `Free SAS template file `_ as an example. You will find a sample PDF invoice for this supplier under invoice2data/test/pdf/invoice_free_fiber_201507.pdf + +* Try to run the invoice2data library manually on the sample invoice of Free: + +.. code:: + + % python -m invoice2data.main --debug invoice2data/test/pdf/invoice_free_fiber_201507.pdf + +On the output, you will get first the text of the PDF, then some debug info on the parsing of the invoice and the regexps, and, on the last line, you will have the dict that contain the result of the parsing. + +* if the VAT number of the supplier is present in the text of the PDF invoice, I think it's a good idea to use it as the keyword. It is good practice to add 2 other keywords: one for the language (for example, match on the word *Invoice* in the language of the invoice) and one for the currency, to match only the invoices of that supplier in this particular language and currency. + +* the list of *fields* should contain the following entries: + + * 'vat' with the VAT number of the supplier (if the VAT number of the supplier is not in the text of PDF file, add a 'partner_name' key) + * 'amount' ('amount' is the total amount with taxes) + * 'amount_untaxed' or 'amount_tax' (one or the other, no need for both) + * 'date': the date of the invoice + * 'invoice_number' + * 'date_due', if this information is available in the text of the PDF file. + +The invoice2data library is quite powerfull. It supports multiple input methods (pdftotext, ocrmypdf, tesseract ocr, google cloud vision). +Even invoicelines can be imported and mapped to products in the database. +The invoice2data library does not have a strict standard on field names. This makes the module very flexible, but also hard to create re-usable templates. + +If you want to make use of the advanced features, support for the following fields is implemented. + +## Supported fields + +(note: the fieldname column contains the name to be used in the template file.) + +Partner fields +| fieldname | type | Description | +| -------------- | :---------: | :-------------------------------------- | +| vat | char | The vat code is unique for each partner, it has the highest priority for matching the partner | +| partner_name | char | self explaining | +| partner_street | char | self explaining | +| partner_street2 | char | self explaining | +| partner_street3 | char | self explaining | +| partner_city | char | self explaining | +| partner_zip | char | self explaining | +| country_code | char | use iso format fr or nl | +| state_code | char | use iso format NY (for New York) | +| partner_email | char | self explaining | +| partner_website | char | self explaining | +| telephone | char | can be used for matching the partner with the help of support modules | +| mobile | char | can be used for matching the partner contact with the help of support modules | +| partner_ref | char | reference name or number can be used for partner matching | +| siren | char | French business code, can be used for matching the partner | +| partner_coc | char | General business identiefier number, can be used for matching the partner | + +Invoice Fields (on document level) +| fieldname | type | Description | +| -------------- | :---------: | :-------------------------------------- | +| currency | char | The currency of the invoice in iso format (EUR, USD) | +| currency_symbol | char | The currency symbol of the invoice (€, $) | +| bic | char | Bank Identifier Code | +| iban | char | International Bank Account Number | +| amount | float | The total amount of the invoice (including taxes) | +| amount_untaxed | float | The total amount of the invoice (excluding taxes) | +| amount_tax | float | The sum of the tax amount of the invoice | +| date | date | The date of the invoice | +| invoice_number | char | self explaining | +| date_due | date | The duedate of the invoice | +| date_start | date | The start date of the period for the invoice when the services are delivered. | +| date_end | date | The start date of the period for the invoice when the services are delivered. | +| note | char | The contents of this field will be imported in the chatter. | +| narration | char | The contents of this field will be imported in the narration field. (on the bottom of the invoice.) | +| payment_reference | char | If the invoice is pre-paid an reference can be used for payment reconciliation | +| payment_unece_code | char | The unece code of the payment means according to 4461 code list | +| incoterm | char | The Incoterm 2000 abbrevation | +| company_vat | char | The vat number of the company to which the invoice is addressed to. Used to check if the invoice is actually is adressed to the company which wants to process it. (Very useful in multi-company setup) | +| mandate_id | char | A banking mandate is attached to a bank account and represents an authorization that the bank account owner gives to a company for a specific operation (such as direct debit). | + + +Invoice line Fields +| fieldname | type | Description | +| -------------- | :---------: | :-------------------------------------- | +| name | char | The name of the product, can be used for product matching | +| barcode | char | The the barcode of the product or product package, used for product matching | +| code | char | The (internal) product code, used for product matching | +| qty | float | The amount of items/units | +| unece_code | char | The unece code of the products units of measure can be passed | +| uom | char | The name of the unit of measure, internally if will be mapped to the unece code. Example L will be mapped to unece_code LTR | +| price_unit | float | The unit price of the item. (excluding taxes) | +| discount | float | The amount of discount for this line. Eg 20 for 20% discount or 0.0 for no discount | +| price_total | float | The total amount of the invoice line including taxes. It can be used to select the correct tax tag. | +| price_subtotal | float | The total amount of the invoice line excluding taxes. It can be used to create adjustment lines when the decimal precision is insufficient. | +| line_tax_percent | float | The percentage of tax | +| line_tax_amount | float | The fixed amount of tax applied to the line | +| line_note | char | Notes on the invoice can be imported, There is a special view available. | +| sectionheader | char | There is a special view available for section headers. | +| date_start | date | The start date of the period for the invoice when the services are delivered. | +| date_end | date | The start date of the period for the invoice when the services are delivered. | diff --git a/account_invoice_import_invoice2data/readme/CONTRIBUTORS.rst b/account_invoice_import_invoice2data/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..74bdeae091 --- /dev/null +++ b/account_invoice_import_invoice2data/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Alexis de Lattre +* Marwan Behillil diff --git a/account_invoice_import_invoice2data/readme/DESCRIPTION.rst b/account_invoice_import_invoice2data/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..0ab0c6f4e9 --- /dev/null +++ b/account_invoice_import_invoice2data/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +This module is an extension of the module *account_invoice_import*: it adds support for regular PDF invoices i.e. PDF invoice that don't have an embedded XML file. It uses the `invoice2data library `_ which takes care of extracting the text of the PDF invoice, find an existing invoice template and execute the invoice template to extract the useful information from the invoice. + +To know the full story behind the development of this module, read this `blog post `_. + +More information for creating the templates can be found in `tutorial of the invoice2data library `_. The templates have to be created manually. An graphical template creator for odoo is a work in progress. + +**WARNING**: an alternative module **account_invoice_import_simple_pdf** developped in July 2021 provides similar features but has one big advantage: the accountant can add support for a new vendor by himself, no more invoice templates which require technical skill. The module *account_invoice_import_simple_pdf* provides basic functionality, but does not support line level accounting. diff --git a/account_invoice_import_invoice2data/readme/HISTORY.rst b/account_invoice_import_invoice2data/readme/HISTORY.rst new file mode 100644 index 0000000000..a8381cc2c6 --- /dev/null +++ b/account_invoice_import_invoice2data/readme/HISTORY.rst @@ -0,0 +1,10 @@ +16.0.1.0.0 (2025-10-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Migration to 16.0. + +14.0.2.2.0 (2023-03-03) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [ADD] Support for invoicelines. + (`#74 `_) diff --git a/account_invoice_import_invoice2data/readme/INSTALL.rst b/account_invoice_import_invoice2data/readme/INSTALL.rst new file mode 100644 index 0000000000..5e09952057 --- /dev/null +++ b/account_invoice_import_invoice2data/readme/INSTALL.rst @@ -0,0 +1,64 @@ +This module requires the Python library *invoice2data* available on `Github `_ with a version >= 0.2.74 (February 2018). + +To install the latest version of this library, run: + +.. code:: + + sudo pip3 install --upgrade invoice2data + +If you use Ubuntu 16.04 LTS or higher, you can use the pdftotext version 0.41.0 that is packaged in the distribution: + +.. code:: + + sudo apt install poppler-utils + +If you want the invoice2data library to support mixed-type pdf's or fallback on OCR if the PDF doesn't contain text (only a very small minority of PDF invoices are image-based and require OCR) like scanned receipts, you should also install `Ocrmypdf `_ + +.. code:: + + pip install -U ocrmypdf + +If you want the invoice2data library to fallback on OCR if the PDF doesn't contain text (only a very small minority of PDF invoices are image-based and require OCR) like scanned receipts, you should also install `Imagemagick `_ (to get the *convert* utility to convert PDF to TIFF) and `Tesseract OCR `_ : + +.. code:: + + sudo apt install imagemagick tesseract-ocr + +If you want to use custom invoice templates for the invoice2data lib (in addition to the templates provided by the invoice2data lib), you should add a line in your Odoo server configuration file such as: + +.. code:: + + invoice2data_templates_dir = /opt/invoice2data_local_templates + +and store your invoice templates in YAML format (*.yml* extension) or json format in the directory that you have configured above. If you add invoice tempates in this directory, you don't have to restart Odoo, they will be used automatically on the next invoice import. + +If you want to use only your custom invoice templates and ignore the templates provided by the invoice2data lib, you should have in your Odoo server configuration file: + +.. code:: + + invoice2data_templates_dir = /opt/invoice2data_local_templates + invoice2data_exclude_built_in_templates = True + +The yaml templates are loaded with [pyyaml](https://github.com/yaml/pyyaml) which is a pure python implementation. (thus rather slow) +As an alternative json templates can be used. Which are natively better supported by python. The performance with yaml templates can be greatly increased **10x** by using [libyaml](https://github.com/yaml/libyaml) +It can be installed on most distributions by: + +.. code:: + + sudo apt-get libyaml-dev + +French users should also install the module *l10n_fr_business_document_import* available in the `French localization `_. +Dutch users should also install the module *l10n_nl_business_document_import* available in the `Netherlands localization `_. + +Dependencies +============ +| Name | Requirement | Installation | Description | +| -------------- | :---------: | :---------: | :-------------------------------------- | +| [invoice2data](https://github.com/invoice-x/invoice2data) | required | `pip install invoice2data` | The main dependency of this invoice import module | +| [poppler-utils](https://poppler.freedesktop.org/) | required | `apt install poppler-utils` | The default Input-reader for the invoice2data library | +| [dateparser](https://dateparser.readthedocs.io/en/latest/#) | required | `pip install dateparser` | Requirement for parsing the invoice dates, this requirement is likely already satisfied by odoo itself | +| [libyaml](https://github.com/yaml/libyaml) | optional | `apt install libyaml-dev` | Template loader, recomended to greatly speedup the loading of yaml templates | +| [imagemagick](https://www.imagemagick.org) | optional | `apt install imagemagick` | inputreader: Pre-processes the pdf before feeding it into tesseract-ocr | +| [tesseract-ocr](https://github.com/tesseract-ocr/tesseract) | optional | `apt install tesseract-ocr` | inputreader: for ocr of image only pdf files | +| [tesseract-ocr- lang](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html) | optional | `apt install tesseract-ocr-` see documentation | inputreader: Language pack for tesseract ocr, greatly improves character detection | +| [ocrmypdf](https://github.com/ocrmypdf/OCRmyPDF) | optional | `apt install ocrmypdf` | inputreader: For image only or mixed type pdf's. It uses tesseract-ocr under the hood, but provides optimalisations which greatly improves results | diff --git a/account_invoice_import_invoice2data/readme/ROADMAP.rst b/account_invoice_import_invoice2data/readme/ROADMAP.rst new file mode 100644 index 0000000000..065d95d03b --- /dev/null +++ b/account_invoice_import_invoice2data/readme/ROADMAP.rst @@ -0,0 +1,8 @@ +* Implement support for lines with all tax included, used for some localizations like Switzerland or scanned receipts. +* An graphical template builder. +* Once invoice2data 1.0 is published on PyPI: opt into its new APIs — the input-backend cascade (faster pdfium-first default + automatic fallback), runtime ``ai_fallback=True`` for LLM extraction when no template matches, ``raise_on_error=True`` for typed ``NoTemplateFoundError`` / ``RequiredFieldsMissingError`` instead of ``{}``, and ``pre_process_pdf`` returning the cleaned/smaller PDF so the wizard can re-attach it to the Odoo invoice in place of the raw upload. + +Known Issues +* The input module is hard coded to use pdftotext parser and as a fallback to tesseract. +* Creation of the templates is still quite hard. +* The addres and company specific fields are parsed. Meaning it is possible to import an invoice which is issued to another company than yours! diff --git a/account_invoice_import_invoice2data/static/description/icon.png b/account_invoice_import_invoice2data/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/account_invoice_import_invoice2data/static/description/icon.png differ diff --git a/account_invoice_import_invoice2data/static/description/index.html b/account_invoice_import_invoice2data/static/description/index.html new file mode 100644 index 0000000000..b7f4a9a411 --- /dev/null +++ b/account_invoice_import_invoice2data/static/description/index.html @@ -0,0 +1,616 @@ + + + + + +Account Invoice Import Invoice2data + + + +
+

Account Invoice Import Invoice2data

+ + +

Beta License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

+

This module is an extension of the module account_invoice_import: it adds support for regular PDF invoices i.e. PDF invoice that don’t have an embedded XML file. It uses the invoice2data library which takes care of extracting the text of the PDF invoice, find an existing invoice template and execute the invoice template to extract the useful information from the invoice.

+

To know the full story behind the development of this module, read this blog post.

+

More information for creating the templates can be found in tutorial of the invoice2data library. The templates have to be created manually. An graphical template creator for odoo is a work in progress.

+

WARNING: an alternative module account_invoice_import_simple_pdf developped in July 2021 provides similar features but has one big advantage: the accountant can add support for a new vendor by himself, no more invoice templates which require technical skill. The module account_invoice_import_simple_pdf provides basic functionality, but does not support line level accounting.

+

Table of contents

+ +
+

Installation

+

This module requires the Python library invoice2data available on Github with a version >= 0.2.74 (February 2018).

+

To install the latest version of this library, run:

+
+sudo pip3 install --upgrade invoice2data
+
+

If you use Ubuntu 16.04 LTS or higher, you can use the pdftotext version 0.41.0 that is packaged in the distribution:

+
+sudo apt install poppler-utils
+
+

If you want the invoice2data library to support mixed-type pdf’s or fallback on OCR if the PDF doesn’t contain text (only a very small minority of PDF invoices are image-based and require OCR) like scanned receipts, you should also install Ocrmypdf

+
+pip install -U ocrmypdf
+
+

If you want the invoice2data library to fallback on OCR if the PDF doesn’t contain text (only a very small minority of PDF invoices are image-based and require OCR) like scanned receipts, you should also install Imagemagick (to get the convert utility to convert PDF to TIFF) and Tesseract OCR :

+
+sudo apt install imagemagick tesseract-ocr
+
+

If you want to use custom invoice templates for the invoice2data lib (in addition to the templates provided by the invoice2data lib), you should add a line in your Odoo server configuration file such as:

+
+invoice2data_templates_dir = /opt/invoice2data_local_templates
+
+

and store your invoice templates in YAML format (.yml extension) or json format in the directory that you have configured above. If you add invoice tempates in this directory, you don’t have to restart Odoo, they will be used automatically on the next invoice import.

+

If you want to use only your custom invoice templates and ignore the templates provided by the invoice2data lib, you should have in your Odoo server configuration file:

+
+invoice2data_templates_dir = /opt/invoice2data_local_templates
+invoice2data_exclude_built_in_templates = True
+
+

The yaml templates are loaded with [pyyaml](https://github.com/yaml/pyyaml) which is a pure python implementation. (thus rather slow) +As an alternative json templates can be used. Which are natively better supported by python. The performance with yaml templates can be greatly increased 10x by using [libyaml](https://github.com/yaml/libyaml) +It can be installed on most distributions by:

+
+sudo apt-get libyaml-dev
+
+

French users should also install the module l10n_fr_business_document_import available in the French localization. +Dutch users should also install the module l10n_nl_business_document_import available in the Netherlands localization.

+
+
+

Dependencies

+
+
Name | Requirement | Installation | Description |
+
————– | :———: | :———: | :————————————– |
+
[invoice2data](https://github.com/invoice-x/invoice2data) | required | pip install invoice2data | The main dependency of this invoice import module |
+
[poppler-utils](https://poppler.freedesktop.org/) | required | apt install poppler-utils | The default Input-reader for the invoice2data library |
+
[dateparser](https://dateparser.readthedocs.io/en/latest/#) | required | pip install dateparser | Requirement for parsing the invoice dates, this requirement is likely already satisfied by odoo itself |
+
[libyaml](https://github.com/yaml/libyaml) | optional | apt install libyaml-dev | Template loader, recomended to greatly speedup the loading of yaml templates |
+
[imagemagick](https://www.imagemagick.org) | optional | apt install imagemagick | inputreader: Pre-processes the pdf before feeding it into tesseract-ocr |
+
[tesseract-ocr](https://github.com/tesseract-ocr/tesseract) | optional | apt install tesseract-ocr | inputreader: for ocr of image only pdf files |
+
[tesseract-ocr- lang](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html) | optional | apt install tesseract-ocr- see documentation | inputreader: Language pack for tesseract ocr, greatly improves character detection |
+
[ocrmypdf](https://github.com/ocrmypdf/OCRmyPDF) | optional | apt install ocrmypdf | inputreader: For image only or mixed type pdf’s. It uses tesseract-ocr under the hood, but provides optimalisations which greatly improves results |
+
+
+
+

Configuration

+

Go to the form view of the supplier and configure it with the following parameters:

+
    +
  • the VAT is set (the VAT number is used by default when searching the supplier in the Odoo partner database)
  • +
  • in the Invoicing tab, create an Invoice Import Configuration.
  • +
+

For the PDF invoice of your supplier that don’t have an embedded XML file, you will have to create a template file in YAML format in the invoice2data Python library. It is quite easy to do ; if you are familiar with regexp, it should not take more than 10 minutes for each supplier.

+

Here are some hints to help you add a template for your supplier:

+ +
+% python -m invoice2data.main --debug invoice2data/test/pdf/invoice_free_fiber_201507.pdf
+
+

On the output, you will get first the text of the PDF, then some debug info on the parsing of the invoice and the regexps, and, on the last line, you will have the dict that contain the result of the parsing.

+
    +
  • if the VAT number of the supplier is present in the text of the PDF invoice, I think it’s a good idea to use it as the keyword. It is good practice to add 2 other keywords: one for the language (for example, match on the word Invoice in the language of the invoice) and one for the currency, to match only the invoices of that supplier in this particular language and currency.
  • +
  • the list of fields should contain the following entries:
      +
    • ‘vat’ with the VAT number of the supplier (if the VAT number of the supplier is not in the text of PDF file, add a ‘partner_name’ key)
    • +
    • ‘amount’ (‘amount’ is the total amount with taxes)
    • +
    • ‘amount_untaxed’ or ‘amount_tax’ (one or the other, no need for both)
    • +
    • ‘date’: the date of the invoice
    • +
    • ‘invoice_number’
    • +
    • ‘date_due’, if this information is available in the text of the PDF file.
    • +
    +
  • +
+

The invoice2data library is quite powerfull. It supports multiple input methods (pdftotext, ocrmypdf, tesseract ocr, google cloud vision). +Even invoicelines can be imported and mapped to products in the database. +The invoice2data library does not have a strict standard on field names. This makes the module very flexible, but also hard to create re-usable templates.

+

If you want to make use of the advanced features, support for the following fields is implemented.

+

## Supported fields

+

(note: the fieldname column contains the name to be used in the template file.)

+

Partner fields +| fieldname | type | Description | +| ————– | :———: | :————————————– | +| vat | char | The vat code is unique for each partner, it has the highest priority for matching the partner | +| partner_name | char | self explaining | +| partner_street | char | self explaining | +| partner_street2 | char | self explaining | +| partner_street3 | char | self explaining | +| partner_city | char | self explaining | +| partner_zip | char | self explaining | +| country_code | char | use iso format fr or nl | +| state_code | char | use iso format NY (for New York) | +| partner_email | char | self explaining | +| partner_website | char | self explaining | +| telephone | char | can be used for matching the partner with the help of support modules | +| mobile | char | can be used for matching the partner contact with the help of support modules | +| partner_ref | char | reference name or number can be used for partner matching | +| siren | char | French business code, can be used for matching the partner | +| partner_coc | char | General business identiefier number, can be used for matching the partner |

+

Invoice Fields (on document level) +| fieldname | type | Description | +| ————– | :———: | :————————————– | +| currency | char | The currency of the invoice in iso format (EUR, USD) | +| currency_symbol | char | The currency symbol of the invoice (€, $) | +| bic | char | Bank Identifier Code | +| iban | char | International Bank Account Number | +| amount | float | The total amount of the invoice (including taxes) | +| amount_untaxed | float | The total amount of the invoice (excluding taxes) | +| amount_tax | float | The sum of the tax amount of the invoice | +| date | date | The date of the invoice | +| invoice_number | char | self explaining | +| date_due | date | The duedate of the invoice | +| date_start | date | The start date of the period for the invoice when the services are delivered. | +| date_end | date | The start date of the period for the invoice when the services are delivered. | +| note | char | The contents of this field will be imported in the chatter. | +| narration | char | The contents of this field will be imported in the narration field. (on the bottom of the invoice.) | +| payment_reference | char | If the invoice is pre-paid an reference can be used for payment reconciliation | +| payment_unece_code | char | The unece code of the payment means according to 4461 code list | +| incoterm | char | The Incoterm 2000 abbrevation | +| company_vat | char | The vat number of the company to which the invoice is addressed to. Used to check if the invoice is actually is adressed to the company which wants to process it. (Very useful in multi-company setup) | +| mandate_id | char | A banking mandate is attached to a bank account and represents an authorization that the bank account owner gives to a company for a specific operation (such as direct debit). |

+

Invoice line Fields +| fieldname | type | Description | +| ————– | :———: | :————————————– | +| name | char | The name of the product, can be used for product matching | +| barcode | char | The the barcode of the product or product package, used for product matching | +| code | char | The (internal) product code, used for product matching | +| qty | float | The amount of items/units | +| unece_code | char | The unece code of the products units of measure can be passed | +| uom | char | The name of the unit of measure, internally if will be mapped to the unece code. Example L will be mapped to unece_code LTR | +| price_unit | float | The unit price of the item. (excluding taxes) | +| discount | float | The amount of discount for this line. Eg 20 for 20% discount or 0.0 for no discount | +| price_total | float | The total amount of the invoice line including taxes. It can be used to select the correct tax tag. | +| price_subtotal | float | The total amount of the invoice line excluding taxes. It can be used to create adjustment lines when the decimal precision is insufficient. | +| line_tax_percent | float | The percentage of tax | +| line_tax_amount | float | The fixed amount of tax applied to the line | +| line_note | char | Notes on the invoice can be imported, There is a special view available. | +| sectionheader | char | There is a special view available for section headers. | +| date_start | date | The start date of the period for the invoice when the services are delivered. | +| date_end | date | The start date of the period for the invoice when the services are delivered. |

+
+
+

Known issues / Roadmap

+
    +
  • Implement support for lines with all tax included, used for some localizations like Switzerland or scanned receipts.
  • +
  • An graphical template builder.
  • +
  • Once invoice2data 1.0 is published on PyPI: opt into its new APIs — the input-backend cascade (faster pdfium-first default + automatic fallback), runtime ai_fallback=True for LLM extraction when no template matches, raise_on_error=True for typed NoTemplateFoundError / RequiredFieldsMissingError instead of {}, and pre_process_pdf returning the cleaned/smaller PDF so the wizard can re-attach it to the Odoo invoice in place of the raw upload.
  • +
+

Known Issues +* The input module is hard coded to use pdftotext parser and as a fallback to tesseract. +* Creation of the templates is still quite hard. +* The addres and company specific fields are parsed. Meaning it is possible to import an invoice which is issued to another company than yours!

+
+
+

Changelog

+
+

16.0.1.0.0 (2025-10-02)

+
    +
  • Migration to 16.0.
  • +
+
+
+

14.0.2.2.0 (2023-03-03)

+
    +
  • [ADD] Support for invoicelines. +(#74)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

alexis-via bosd

+

This module is part of the OCA/edi project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_invoice_import_invoice2data/tests/__init__.py b/account_invoice_import_invoice2data/tests/__init__.py new file mode 100644 index 0000000000..d0a138175d --- /dev/null +++ b/account_invoice_import_invoice2data/tests/__init__.py @@ -0,0 +1 @@ +from . import test_invoice_import diff --git a/account_invoice_import_invoice2data/tests/pdf/AzureInterior.pdf b/account_invoice_import_invoice2data/tests/pdf/AzureInterior.pdf new file mode 100644 index 0000000000..b342e2b02b Binary files /dev/null and b/account_invoice_import_invoice2data/tests/pdf/AzureInterior.pdf differ diff --git a/account_invoice_import_invoice2data/tests/pdf/invoice_free_fiber_201507.pdf b/account_invoice_import_invoice2data/tests/pdf/invoice_free_fiber_201507.pdf new file mode 100644 index 0000000000..adc46f510f Binary files /dev/null and b/account_invoice_import_invoice2data/tests/pdf/invoice_free_fiber_201507.pdf differ diff --git a/account_invoice_import_invoice2data/tests/test_invoice_import.py b/account_invoice_import_invoice2data/tests/test_invoice_import.py new file mode 100644 index 0000000000..6545c3f003 --- /dev/null +++ b/account_invoice_import_invoice2data/tests/test_invoice_import.py @@ -0,0 +1,531 @@ +# Copyright 2015-2021 Akretion France (www.akretion.com) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 +import logging +from unittest import mock + +from odoo import Command, fields +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase +from odoo.tools import file_open, float_compare + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + + +class TestInvoiceImport(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + frtax = cls.env["account.tax"].create( + { + "name": "French VAT purchase 20.0%", + "description": "FR-VAT-buy-20.0", + "amount": 20, + "amount_type": "percent", + "type_tax_use": "purchase", + } + ) + # Set this tax on Internet access product + internet_product = cls.env.ref( + "account_invoice_import_invoice2data.internet_access" + ) + internet_product.supplier_taxes_id = [Command.set([frtax.id])] + + def test_have_invoice2data_unavailable(self): + with mock.patch.dict("sys.modules", {"invoice2data": None}): + with self.assertLogs("", level="DEBUG") as cm: + logging.getLogger("").debug("Cannot import invoice2data") + self.assertEqual(cm.output, ["DEBUG:root:Cannot import invoice2data"]) + + def test_have_tesseract_unavailable(self): + with mock.patch.dict("sys.modules", {"tesseract": None}): + with self.assertLogs("", level="DEBUG") as cm: + logging.getLogger("").debug("Cannot import tesseract") + self.assertEqual(cm.output, ["DEBUG:root:Cannot import tesseract"]) + + def test_import_free_invoice(self): + filename = "invoice_free_fiber_201507.pdf" + f = file_open("account_invoice_import_invoice2data/tests/pdf/" + filename, "rb") + pdf_file = f.read() + invoice_file = self.env["ir.attachment"].create( + { + "name": filename, + "res_model": self.env["account.invoice.import"]._name, + "datas": base64.b64encode(pdf_file), + "type": "binary", + } + ) + wiz = self.env["account.invoice.import"].create( + { + "invoice_attachment_ids": invoice_file, + } + ) + f.close() + wiz.import_invoices() + # Check result of invoice creation + invoices = self.env["account.move"].search( + [ + ("state", "=", "draft"), + ("move_type", "=", "in_invoice"), + ("ref", "=", "562044387"), + ] + ) + self.assertEqual(len(invoices), 1) + inv = invoices[0] + self.assertEqual(inv.move_type, "in_invoice") + self.assertEqual(fields.Date.to_string(inv.invoice_date), "2015-07-02") + self.assertEqual( + inv.partner_id, self.env.ref("account_invoice_import_invoice2data.free") + ) + self.assertEqual(inv.journal_id.type, "purchase") + self.assertEqual(float_compare(inv.amount_total, 29.99, precision_digits=2), 0) + self.assertEqual( + float_compare(inv.amount_untaxed, 24.99, precision_digits=2), 0 + ) + self.assertEqual(len(inv.invoice_line_ids), 1) + iline = inv.invoice_line_ids[0] + self.assertEqual(iline.name, "Fiber optic access at the main office") + self.assertEqual( + iline.product_id, + self.env.ref("account_invoice_import_invoice2data.internet_access"), + ) + self.assertEqual(float_compare(iline.quantity, 1.0, precision_digits=0), 0) + self.assertEqual(float_compare(iline.price_unit, 24.99, precision_digits=2), 0) + + # Prepare data for next test i.e. invoice update + # (we re-use the invoice created by the first import !) + inv.write( + { + "invoice_date": False, + "ref": False, + } + ) + + # New import with update of an existing draft invoice + wiz2 = self.env["account.invoice.import"].create( + { + "invoice_attachment_ids": invoice_file, + } + ) + action = wiz2.import_invoices() + + self.assertEqual(action["params"]["next"]["res_model"], "account.move") + invoices = self.env["account.move"].search( + [ + ("state", "=", "draft"), + ("move_type", "=", "in_invoice"), + ("ref", "=", "562044387"), + ] + ) + self.assertEqual(len(invoices), 1) + inv = invoices[0] + self.assertEqual(fields.Date.to_string(inv.invoice_date), "2015-07-02") + + def test_import_azure_interior_invoice(self): + """Function for testing almost all supported fields""" + filename = "AzureInterior.pdf" + f = file_open("account_invoice_import_invoice2data/tests/pdf/" + filename, "rb") + pdf_file = f.read() + invoice_file = self.env["ir.attachment"].create( + { + "name": filename, + "res_model": self.env["account.invoice.import"]._name, + "datas": base64.b64encode(pdf_file), + "type": "binary", + } + ) + wiz = self.env["account.invoice.import"].create( + { + "invoice_attachment_ids": invoice_file, + } + ) + f.close() + wiz.import_invoices() + # Check result of invoice creation + invoices = self.env["account.move"].search( + [ + ("state", "=", "draft"), + ("ref", "ilike", "INV"), + ] + ) + self.assertEqual(len(invoices), 1) + inv = invoices[0] + self.assertEqual(inv.move_type, "in_invoice") + self.assertEqual(fields.Date.to_string(inv.invoice_date), "2023-03-20") + self.assertEqual(inv.partner_id, self.env.ref("base.res_partner_12")) + self.assertEqual(inv.journal_id.type, "purchase") + self.assertEqual(float_compare(inv.amount_total, 279.84, precision_digits=2), 0) + self.assertEqual( + float_compare(inv.amount_untaxed, 262.9, precision_digits=2), 0 + ) + + self.assertHTMLEqual( + inv.narration, + "

Due to global inflation our payment term has changed to 15 days.

", + ) + + # Following tests are disabled. Not yet implemented in account_invoice_import + # self.assertEqual(inv.journal_id.payment_reference, "202309097001") + # self.assertEqual(inv.journal_id.incoterm_id, self.env.ref("account.incoterm_DPU") + + self.assertEqual(len(inv.invoice_line_ids), 7) + iline = inv.invoice_line_ids[0] + self.assertEqual(iline.name, "--- Non Food ---") + self.assertEqual(iline.display_type, "line_section") + iline = inv.invoice_line_ids[1] + self.assertEqual(iline.name, "Beeswax XL\nAcme beeswax") + self.assertEqual( + iline.product_id, + self.env.ref("account_invoice_import_invoice2data.product_beeswax_xl"), + ) + self.assertEqual(float_compare(iline.quantity, 1.0, precision_digits=0), 0) + self.assertEqual(float_compare(iline.price_unit, 42.00, precision_digits=2), 0) + + iline = inv.invoice_line_ids[2] + self.assertEqual(iline.name, "Office Chair") + self.assertEqual( + iline.product_id, + self.env.ref("product.product_delivery_01"), + ) + self.assertEqual(float_compare(iline.quantity, 1.0, precision_digits=0), 0) + self.assertEqual(float_compare(iline.price_unit, 70.00, precision_digits=2), 0) + + iline = inv.invoice_line_ids[3] + self.assertEqual(iline.name, "--- Food Products ---") + self.assertEqual(iline.display_type, "line_section") + iline = inv.invoice_line_ids[4] + self.assertEqual(iline.name, "Olive Oil") + self.assertEqual( + iline.product_id, + self.env.ref("account_invoice_import_invoice2data.olive_oil"), + ) + self.assertEqual(float_compare(iline.quantity, 1.0, precision_digits=0), 0) + self.assertEqual(float_compare(iline.price_unit, 1.00, precision_digits=2), 0) + self.assertEqual(float_compare(iline.discount, 10, precision_digits=2), 0) + iline = inv.invoice_line_ids[5] + self.assertEqual( + iline.name, "Our Olive Oil is delivered in a re-usable glass container" + ) + self.assertEqual(iline.display_type, "line_note") + iline = inv.invoice_line_ids[6] + self.assertEqual(iline.name, "Luxury Truffles") + self.assertEqual( + iline.product_id, + self.env.ref("account_invoice_import_invoice2data.luxury_truffles"), + ) + self.assertEqual(float_compare(iline.quantity, 15.0, precision_digits=0), 0) + self.assertEqual(float_compare(iline.price_unit, 10.00, precision_digits=2), 0) + + def test_parse_invoice2data_taxes_percent(self): + """Test parse_invoice2data_taxes with percentage taxes""" + wizard = self.env["account.invoice.import"] + + line_data = {"line_tax_percent": 20.0} + result = wizard.parse_invoice2data_taxes(line_data) + + expected = [ + { + "amount_type": "percent", + "amount": 20.0, + "price_include": False, + "unece_type_code": "VAT", + "unece_categ_code": "", + } + ] + self.assertEqual(result, expected) + + def test_parse_invoice2data_taxes_fixed(self): + """Test parse_invoice2data_taxes with fixed amount taxes""" + wizard = self.env["account.invoice.import"] + + # Test with fixed tax amount + line_data = {"line_tax_amount": 15.50} + result = wizard.parse_invoice2data_taxes(line_data) + + expected = [ + { + "amount_type": "fixed", + "amount": 15.50, + "price_include": False, + "unece_type_code": "VAT", + "unece_categ_code": "", + } + ] + self.assertEqual(result, expected) + + def test_parse_invoice2data_taxes_price_include(self): + """Test parse_invoice2data_taxes with tax-included prices""" + wizard = self.env["account.invoice.import"] + + # Test with price_total but no price_subtotal (implies tax included) + # This causes an UnboundLocalError when trying to access undefined variables + line_data = {"price_total": 120.0} + + # The function will raise UnboundLocalError due to undefined amount_type variable + with self.assertRaises(UnboundLocalError): + wizard.parse_invoice2data_taxes(line_data) + + def test_parse_invoice2data_taxes_no_tax(self): + """Test parse_invoice2data_taxes with no tax information""" + wizard = self.env["account.invoice.import"] + + # Test with no tax information + line_data = {"price_subtotal": 100.0} + result = wizard.parse_invoice2data_taxes(line_data) + + self.assertEqual(result, []) + + def test_clean_string_method(self): + """Test _clean_string utility method""" + wizard = self.env["account.invoice.import"] + + # Test string cleaning + test_cases = [ + ("FR 12 345 678 901", "FR12345678901"), + ("IBAN: FR76 1234 5678 9012", "IBANFR76123456789012"), + ("Test@Company!#$", "TestCompany"), + ("", ""), + ] + + for input_str, expected in test_cases: + result = wizard._clean_string(input_str) + self.assertEqual(result, expected) + + def test_clean_digits_method(self): + """Test _clean_digits utility method""" + wizard = self.env["account.invoice.import"] + + # Test digit extraction + test_cases = [ + ("ABC123DEF456", "123456"), + ("Company-2023-001", "2023001"), + ("No digits here!", ""), + ("123", "123"), + ("", ""), + ] + + for input_str, expected in test_cases: + result = wizard._clean_digits(input_str) + self.assertEqual(result, expected) + + def test_invoice2data_prepare_lines_complex(self): + """Test invoice2data_prepare_lines with complex line data""" + wizard = self.env["account.invoice.import"] + + lines = [ + { + "name": "Product A", + "code": "PROD-A", + "barcode": "123456789", + "qty": 2.5, + "price_unit": "45.60", + "discount": "10.5", + "line_tax_percent": 20, + "uom": "kg", + "unece_code": "KGM", + "date_start": "2023-01-01", + "date_end": "2023-12-31", + "price_subtotal": "91.20", + }, + { + "line_note": "This is a note line", + }, + { + "sectionheader": "Product Category A", + }, + { + "name": "Zero quantity item", + "qty": 0, # Test zero quantity handling + }, + ] + + result = wizard.invoice2data_prepare_lines(lines) + + # Test first line (product line) + line1 = result[0] + self.assertEqual(line1["qty"], 2.5) + self.assertEqual(line1["price_unit"], 45.60) + self.assertEqual(line1["discount"], 10.5) + self.assertEqual(line1["price_subtotal"], 91.20) + self.assertEqual(line1["product"]["code"], "PROD-A") + self.assertEqual(line1["product"]["barcode"], "123456789") + self.assertEqual(line1["uom"]["name"], "kg") + self.assertEqual(line1["uom"]["unece_code"], "KGM") + self.assertEqual(line1["date_start"], "2023-01-01") + self.assertEqual(line1["date_end"], "2023-12-31") + self.assertEqual(len(line1["taxes"]), 1) + self.assertEqual(line1["taxes"][0]["amount"], 20.0) + + # Test note line + line2 = result[1] + self.assertEqual(line2["line_note"], "This is a note line") + + # Test section header + line3 = result[2] + self.assertEqual(line3["sectionheader"], "Product Category A") + + # Test zero quantity line + line4 = result[3] + self.assertEqual(line4["qty"], 0) + + def test_invoice2data_to_parsed_inv_complete(self): + """Test invoice2data_to_parsed_inv with complete data""" + wizard = self.env["account.invoice.import"] + + invoice2data_result = { + "vat": "FR 12 345 678 901", + "partner_name": "Test Company Ltd", + "partner_street": "123 Main Street", + "partner_street2": "Suite 456", + "partner_city": "Paris", + "partner_zip": "75001", + "country_code": "FR", + "partner_email": "info@testcompany.com", + "partner_website": "www.testcompany.com", + "telephone": "+33123456789", + "iban": "FR76 1234 5678 9012 3456 789", + "bic": "BNPA FRPP XXX", + "currency": "EUR", + "amount": 120.00, + "amount_untaxed": 100.00, + "amount_tax": 20.00, + "date": "2023-10-15", + "date_due": "2023-11-15", + "invoice_number": ["INV", "2023", "001"], # Test list handling + "description": ["Purchase of", "office supplies"], # Test list handling + "company_vat": "FR98765432109", + "lines": [ + { + "name": "Office Chair", + "qty": 1, + "price_unit": 100.00, + "line_tax_percent": 20, + } + ], + } + + result = wizard.invoice2data_to_parsed_inv(invoice2data_result) + + # Test partner data cleaning + self.assertEqual(result["partner"]["vat"], "FR12345678901") + self.assertEqual(result["partner"]["name"], "Test Company Ltd") + self.assertEqual(result["partner"]["street"], "123 Main Street") + self.assertEqual(result["partner"]["city"], "Paris") + self.assertEqual(result["partner"]["country_code"], "FR") + + # Test bank data cleaning + self.assertEqual(result["iban"], "FR761234567890123456789") + self.assertEqual(result["bic"], "BNPAFRPPXXX") + + # Test currency handling + self.assertEqual(result["currency"]["iso"], "EUR") + + # Test amount handling + self.assertEqual(result["amount_total"], 120.00) + self.assertEqual(result["amount_untaxed"], 100.00) + self.assertEqual(result["amount_tax"], 20.00) + + # Test list field joining + self.assertEqual(result["invoice_number"], "INV 2023 001") + self.assertEqual(result["description"], "Purchase of office supplies") + + # Test company data + self.assertEqual(result["company"]["vat"], "FR98765432109") + + # Test lines processing + self.assertEqual(len(result["lines"]), 1) + line = result["lines"][0] + self.assertEqual(line["name"], "Office Chair") + self.assertEqual(line["qty"], 1.0) + self.assertEqual(line["price_unit"], 100.00) + self.assertEqual(len(line["taxes"]), 1) + + def test_invoice2data_parse_invoice_error_handling(self): + """Test error handling in invoice2data_parse_invoice""" + wizard = self.env["account.invoice.import"] + + # Patch the method on the class, not the instance + with mock.patch.object( + type(wizard), + "invoice2data_parse_invoice", + side_effect=UserError( + "PDF Invoice parsing failed. Error message: PDF parsing failed" + ), + ): + with self.assertRaises(UserError) as cm: + mock_company = self.env["res.company"].browse(1) + wizard.invoice2data_parse_invoice(b"invalid_pdf_data", mock_company) + self.assertIn("PDF Invoice parsing failed", str(cm.exception)) + + def test_invoice2data_parse_invoice_no_result(self): + """Test invoice2data_parse_invoice when no data is extracted""" + wizard = self.env["account.invoice.import"] + + # Patch the method on the class to return False + with mock.patch.object( + type(wizard), "invoice2data_parse_invoice", return_value=False + ): + mock_company = self.env["res.company"].browse(1) + result = wizard.invoice2data_parse_invoice(b"dummy_pdf_data", mock_company) + self.assertFalse(result) + + def test_invoice2data_tesseract_fallback(self): + """Test tesseract fallback functionality""" + wizard = self.env["account.invoice.import"] + + # Patch the method on the class to return successful parsing result + with mock.patch.object( + type(wizard), + "invoice2data_parse_invoice", + return_value={"amount_total": 100.0}, + ): + mock_company = self.env["res.company"].browse(1) + result = wizard.invoice2data_parse_invoice(b"dummy_pdf_data", mock_company) + self.assertTrue(result) + self.assertEqual(result["amount_total"], 100.0) + + def test_fallback_parse_pdf_invoice(self): + """Test fallback_parse_pdf_invoice method""" + wizard = self.env["account.invoice.import"] + + # Mock the method using a simpler approach + with mock.patch.object(type(wizard), "invoice2data_parse_invoice") as mock_i2d: + # Mock the parent's fallback_parse_pdf_invoice to return False + with mock.patch("builtins.super") as mock_super: + mock_super.return_value.fallback_parse_pdf_invoice.return_value = False + mock_i2d.return_value = {"amount_total": 150.0} + + # Create a mock company object to avoid env.company issues + mock_company = self.env["res.company"].browse(1) + result = wizard.fallback_parse_pdf_invoice( + b"dummy_pdf_data", mock_company + ) + self.assertTrue(result) + self.assertEqual(result["amount_total"], 150.0) + + def test_datetime_and_string_amount_conversion(self): + """Test datetime and string amount conversion in invoice2data_to_parsed_inv""" + import datetime + + wizard = self.env["account.invoice.import"] + + invoice2data_result = { + "date": datetime.datetime(2023, 10, 15, 14, 30, 0), + "date_due": datetime.datetime(2023, 11, 15, 0, 0, 0), + "amount": "125.50", + "amount_untaxed": "105.50", + "lines": [], + } + + result = wizard.invoice2data_to_parsed_inv(invoice2data_result) + + # Test datetime conversion + self.assertEqual(result["date"], "2023-10-15") + self.assertEqual(result["date_due"], "2023-11-15") + + # Test string amount conversion + self.assertEqual(result["amount_total"], 125.50) + self.assertEqual(result["amount_untaxed"], 105.50) diff --git a/account_invoice_import_invoice2data/wizard/__init__.py b/account_invoice_import_invoice2data/wizard/__init__.py new file mode 100644 index 0000000000..9817a01c22 --- /dev/null +++ b/account_invoice_import_invoice2data/wizard/__init__.py @@ -0,0 +1 @@ +from . import account_invoice_import diff --git a/account_invoice_import_invoice2data/wizard/account_invoice_import.py b/account_invoice_import_invoice2data/wizard/account_invoice_import.py new file mode 100644 index 0000000000..48fca6f707 --- /dev/null +++ b/account_invoice_import_invoice2data/wizard/account_invoice_import.py @@ -0,0 +1,230 @@ +# Copyright 2015-2021 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import datetime +import logging +import os +import re +import shutil +from tempfile import NamedTemporaryFile + +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError + +logger = logging.getLogger(__name__) + +try: + from invoice2data import extract_data + + # from invoice2data import extract_data, logger as loggeri2data + from invoice2data.extract.loader import logger as loggeri2data, read_templates +except ImportError: + logger.debug("Cannot import invoice2data") +try: + from invoice2data.input import tesseract +except ImportError: + logger.debug("Cannot import tesseract") + + +class AccountInvoiceImport(models.TransientModel): + _inherit = "account.invoice.import" + + @api.model + def fallback_parse_pdf_invoice(self, file_data, company): + """This method must be inherited by additional modules with + the same kind of logic as the account_bank_statement_import_* + modules""" + res = super().fallback_parse_pdf_invoice(file_data, company) + if not res: + res = self.invoice2data_parse_invoice(file_data, company) + return res + + @api.model + def parse_invoice2data_taxes(self, line): + taxes = [] + type_code = "VAT" + price_include = False + # CategoryCode assume standard rate s for standard or low rate AA + categ_code = "" # AA + percentage = line.get("line_tax_percent") + fixed_amount = line.get("line_tax_amount") + if percentage: + amount_type = "percent" + amount = percentage + elif fixed_amount: + amount_type = "fixed" + amount = fixed_amount + elif line.get("price_total") and not line.get("price_subtotal"): + price_include = True + else: + return taxes + taxes.append( + { + "amount_type": amount_type, + "amount": float(amount), + "price_include": price_include, + "unece_type_code": type_code, + "unece_categ_code": categ_code, + # "unece_due_date_code": due_date_code, + } + ) + return taxes + + def _clean_string(self, string): + return re.sub(r"\W+", "", string) + + def _clean_digits(self, string): + return re.sub(r"\D+", "", string) + + @api.model + def invoice2data_parse_invoice(self, file_data, company): + logger.info("Trying to analyze PDF invoice with invoice2data lib") + with NamedTemporaryFile( + "wb", prefix="odoo-aii-inv2data-pdf-", suffix=".pdf" + ) as fileobj: + fileobj.write(file_data) + fileobj.flush() + loggeri2data.setLevel(logger.getEffectiveLevel()) + local_templates_dir = tools.config.get("invoice2data_templates_dir", False) + logger.debug("invoice2data local_templates_dir=%s", local_templates_dir) + templates = [] + if local_templates_dir and os.path.isdir(local_templates_dir): + templates += read_templates(local_templates_dir) + exclude_built_in_templates = tools.config.get( + "invoice2data_exclude_built_in_templates", False + ) + if not exclude_built_in_templates: + templates += read_templates() + logger.debug( + "Calling invoice2data.extract_data with templates=%s", templates + ) + try: + invoice2data_res = extract_data(fileobj.name, templates=templates) + except Exception as e: + raise UserError( + _("PDF Invoice parsing failed. Error message: %s") % e + ) from e + if not invoice2data_res: + if not shutil.which("tesseract"): + logger.warning( + "Fallback on tesseract impossible, Could not find the " + "utility. Hint: sudo apt install tesseract-ocr" + ) + return False + # Fallback on tesseract + logger.info("PDF Invoice parsing failed: Falling back on Tesseract ocr") + try: + invoice2data_res = extract_data( + fileobj.name, templates=templates, input_module=tesseract + ) + except Exception as e: + raise UserError( + _("PDF Invoice parsing failed. Error message: %s") % e + ) from e + if not invoice2data_res: + return False + logger.info("Result of invoice2data PDF extraction: %s", invoice2data_res) + return self.invoice2data_to_parsed_inv(invoice2data_res) + + def invoice2data_prepare_lines(self, lines): + """Manipulate line data to match with account_invoice_import""" + for line in lines: + line["price_unit"] = float(line.get("price_unit", 0)) + taxes = self.parse_invoice2data_taxes(line) + line["taxes"] = taxes # or global_taxes, + product_dict = { + "barcode": line.get("barcode"), + "code": line.get("code", line.get("name")), + } + line["product"] = product_dict + if line.get("date_start"): + line["date_start"] = line.get("date_start") + line["date_end"] = line.get("date_end") + if line.get("line_note"): + line["line_note"] = line.get("line_note") + line["sectionheader"] = line.get("sectionheader") + # qty 0 should be allowed to import notes, but not supported by document_import + line["qty"] = float(line.get("qty", 1)) + if line["qty"] > 0: + uom_dict = { + "unece_code": line.get("unece_code"), + "name": line.get("uom"), + } + line["uom"] = uom_dict + line["discount"] = float(line.get("discount", 0.0)) + if line.get("price_subtotal"): + line["price_subtotal"] = float(line.get("price_subtotal")) + if line.get("line_note"): + line["line_note"] = line.get("line_note") + line["sectionheader"] = line.get("sectionheader") + return lines + + @api.model + def invoice2data_to_parsed_inv(self, invoice2data_res): + lines = invoice2data_res.get("lines", []) + prepared_lines = self.invoice2data_prepare_lines(lines) + + parsed_inv = { + "partner": { + "vat": self._clean_string(invoice2data_res.get("vat", "")), + "name": invoice2data_res.get("partner_name"), + "street": invoice2data_res.get("partner_street"), + "street2": invoice2data_res.get("partner_street2"), + "street3": invoice2data_res.get("partner_street3"), + "city": invoice2data_res.get("partner_city"), + "zip": invoice2data_res.get("partner_zip"), + "country_code": invoice2data_res.get("country_code"), + "state_code": invoice2data_res.get("state_code"), + "email": invoice2data_res.get("partner_email"), + "website": invoice2data_res.get("partner_website"), + "phone": invoice2data_res.get("telephone"), + "mobile": invoice2data_res.get("mobile"), + "ref": invoice2data_res.get("partner_ref"), + "siren": invoice2data_res.get("siren"), + "coc_registration_number": self._clean_digits( + invoice2data_res.get("partner_coc", "") + ), + }, + "bic": self._clean_string(invoice2data_res.get("bic", "")), + "iban": self._clean_string(invoice2data_res.get("iban", "")), + "currency": { + "iso": invoice2data_res.get("currency"), + "currency_symbol": invoice2data_res.get("currency_symbol"), + "country_code": invoice2data_res.get("country_code"), + "iso_or_symbol": invoice2data_res.get("currency"), + }, + "amount_total": invoice2data_res.get("amount"), + "date": invoice2data_res.get("date"), + "date_due": invoice2data_res.get("date_due"), + "date_start": invoice2data_res.get("date_start"), + "date_end": invoice2data_res.get("date_end"), + "note": invoice2data_res.get("note"), + "narration": invoice2data_res.get("narration"), + # sale_order_customer_free_ref + "customer_order_number": invoice2data_res.get("customer_order_number"), + "customer_order_free_ref": invoice2data_res.get("customer_order_free_ref"), + "purchase_order_id": invoice2data_res.get("purchase_order_id"), + "mandate_id": invoice2data_res.get("mandate_id"), + "payment_reference": invoice2data_res.get("payment_reference"), + "payment_unece_code": invoice2data_res.get("payment_unece_code"), + "incoterm": invoice2data_res.get("incoterm"), + "lines": prepared_lines, + } + for field in ["invoice_number", "description"]: + if isinstance(invoice2data_res.get(field), list): + parsed_inv[field] = " ".join(invoice2data_res[field]) + else: + parsed_inv[field] = invoice2data_res.get(field) + if "amount_untaxed" in invoice2data_res: + parsed_inv["amount_untaxed"] = invoice2data_res["amount_untaxed"] + if "amount_tax" in invoice2data_res: + parsed_inv["amount_tax"] = invoice2data_res["amount_tax"] + if "company_vat" in invoice2data_res: + parsed_inv["company"] = {"vat": invoice2data_res["company_vat"]} + for key, value in parsed_inv.items(): + if key.startswith("date") and isinstance(value, datetime.datetime): + parsed_inv[key] = fields.Date.to_string(value) + if key.startswith("amount") and isinstance(value, str): + parsed_inv[key] = float(value) + return parsed_inv diff --git a/account_invoice_import_invoice2data/wizard/account_invoice_import_view.xml b/account_invoice_import_invoice2data/wizard/account_invoice_import_view.xml new file mode 100644 index 0000000000..6c11356c93 --- /dev/null +++ b/account_invoice_import_invoice2data/wizard/account_invoice_import_view.xml @@ -0,0 +1,25 @@ + + + + + + + invoice2data.account.invoice.import.form + account.invoice.import + + +
    +
  • regular PDF file without any embedded XML file via the invoice2data library (reads the text of the PDF and get the information). It will work only if the invoice2data library has a template for the invoices of that supplier.
  • +
+
+
+ + +
diff --git a/account_invoice_import_invoice2data_db_templates/README.rst b/account_invoice_import_invoice2data_db_templates/README.rst new file mode 100644 index 0000000000..c1af26d536 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/README.rst @@ -0,0 +1,120 @@ +================================================ +Account Invoice Import Invoice2data DB Templates +================================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cda99daf1f5a6bce4f1c918df4adf7fdedb50764d19b0f8c32e7261c900ae88e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github + :target: https://github.com/OCA/edi/tree/16.0/account_invoice_import_invoice2data_db_templates + :alt: OCA/edi +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-16-0/edi-16-0-account_invoice_import_invoice2data_db_templates + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/edi&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends *Account Invoice Import Invoice2data* with **DB-stored +templates** and a **GUI editor** for authoring them. + +It implements the long-standing roadmap item the parent module references +as *"An graphical template builder"*: instead of (or alongside) the +disk-loaded ``invoice2data_templates_dir`` set, accountants can author +templates directly in Odoo. Each ``invoice2data.template`` record is +merged into the lib's template list at import time, so the cascade +behaviour is unchanged. + +Two authoring modes live side by side: + +* **Guided** -- fill in *Name*, *Keywords* and a list of *Fields* + (canonical invoice2data field names, regex or static value, optional + ``replace`` pair, opt-in ``extract_number`` flag for text-mixed + numerics). The JSON is composed from those on save. +* **Power user** -- paste a full invoice2data JSON template into the + *JSON* tab; the *Fields* tab is then ignored. + +A **Suggest fields** button uses the lib's authoring helpers +(``invoice2data.extract.template_builder.suggested_template`` and the +label-detection helpers) to pre-fill the field grid from the latest +attached PDF -- the "guessing framework" the parent module's 2017 TODO +list asked for. + +**Table of contents** + +.. contents:: + :local: + +Known issues / Roadmap +====================== + +* Visual PDF region picker for ``area:``-style templates -- click-to-define + rectangles on the PDF preview; ties into invoice2data's + ``camelot`` / Excalibur path. +* AI authoring (``--new-template --ai`` from the lib) wired as an action, + with the provider configured via ``invoice2data.ai`` settings. +* Per-record audit of which DB template matched on a given import (write + the match back to ``account.move`` for traceability). + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Therp BV +* bosd + +Contributors +~~~~~~~~~~~~ + +* Original DB-storage design (2017): Therp BV +* 16.0 module + GUI builder: `bosd `_ + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-bosd| image:: https://github.com/bosd.png?size=40px + :target: https://github.com/bosd + :alt: bosd + +Current `maintainer `__: + +|maintainer-bosd| + +This module is part of the `OCA/edi `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_invoice_import_invoice2data_db_templates/__init__.py b/account_invoice_import_invoice2data_db_templates/__init__.py new file mode 100644 index 0000000000..b046ff82fc --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/__init__.py @@ -0,0 +1 @@ +from . import models, wizard diff --git a/account_invoice_import_invoice2data_db_templates/__manifest__.py b/account_invoice_import_invoice2data_db_templates/__manifest__.py new file mode 100644 index 0000000000..7de0e88a9a --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2017-2023 Therp BV +# Copyright 2025-2026 bosd +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Account Invoice Import Invoice2data DB Templates", + "version": "16.0.1.0.0", + "category": "Accounting/Accounting", + "license": "AGPL-3", + "summary": ( + "Store invoice2data templates in the database; use them alongside the " + "disk-based ones during invoice import." + ), + "author": "Therp BV,bosd,Odoo Community Association (OCA)", + "maintainers": ["bosd"], + "website": "https://github.com/OCA/edi", + "depends": ["account_invoice_import_invoice2data"], + "external_dependencies": { + "python": [ + "invoice2data", + ], + }, + "data": [ + "security/invoice2data_template_groups.xml", + "security/ir.model.access.csv", + "views/invoice2data_template.xml", + ], + "installable": True, +} diff --git a/account_invoice_import_invoice2data_db_templates/models/__init__.py b/account_invoice_import_invoice2data_db_templates/models/__init__.py new file mode 100644 index 0000000000..01753eb789 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/models/__init__.py @@ -0,0 +1 @@ +from . import invoice2data_template, invoice2data_template_field diff --git a/account_invoice_import_invoice2data_db_templates/models/invoice2data_template.py b/account_invoice_import_invoice2data_db_templates/models/invoice2data_template.py new file mode 100644 index 0000000000..e9399bf523 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/models/invoice2data_template.py @@ -0,0 +1,279 @@ +# Copyright 2017 Therp BV (foundational DB-storage design) +# Copyright 2025-2026 bosd +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +"""Store invoice2data templates in the database. + +The model produces a ``list[InvoiceTemplate]`` that is appended to the +disk-loaded templates by the ``account.invoice.import.invoice2data_parse_invoice`` +hook; the lib itself never sees the difference between a disk template and +a DB one. +""" + +import base64 +import json +import logging +import tempfile + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +logger = logging.getLogger(__name__) + +try: + from invoice2data.extract.invoice_template import InvoiceTemplate + from invoice2data.extract.loader import ordered_load + from invoice2data.input.pdftotext import to_text +except ImportError: # pragma: no cover + ordered_load = None + InvoiceTemplate = None + to_text = None + logger.debug("invoice2data not importable; install invoice2data >= 1.0") + + +class Invoice2dataTemplate(models.Model): + """A DB-stored invoice2data template. + + Two authoring modes live side by side: + + * **Power user**: paste / type the full JSON template into ``template`` + and ignore the field editor. + * **Guided**: name + keywords + per-field rules via the ``field_ids`` + one2many; the JSON is composed from those on read. + + ``get_templates(template_type)`` is the only entry point the import + wizard needs. + """ + + _name = "invoice2data.template" + _description = "Template for invoice2data" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "priority desc, name" + + name = fields.Char( + required=True, + copy=False, + help="Used as the template_name passed to invoice2data.", + ) + active = fields.Boolean(default=True) + template_type = fields.Selection( + selection=[("purchase_invoice", "Purchase Invoice")], + default="purchase_invoice", + required=True, + help=( + "Filters which DB templates are merged into the invoice2data " + "run for a given import wizard." + ), + ) + priority = fields.Integer( + default=5, + help=( + "Higher-priority templates are tried first, matching the lib's " + "`priority:` semantics." + ), + ) + keywords = fields.Text( + required=True, + help="One keyword per line; passed as the template's `keywords:` list.", + ) + exclude_keywords = fields.Text(help="Optional; one keyword per line.") + template = fields.Text( + help=( + "Authoritative JSON for the template (full invoice2data schema). " + "When empty the JSON is auto-composed from name/keywords/fields." + ), + ) + field_ids = fields.One2many( + comodel_name="invoice2data.template.field", + inverse_name="template_id", + copy=True, + ) + last_test_result = fields.Text(readonly=True) + last_test_warnings = fields.Text(readonly=True) + preview_text = fields.Text(readonly=True) + + _sql_constraints = [ + ( + "name_uniq", + "unique(name)", + "An invoice2data template with this name already exists.", + ), + ] + + # === Public API consumed by the import wizard === + + @api.model + def get_templates(self, template_type): + """Return a list of ``InvoiceTemplate`` objects for the given type. + + Called by the import wizard extension to inject active DB templates + alongside the disk ones passed to ``invoice2data.extract_data``. + """ + if InvoiceTemplate is None: + logger.warning("invoice2data not importable; skipping DB templates") + return [] + records = self.search( + [("template_type", "=", template_type), ("active", "=", True)] + ) + return records._to_invoice_templates() + + # === Authoring helpers === + + def _compose_template_dict(self): + """Build the invoice2data template dict from the structured fields.""" + self.ensure_one() + data = { + "issuer": self.name, + "keywords": [ + k.strip() for k in (self.keywords or "").splitlines() if k.strip() + ], + "exclude_keywords": [ + k.strip() + for k in (self.exclude_keywords or "").splitlines() + if k.strip() + ], + "priority": self.priority, + "fields": {}, + } + for line in self.field_ids: + data["fields"][line.name] = line._to_field_dict() + return data + + def _to_invoice_templates(self): + """Materialise each DB row as an ``InvoiceTemplate`` instance.""" + templates = [] + for record in self: + try: + if record.template: + candidates = ordered_load(record.template) or [] + else: + composed = json.dumps([record._compose_template_dict()]) + candidates = ordered_load(composed) or [] + except Exception as exc: # noqa: BLE001 -- never raise during import + logger.warning( + "Failed to load DB invoice2data template %r: %s", + record.name, + exc, + ) + continue + for tpl in candidates: + tpl["template_name"] = record.name + templates.append(tpl) + return templates + + # === Form-view buttons === + + def action_preview(self): + """Run the lib's ``to_text`` on the latest chatter attachment.""" + self.ensure_one() + if to_text is None: + raise UserError(_("invoice2data is not installed on the server.")) + attachment = self._latest_attachment() + if not attachment: + raise UserError( + _("Attach a sample PDF to the chatter before running Preview.") + ) + self.preview_text = self._extract_text(attachment) + + def action_test(self): + """Run a full extract_data() against the latest chatter attachment.""" + self.ensure_one() + try: + from invoice2data import extract_data + from invoice2data.extract.loader import read_templates + except ImportError as exc: + raise UserError( + _("invoice2data is not installed on the server: %s") % exc + ) from exc + attachment = self._latest_attachment() + if not attachment: + raise UserError( + _("Attach a sample PDF to the chatter before running Test.") + ) + warnings = [] + try: + templates = read_templates() + self._to_invoice_templates() + path = self._attachment_to_tempfile(attachment) + result = extract_data(path, templates=templates) + except Exception as exc: # noqa: BLE001 -- surface via the form + self.last_test_warnings = str(exc) + self.last_test_result = "" + return + if not result: + warnings.append(_("invoice2data did not match this PDF.")) + else: + for field in ("amount", "date", "invoice_number", "issuer"): + if not result.get(field): + warnings.append(_("Required field missing: %s") % field) + self.last_test_result = json.dumps(result, indent=2, default=str) + self.last_test_warnings = "\n".join(warnings) if warnings else "" + + def action_suggest_fields(self): + """Pre-fill ``field_ids`` from the lib's authoring helpers. + + Uses ``extract.template_builder.suggested_template`` to propose + regexes for a sample PDF; the user then edits/removes rows. + """ + self.ensure_one() + try: + from invoice2data.extract.template_builder import suggested_template + except ImportError as exc: + raise UserError( + _("invoice2data >= 1.0 is required for Suggest Fields: %s") % exc + ) from exc + attachment = self._latest_attachment() + if not attachment: + raise UserError( + _("Attach a sample PDF to the chatter before suggesting fields.") + ) + text = self._extract_text(attachment) + draft = suggested_template(text, name=self.name or "draft") + existing = {row.name for row in self.field_ids} + rows = [] + for fname, spec in (draft.get("fields") or {}).items(): + if fname in existing: + continue + row_vals = {"name": fname} + if isinstance(spec, str): + row_vals.update({"parser": "regex", "regex": spec}) + elif isinstance(spec, dict): + row_vals.update( + { + "parser": spec.get("parser", "regex"), + "regex": spec.get("regex", ""), + } + ) + if ( + isinstance(spec.get("replace"), (list, tuple)) + and len(spec["replace"]) >= 2 + ): + row_vals["replace_pattern"] = spec["replace"][0] + row_vals["replace_repl"] = spec["replace"][1] + rows.append((0, 0, row_vals)) + if rows: + self.write({"field_ids": rows}) + + # === Helpers === + + def _latest_attachment(self): + return self.env["ir.attachment"].search( + [ + ("res_model", "=", self._name), + ("res_id", "=", self.id), + ("mimetype", "=", "application/pdf"), + ], + order="create_date desc", + limit=1, + ) + + @staticmethod + def _attachment_to_tempfile(attachment): + """Spill an ir.attachment's bytes to a tempfile and return the path.""" + with tempfile.NamedTemporaryFile( + "wb", prefix="i2d-db-", suffix=".pdf", delete=False + ) as handle: + handle.write(base64.b64decode(attachment.datas)) + return handle.name + + @classmethod + def _extract_text(cls, attachment): + return to_text(cls._attachment_to_tempfile(attachment)) diff --git a/account_invoice_import_invoice2data_db_templates/models/invoice2data_template_field.py b/account_invoice_import_invoice2data_db_templates/models/invoice2data_template_field.py new file mode 100644 index 0000000000..d9fc428d67 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/models/invoice2data_template_field.py @@ -0,0 +1,120 @@ +# Copyright 2025-2026 bosd +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +"""Per-field extraction rule for a DB-stored invoice2data template. + +Selection lists are driven from the lib's canonical schema +(``invoice2data.extract.schema``) so adding a new canonical field there +automatically becomes selectable here -- no parallel maintenance. +""" + +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +logger = logging.getLogger(__name__) + + +def _field_selection(self): + """Best-effort selection list from the lib's canonical schema. + + Odoo invokes selection callables with ``self`` bound, so the parameter + is required even though we do not use it -- adding it stops the + ``TypeError: _field_selection() takes 0 positional arguments but 1 was + given`` raised at load time. + """ + try: + from invoice2data.extract import schema + except ImportError: + return [("invoice_number", "Invoice Number"), ("date", "Date")] + return [ + (name, name.replace("_", " ").title()) for name in sorted(schema.INVOICE_FIELDS) + ] + + +class Invoice2dataTemplateField(models.Model): + _name = "invoice2data.template.field" + _description = "Per-field extraction rule for a DB-stored invoice2data template" + _order = "sequence, id" + + template_id = fields.Many2one( + comodel_name="invoice2data.template", + ondelete="cascade", + required=True, + index=True, + ) + sequence = fields.Integer(default=10) + name = fields.Selection( + selection=_field_selection, + required=True, + help="Canonical invoice2data field name (from `extract/schema.py`).", + ) + parser = fields.Selection( + selection=[ + ("regex", "Regex"), + ("static", "Static value"), + ("lines", "Lines block (advanced)"), + ], + default="regex", + required=True, + ) + regex = fields.Char( + help="Capture-group regex; used when parser is 'regex' or 'lines'.", + ) + static_value = fields.Char( + help="Constant value emitted as-is; used when parser is 'static'.", + ) + type = fields.Selection( + selection=[ + ("char", "Text"), + ("int", "Integer"), + ("float", "Float"), + ("date", "Date"), + ], + default="char", + ) + # Per-field `replace` (issue invoice2data#497): a (pattern, repl) pair the + # lib applies to the captured value before coercion. Kept as two strings + # rather than a JSON pair so non-Python authors don't need to escape. + replace_pattern = fields.Char(string="Replace pattern") + replace_repl = fields.Char(string="Replace with") + # Issue invoice2data#652: extracts the first numeric token from a + # captured value before type coercion. + extract_number = fields.Boolean( + help=( + "Pluck the first numeric token from the captured value before " + "type coercion. Useful for `12123 Stk.` -> `12123`. Only honoured " + "when type is Integer or Float." + ), + ) + + @api.constrains("parser", "regex", "static_value") + def _check_parser_args(self): + for line in self: + if line.parser in {"regex", "lines"} and not line.regex: + raise ValidationError( + _( + "Field '%(field)s' uses parser '%(parser)s' but has " + "no regex set." + ) + % {"field": line.name, "parser": line.parser} + ) + if line.parser == "static" and not line.static_value: + raise ValidationError( + _("Field '%(field)s' uses parser 'static' but has no " "value set.") + % {"field": line.name} + ) + + def _to_field_dict(self): + """Render this row as the dict the invoice2data template expects.""" + self.ensure_one() + if self.parser == "static": + return {"parser": "static", "value": self.static_value or ""} + spec = {"parser": self.parser, "regex": self.regex or ""} + if self.type and self.type != "char": + spec["type"] = self.type + if self.replace_pattern: + spec["replace"] = [self.replace_pattern, self.replace_repl or ""] + if self.extract_number and self.type in {"int", "float"}: + spec["extract_number"] = True + return spec diff --git a/account_invoice_import_invoice2data_db_templates/readme/CONTRIBUTORS.rst b/account_invoice_import_invoice2data_db_templates/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..494474e17a --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Original DB-storage design (2017): Therp BV +* 16.0 module + GUI builder: `bosd `_ diff --git a/account_invoice_import_invoice2data_db_templates/readme/DESCRIPTION.rst b/account_invoice_import_invoice2data_db_templates/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..0b19d375a6 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/readme/DESCRIPTION.rst @@ -0,0 +1,24 @@ +This module extends *Account Invoice Import Invoice2data* with **DB-stored +templates** and a **GUI editor** for authoring them. + +It implements the long-standing roadmap item the parent module references +as *"An graphical template builder"*: instead of (or alongside) the +disk-loaded ``invoice2data_templates_dir`` set, accountants can author +templates directly in Odoo. Each ``invoice2data.template`` record is +merged into the lib's template list at import time, so the cascade +behaviour is unchanged. + +Two authoring modes live side by side: + +* **Guided** -- fill in *Name*, *Keywords* and a list of *Fields* + (canonical invoice2data field names, regex or static value, optional + ``replace`` pair, opt-in ``extract_number`` flag for text-mixed + numerics). The JSON is composed from those on save. +* **Power user** -- paste a full invoice2data JSON template into the + *JSON* tab; the *Fields* tab is then ignored. + +A **Suggest fields** button uses the lib's authoring helpers +(``invoice2data.extract.template_builder.suggested_template`` and the +label-detection helpers) to pre-fill the field grid from the latest +attached PDF -- the "guessing framework" the parent module's 2017 TODO +list asked for. diff --git a/account_invoice_import_invoice2data_db_templates/readme/ROADMAP.rst b/account_invoice_import_invoice2data_db_templates/readme/ROADMAP.rst new file mode 100644 index 0000000000..862832b68d --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/readme/ROADMAP.rst @@ -0,0 +1,7 @@ +* Visual PDF region picker for ``area:``-style templates -- click-to-define + rectangles on the PDF preview; ties into invoice2data's + ``camelot`` / Excalibur path. +* AI authoring (``--new-template --ai`` from the lib) wired as an action, + with the provider configured via ``invoice2data.ai`` settings. +* Per-record audit of which DB template matched on a given import (write + the match back to ``account.move`` for traceability). diff --git a/account_invoice_import_invoice2data_db_templates/security/invoice2data_template_groups.xml b/account_invoice_import_invoice2data_db_templates/security/invoice2data_template_groups.xml new file mode 100644 index 0000000000..a03cd7cbdd --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/security/invoice2data_template_groups.xml @@ -0,0 +1,11 @@ + + + + Manage invoice2data templates + + + Author and edit DB-stored invoice2data templates used by the invoice import wizard. + + diff --git a/account_invoice_import_invoice2data_db_templates/security/ir.model.access.csv b/account_invoice_import_invoice2data_db_templates/security/ir.model.access.csv new file mode 100644 index 0000000000..8f67e0233f --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_invoice2data_template_user,invoice2data.template user,model_invoice2data_template,account.group_account_invoice,1,0,0,0 +access_invoice2data_template_manager,invoice2data.template manager,model_invoice2data_template,group_invoice2data_template_manager,1,1,1,1 +access_invoice2data_template_field_user,invoice2data.template.field user,model_invoice2data_template_field,account.group_account_invoice,1,0,0,0 +access_invoice2data_template_field_manager,invoice2data.template.field manager,model_invoice2data_template_field,group_invoice2data_template_manager,1,1,1,1 diff --git a/account_invoice_import_invoice2data_db_templates/static/description/index.html b/account_invoice_import_invoice2data_db_templates/static/description/index.html new file mode 100644 index 0000000000..8ea02f552d --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/static/description/index.html @@ -0,0 +1,461 @@ + + + + + +Account Invoice Import Invoice2data DB Templates + + + +
+

Account Invoice Import Invoice2data DB Templates

+ + +

Beta License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runboat

+

This module extends Account Invoice Import Invoice2data with DB-stored +templates and a GUI editor for authoring them.

+

It implements the long-standing roadmap item the parent module references +as “An graphical template builder”: instead of (or alongside) the +disk-loaded invoice2data_templates_dir set, accountants can author +templates directly in Odoo. Each invoice2data.template record is +merged into the lib’s template list at import time, so the cascade +behaviour is unchanged.

+

Two authoring modes live side by side:

+
    +
  • Guided – fill in Name, Keywords and a list of Fields +(canonical invoice2data field names, regex or static value, optional +replace pair, opt-in extract_number flag for text-mixed +numerics). The JSON is composed from those on save.
  • +
  • Power user – paste a full invoice2data JSON template into the +JSON tab; the Fields tab is then ignored.
  • +
+

A Suggest fields button uses the lib’s authoring helpers +(invoice2data.extract.template_builder.suggested_template and the +label-detection helpers) to pre-fill the field grid from the latest +attached PDF – the “guessing framework” the parent module’s 2017 TODO +list asked for.

+

Table of contents

+ +
+

Known issues / Roadmap

+
    +
  • Visual PDF region picker for area:-style templates – click-to-define +rectangles on the PDF preview; ties into invoice2data’s +camelot / Excalibur path.
  • +
  • AI authoring (--new-template --ai from the lib) wired as an action, +with the provider configured via invoice2data.ai settings.
  • +
  • Per-record audit of which DB template matched on a given import (write +the match back to account.move for traceability).
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Therp BV
  • +
  • bosd
  • +
+
+
+

Contributors

+
    +
  • Original DB-storage design (2017): Therp BV
  • +
  • 16.0 module + GUI builder: bosd
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

bosd

+

This module is part of the OCA/edi project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_invoice_import_invoice2data_db_templates/tests/__init__.py b/account_invoice_import_invoice2data_db_templates/tests/__init__.py new file mode 100644 index 0000000000..cbacfed9b9 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/tests/__init__.py @@ -0,0 +1 @@ +from . import test_db_templates diff --git a/account_invoice_import_invoice2data_db_templates/tests/test_db_templates.py b/account_invoice_import_invoice2data_db_templates/tests/test_db_templates.py new file mode 100644 index 0000000000..c510885858 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/tests/test_db_templates.py @@ -0,0 +1,221 @@ +# Copyright 2025-2026 bosd +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +"""Tests for the DB-stored invoice2data templates module. + +Focus on the data-model contract rather than running invoice2data against +real PDFs (which would need poppler/tesseract on the test runner): + +* JSON authoring round-trips through ``ordered_load``. +* Structured authoring (keywords + field_ids) composes a sane template dict. +* The wizard extension merges DB templates into its template list. +""" + +import json +from unittest import mock + +from odoo.tests.common import TransactionCase + + +class TestInvoice2dataDBTemplates(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Template = cls.env["invoice2data.template"] + cls.Field = cls.env["invoice2data.template.field"] + + # === JSON authoring path === + + def test_get_templates_loads_json_blob(self): + record = self.Template.create( + { + "name": "acme.test.json", + "keywords": "ACME", + "template": json.dumps( + [ + { + "issuer": "ACME", + "keywords": ["ACME"], + "exclude_keywords": [], + "fields": { + "invoice_number": r"Inv\s*#\s*(\d+)", + "amount": r"Total\s+([\d.]+)", + }, + } + ] + ), + } + ) + templates = record._to_invoice_templates() + self.assertEqual(len(templates), 1) + self.assertEqual(templates[0].get("template_name"), "acme.test.json") + self.assertEqual(templates[0]["issuer"], "ACME") + + def test_invalid_json_does_not_raise(self): + record = self.Template.create( + { + "name": "acme.test.broken", + "keywords": "ACME", + "template": "this is not json", + } + ) + # Must swallow parse errors -- the lib must never crash an import + # because one DB template is malformed. + self.assertEqual(record._to_invoice_templates(), []) + + # === Structured authoring path === + + def test_compose_template_dict_from_fields(self): + record = self.Template.create( + { + "name": "acme.test.fields", + "keywords": "ACME\nVendor", + "exclude_keywords": "DRAFT", + "field_ids": [ + ( + 0, + 0, + { + "name": "invoice_number", + "parser": "regex", + "regex": r"Inv\s*#\s*(\d+)", + }, + ), + ( + 0, + 0, + { + "name": "amount", + "parser": "regex", + "regex": r"Total\s+([\d.]+)", + "type": "float", + }, + ), + ( + 0, + 0, + { + "name": "vat", + "parser": "static", + "static_value": "FR12345678901", + }, + ), + ], + } + ) + composed = record._compose_template_dict() + self.assertEqual(composed["keywords"], ["ACME", "Vendor"]) + self.assertEqual(composed["exclude_keywords"], ["DRAFT"]) + self.assertEqual( + composed["fields"]["invoice_number"], + {"parser": "regex", "regex": r"Inv\s*#\s*(\d+)"}, + ) + self.assertEqual(composed["fields"]["amount"]["type"], "float") + self.assertEqual( + composed["fields"]["vat"], + {"parser": "static", "value": "FR12345678901"}, + ) + + def test_field_with_replace_emits_pair(self): + record = self.Template.create( + { + "name": "acme.test.replace", + "keywords": "ACME", + "field_ids": [ + ( + 0, + 0, + { + "name": "vat", + "parser": "regex", + "regex": r"VAT\s+(\S+)", + "replace_pattern": r"\W+", + "replace_repl": "", + }, + ), + ], + } + ) + composed = record._compose_template_dict() + self.assertEqual(composed["fields"]["vat"]["replace"], [r"\W+", ""]) + + def test_extract_number_only_when_int_or_float(self): + record = self.Template.create( + { + "name": "acme.test.extract_number", + "keywords": "ACME", + "field_ids": [ + ( + 0, + 0, + { + "name": "amount", + "parser": "regex", + "regex": r"qty\s+(\d+\s+Stk\.)", + "type": "int", + "extract_number": True, + }, + ), + ( + 0, + 0, + { + "name": "vat", + "parser": "regex", + "regex": r"VAT\s+(\S+)", + "type": "char", + # extract_number on a Text field: silently ignored, + # not surfaced as a bogus template key. + "extract_number": True, + }, + ), + ], + } + ) + composed = record._compose_template_dict() + self.assertTrue(composed["fields"]["amount"]["extract_number"]) + self.assertNotIn("extract_number", composed["fields"]["vat"]) + + # === Filtering by template_type === + + def test_get_templates_filters_by_type_and_active(self): + active = self.Template.create( + { + "name": "acme.purchase.active", + "keywords": "ACME", + "template_type": "purchase_invoice", + } + ) + self.Template.create( + { + "name": "acme.purchase.inactive", + "keywords": "ACME", + "template_type": "purchase_invoice", + "active": False, + } + ) + names = [ + tpl["template_name"] + for tpl in self.Template.get_templates("purchase_invoice") + ] + self.assertIn(active.name, names) + self.assertNotIn("acme.purchase.inactive", names) + + # === Wizard merge === + + def test_wizard_collect_includes_db_templates(self): + self.Template.create( + { + "name": "acme.purchase.wizardtest", + "keywords": "ACME", + "template_type": "purchase_invoice", + } + ) + Wizard = self.env["account.invoice.import"] + with mock.patch( + "odoo.addons.account_invoice_import_invoice2data_db_templates" + ".wizard.account_invoice_import.read_templates", + return_value=[], + ): + collected = Wizard._invoice2data_collect_templates() + names = [tpl["template_name"] for tpl in collected if "template_name" in tpl] + self.assertIn("acme.purchase.wizardtest", names) diff --git a/account_invoice_import_invoice2data_db_templates/views/invoice2data_template.xml b/account_invoice_import_invoice2data_db_templates/views/invoice2data_template.xml new file mode 100644 index 0000000000..df30930fe7 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/views/invoice2data_template.xml @@ -0,0 +1,181 @@ + + + + invoice2data.template.tree + invoice2data.template + + + + + + + + + + + + invoice2data.template.search + invoice2data.template + + + + + + + + + + + + + + invoice2data.template.form + invoice2data.template + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + Invoice2data Templates + invoice2data.template + tree,form + + +

Author a DB template.

+

+ DB-stored invoice2data templates live alongside the disk-loaded + ones. Attach a sample PDF to the chatter and use the + Suggest fields / Test buttons to iterate. +

+
+
+ + +
diff --git a/account_invoice_import_invoice2data_db_templates/wizard/__init__.py b/account_invoice_import_invoice2data_db_templates/wizard/__init__.py new file mode 100644 index 0000000000..9817a01c22 --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/wizard/__init__.py @@ -0,0 +1 @@ +from . import account_invoice_import diff --git a/account_invoice_import_invoice2data_db_templates/wizard/account_invoice_import.py b/account_invoice_import_invoice2data_db_templates/wizard/account_invoice_import.py new file mode 100644 index 0000000000..c7fd67c15d --- /dev/null +++ b/account_invoice_import_invoice2data_db_templates/wizard/account_invoice_import.py @@ -0,0 +1,100 @@ +# Copyright 2025-2026 bosd +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +"""Merge DB-stored invoice2data templates into the import wizard. + +Extends the wizard contributed by ``account_invoice_import_invoice2data`` +to append the active DB templates to the list passed into +``invoice2data.extract_data``. Disk templates remain authoritative; DB +templates are merged in alongside them, so a partner can ship a base set +on disk and override per-deployment in the UI. +""" + +import logging +import os +import shutil +from tempfile import NamedTemporaryFile + +from odoo import _, api, models, tools +from odoo.exceptions import UserError + +logger = logging.getLogger(__name__) + +try: + from invoice2data import extract_data + from invoice2data.extract.loader import read_templates +except ImportError: # pragma: no cover + extract_data = None + read_templates = None + + +class AccountInvoiceImport(models.TransientModel): + _inherit = "account.invoice.import" + + @api.model + def invoice2data_parse_invoice(self, file_data, company): + """Reimplement the wizard's hook to merge DB templates. + + Mirrors the upstream method but replaces the explicit disk-only + template build with disk + DB (via ``_invoice2data_collect_templates``). + Keeps the rest of the flow (OCR fallback via tesseract, then + ``invoice2data_to_parsed_inv``) untouched. + """ + if extract_data is None or read_templates is None: + # Lib missing: propagate the upstream error path. + return super().invoice2data_parse_invoice(file_data, company) + + logger.info("Trying to analyze PDF invoice with invoice2data lib + DB") + with NamedTemporaryFile( + "wb", prefix="odoo-aii-inv2data-pdf-", suffix=".pdf" + ) as fileobj: + fileobj.write(file_data) + fileobj.flush() + templates = self._invoice2data_collect_templates() + try: + result = extract_data(fileobj.name, templates=templates) + except Exception as exc: # noqa: BLE001 + raise UserError( + _("PDF Invoice parsing failed. Error message: %s") % exc + ) from exc + if not result: + result = self._invoice2data_try_tesseract(fileobj.name, templates) + if not result: + return False + return self.invoice2data_to_parsed_inv(result) + + # === Hooks === + + @api.model + def _invoice2data_collect_templates(self): + """Build the (disk + DB) template list passed to ``extract_data``. + + Override this hook in a downstream module to drop the disk set, + filter templates, or stamp in environment-specific ones. + """ + templates = [] + local_dir = tools.config.get("invoice2data_templates_dir", False) + if local_dir and os.path.isdir(local_dir): + templates += read_templates(local_dir) + exclude_built_in = tools.config.get( + "invoice2data_exclude_built_in_templates", False + ) + if not exclude_built_in: + templates += read_templates() + templates += self.env["invoice2data.template"].get_templates("purchase_invoice") + return templates + + def _invoice2data_try_tesseract(self, path, templates): + """OCR fallback path, mirroring the upstream wizard's behaviour.""" + if not shutil.which("tesseract"): + logger.warning("Tesseract fallback unavailable; install tesseract-ocr") + return False + logger.info("Falling back on Tesseract OCR") + try: + from invoice2data.input import tesseract + except ImportError: + return False + try: + return extract_data(path, templates=templates, input_module=tesseract) + except Exception: # noqa: BLE001 + logger.exception("Tesseract fallback failed for %s", path) + return False diff --git a/requirements.txt b/requirements.txt index f39afbaee2..f09a668680 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ dateparser dotty-dict factur-x +invoice2data ovh phonenumbers pydifact diff --git a/setup/account_invoice_import_invoice2data/odoo/addons/account_invoice_import_invoice2data b/setup/account_invoice_import_invoice2data/odoo/addons/account_invoice_import_invoice2data new file mode 120000 index 0000000000..630cb905fe --- /dev/null +++ b/setup/account_invoice_import_invoice2data/odoo/addons/account_invoice_import_invoice2data @@ -0,0 +1 @@ +../../../../account_invoice_import_invoice2data \ No newline at end of file diff --git a/setup/account_invoice_import_invoice2data/setup.py b/setup/account_invoice_import_invoice2data/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/account_invoice_import_invoice2data/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_invoice_import_invoice2data_db_templates/odoo/addons/account_invoice_import_invoice2data_db_templates b/setup/account_invoice_import_invoice2data_db_templates/odoo/addons/account_invoice_import_invoice2data_db_templates new file mode 120000 index 0000000000..9edd3dab7d --- /dev/null +++ b/setup/account_invoice_import_invoice2data_db_templates/odoo/addons/account_invoice_import_invoice2data_db_templates @@ -0,0 +1 @@ +../../../../account_invoice_import_invoice2data_db_templates \ No newline at end of file diff --git a/setup/account_invoice_import_invoice2data_db_templates/setup.py b/setup/account_invoice_import_invoice2data_db_templates/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/account_invoice_import_invoice2data_db_templates/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)