summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenjamin Frantzen (bfr) <bfr@odoo.com>2020-05-26 12:53:23 +0000
committerLaurent Smet <las@odoo.com>2020-08-17 10:22:01 +0000
commit013936261b26c601ce55015dbbbb99a7313a9a39 (patch)
tree5bdfc89dbdba487644c23c8dbe9b4dd31b614279
parent34c4752eeafc33bd88e18c7f0408dfda17dfa8d8 (diff)
[IMP] account_edi: added flows to manage web-services and payments.
Added edi.documents representing an electronic document for a move and an edi.format. A format can be asynchronous if it needs to call a web-service to generate the document, it will then be generated by the CRON (otherwise it's generated in post). The formats can support payments if needed (can be generated immediately or by the CRON). Added support for errors and related views. Setting defaults format on a journal can be done automatically (based on a hook). Added tests : xml comparaison with diff and helpers to test a EDI import/export --task: 2247368
-rw-r--r--addons/account/models/account_bank_statement.py1
-rw-r--r--addons/account/models/account_journal.py1
-rw-r--r--addons/account/models/account_move.py6
-rw-r--r--addons/account/models/account_payment.py1
-rw-r--r--addons/account/models/account_tax.py1
-rw-r--r--addons/account/tests/account_test_xml.py96
-rw-r--r--addons/account/tests/common.py96
-rw-r--r--addons/account/views/account_bank_statement_views.xml1
-rw-r--r--addons/account/views/account_journal_views.xml1
-rw-r--r--addons/account/views/account_move_views.xml5
-rw-r--r--addons/account/views/account_payment_view.xml1
-rw-r--r--addons/account/views/account_tax_views.xml1
-rw-r--r--addons/account/wizard/account_payment_register.py1
-rw-r--r--addons/account/wizard/account_payment_register_views.xml1
-rw-r--r--addons/account_edi/__manifest__.py6
-rw-r--r--addons/account_edi/data/cron.xml11
-rw-r--r--addons/account_edi/models/__init__.py3
-rw-r--r--addons/account_edi/models/account_edi_document.py177
-rw-r--r--addons/account_edi/models/account_edi_format.py172
-rw-r--r--addons/account_edi/models/account_journal.py47
-rw-r--r--addons/account_edi/models/account_move.py275
-rw-r--r--addons/account_edi/models/account_payment.py11
-rw-r--r--addons/account_edi/models/ir_attachment.py10
-rw-r--r--addons/account_edi/models/mail_template.py24
-rw-r--r--addons/account_edi/security/ir.model.access.csv3
-rw-r--r--addons/account_edi/tests/__init__.py5
-rw-r--r--addons/account_edi/tests/common.py82
-rw-r--r--addons/account_edi/tests/test_edi.py46
-rw-r--r--addons/account_edi/views/account_edi_document_views.xml15
-rw-r--r--addons/account_edi/views/account_journal_views.xml7
-rw-r--r--addons/account_edi/views/account_move_views.xml48
-rw-r--r--addons/account_edi/views/account_payment_views.xml40
-rw-r--r--addons/account_edi_facturx/models/account_edi_format.py38
-rw-r--r--addons/account_edi_facturx/test_file/test_facturx.pdfbin0 -> 38295 bytes
-rw-r--r--addons/account_edi_facturx/test_file/test_facturx.xml148
-rw-r--r--addons/account_edi_facturx/tests/test_facturx.py158
-rw-r--r--addons/l10n_be_edi/models/account_edi_format.py39
-rw-r--r--addons/l10n_be_edi/tests/test_ubl.py35
-rw-r--r--addons/l10n_it_edi/data/account_edi_data.xml1
-rw-r--r--addons/l10n_it_edi/models/account_edi_format.py11
40 files changed, 1311 insertions, 314 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 02a475c7323..9ca5d54d708 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -529,6 +529,7 @@ class AccountBankStatementLine(models.Model):
compute='_compute_is_reconciled',
help="Technical field indicating if the statement line is already reconciled.")
state = fields.Selection(related='statement_id.state', string='Status', readonly=True)
+ country_code = fields.Char(related='company_id.country_id.code')
# -------------------------------------------------------------------------
# HELPERS
diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py
index 2a389ff33a9..9be7d9c68b5 100644
--- a/addons/account/models/account_journal.py
+++ b/addons/account/models/account_journal.py
@@ -101,6 +101,7 @@ class AccountJournal(models.Model):
currency_id = fields.Many2one('res.currency', help='The currency used to enter statement', string="Currency")
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, index=True, default=lambda self: self.env.company,
help="Company related to this journal")
+ country_code = fields.Char(related='company_id.country_id.code', readonly=True)
refund_sequence = fields.Boolean(string='Dedicated Credit Note Sequence', help="Check this box if you don't want to share the same sequence for invoices and credit notes made from this journal", default=False)
sequence_override_regex = fields.Text(help="Technical field used to enforce complex sequence composition that the system would normally misunderstand.\n"\
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 99753a10e1d..3cea680f502 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -306,6 +306,7 @@ class AccountMove(models.Model):
help="Technical field used to display a message when the invoice's accounting date is prior of the tax lock date.")
# Technical field to hide Reconciled Entries stat button
has_reconciled_entries = fields.Boolean(compute="_compute_has_reconciled_entries")
+ show_reset_to_draft_button = fields.Boolean(compute='_compute_show_reset_to_draft_button')
# ==== Hash Fields ====
restrict_mode_hash_table = fields.Boolean(related='journal_id.restrict_mode_hash_table')
@@ -1430,6 +1431,11 @@ class AccountMove(models.Model):
else:
move.tax_lock_date_message = False
+ @api.depends('restrict_mode_hash_table', 'state')
+ def _compute_show_reset_to_draft_button(self):
+ for move in self:
+ move.show_reset_to_draft_button = not move.restrict_mode_hash_table and move.state in ('posted', 'cancel')
+
# -------------------------------------------------------------------------
# BUSINESS MODELS SYNCHRONIZATION
# -------------------------------------------------------------------------
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 1fbd206f050..a3f6d746212 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -125,6 +125,7 @@ class AccountPayment(models.Model):
require_partner_bank_account = fields.Boolean(
compute='_compute_show_require_partner_bank',
help="Technical field used to know whether the field `partner_bank_id` needs to be required or not in the payments form views")
+ country_code = fields.Char(related='company_id.country_id.code')
_sql_constraints = [
(
diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py
index 4e389fcd89f..6d04db8b3c6 100644
--- a/addons/account/models/account_tax.py
+++ b/addons/account/models/account_tax.py
@@ -92,6 +92,7 @@ class AccountTax(models.Model):
invoice_repartition_line_ids = fields.One2many(string="Repartition for Invoices", comodel_name="account.tax.repartition.line", inverse_name="invoice_tax_id", copy=True, help="Repartition when the tax is used on an invoice")
refund_repartition_line_ids = fields.One2many(string="Repartition for Refund Invoices", comodel_name="account.tax.repartition.line", inverse_name="refund_tax_id", copy=True, help="Repartition when the tax is used on a refund")
tax_fiscal_country_id = fields.Many2one(string='Fiscal Country', comodel_name='res.country', related='company_id.account_tax_fiscal_country_id', help="Technical field used to restrict the domain of account tags for tax repartition lines created for this tax.")
+ country_code = fields.Char(related='company_id.country_id.code', readonly=True)
_sql_constraints = [
('name_company_uniq', 'unique(name, company_id, type_tax_use, tax_scope)', 'Tax names must be unique !'),
diff --git a/addons/account/tests/account_test_xml.py b/addons/account/tests/account_test_xml.py
deleted file mode 100644
index 04fc717a12c..00000000000
--- a/addons/account/tests/account_test_xml.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# -*- coding: utf-8 -*-
-from odoo.addons.account.tests.common import AccountTestInvoicingCommon
-
-import base64
-
-from lxml import etree
-
-
-class AccountTestEdiCommon(AccountTestInvoicingCommon):
-
- def assertXmlTreeEqual(self, xml_tree, expected_xml_tree):
- ''' Compare two lxml.etree.
- :param xml_tree: The current tree.
- :param expected_xml_tree: The expected tree.
- '''
-
- def _turn_node_as_dict_hierarchy(node):
- ''' Turn the node as a python dictionary to be compared later with another one.
- Allow to ignore the management of namespaces.
- :param node: A node inside an xml tree.
- :return: A python dictionary.
- '''
- tag_split = node.tag.split('}')
- tag_wo_ns = tag_split[-1]
- attrib_wo_ns = {k: v for k, v in node.attrib.items() if '}' not in k}
- return {
- 'tag': tag_wo_ns,
- 'namespace': None if len(tag_split) < 2 else tag_split[0],
- 'text': (node.text or '').strip(),
- 'attrib': attrib_wo_ns,
- 'children': [_turn_node_as_dict_hierarchy(child_node) for child_node in node.getchildren()],
- }
-
- def assertNodeDictEqual(node_dict, expected_node_dict):
- ''' Compare nodes created by the `_turn_node_as_dict_hierarchy` method.
- :param node_dict: The node to compare with.
- :param expected_node_dict: The expected node.
- '''
- # Check tag.
- self.assertEqual(node_dict['tag'], expected_node_dict['tag'])
-
- # Check attributes.
- node_dict_attrib = {k: '___ignore___' if expected_node_dict['attrib'].get(k) == '___ignore___' else v
- for k, v in node_dict['attrib'].items()}
- expected_node_dict_attrib = {k: v for k, v in expected_node_dict['attrib'].items() if v != '___remove___'}
- self.assertDictEqual(
- node_dict_attrib,
- expected_node_dict_attrib,
- "Element attributes are different for node %s" % node_dict['tag'],
- )
-
- # Check text.
- if expected_node_dict['text'] != '___ignore___':
- self.assertEqual(
- node_dict['text'],
- expected_node_dict['text'],
- "Element text are different for node %s" % node_dict['tag'],
- )
-
- # Check children.
- self.assertEqual(
- [child['tag'] for child in node_dict['children']],
- [child['tag'] for child in expected_node_dict['children']],
- "Number of children elements for node %s is different." % node_dict['tag'],
- )
-
- for child_node_dict, expected_child_node_dict in zip(node_dict['children'], expected_node_dict['children']):
- assertNodeDictEqual(child_node_dict, expected_child_node_dict)
-
- assertNodeDictEqual(
- _turn_node_as_dict_hierarchy(xml_tree),
- _turn_node_as_dict_hierarchy(expected_xml_tree),
- )
-
- def with_applied_xpath(self, xml_tree, xpath):
- ''' Applies the xpath to the xml_tree passed as parameter.
- :param xml_tree: An instance of etree.
- :param xpath: The xpath to apply as a string.
- :return: The resulting etree after applying the xpaths.
- '''
- diff_xml_tree = etree.fromstring('<data>%s</data>' % xpath)
- return self.env['ir.ui.view'].apply_inheritance_specs(xml_tree, diff_xml_tree)
-
- def get_xml_tree_from_attachment(self, attachment):
- ''' Extract an instance of etree from an ir.attachment.
- :param attachment: An ir.attachment.
- :return: An instance of etree.
- '''
- return etree.fromstring(base64.b64decode(attachment.with_context(bin_size=False).datas))
-
- def get_xml_tree_from_string(self, xml_tree_str):
- ''' Convert the string passed as parameter to an instance of etree.
- :param xml_tree_str: A string representing an xml.
- :return: An instance of etree.
- '''
- return etree.fromstring(xml_tree_str)
diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py
index fdb28764ff3..51bcecb9d01 100644
--- a/addons/account/tests/common.py
+++ b/addons/account/tests/common.py
@@ -4,7 +4,8 @@ from odoo import fields
from odoo.tests.common import SavepointCase, HttpSavepointCase, tagged, Form
import time
-
+import base64
+from lxml import etree
@tagged('post_install', '-at_install')
class AccountTestInvoicingCommon(SavepointCase):
@@ -45,7 +46,7 @@ class AccountTestInvoicingCommon(SavepointCase):
cls.cr = cls.env.cr
cls.company_data_2 = cls.setup_company_data('company_2_data', chart_template=chart_template)
- cls.company_data = cls.setup_company_data('company_1_data', chart_template=chart_template)
+ cls.company_data = cls.setup_company_data('company_1_data', chart_template=chart_template)
user.write({
'company_ids': [(6, 0, (cls.company_data['company'] + cls.company_data_2['company']).ids)],
@@ -375,6 +376,97 @@ class AccountTestInvoicingCommon(SavepointCase):
self.assertRecordValues(sort_lines(move.invoice_line_ids.sorted()), expected_lines_values[:len(move.invoice_line_ids)])
self.assertRecordValues(move, [expected_move_values])
+ ####################################################
+ # Xml Comparison
+ ####################################################
+
+ def _turn_node_as_dict_hierarchy(self, node):
+ ''' Turn the node as a python dictionary to be compared later with another one.
+ Allow to ignore the management of namespaces.
+ :param node: A node inside an xml tree.
+ :return: A python dictionary.
+ '''
+ tag_split = node.tag.split('}')
+ tag_wo_ns = tag_split[-1]
+ attrib_wo_ns = {k: v for k, v in node.attrib.items() if '}' not in k}
+ return {
+ 'tag': tag_wo_ns,
+ 'namespace': None if len(tag_split) < 2 else tag_split[0],
+ 'text': (node.text or '').strip(),
+ 'attrib': attrib_wo_ns,
+ 'children': [self._turn_node_as_dict_hierarchy(child_node) for child_node in node.getchildren()],
+ }
+
+ def assertXmlTreeEqual(self, xml_tree, expected_xml_tree):
+ ''' Compare two lxml.etree.
+ :param xml_tree: The current tree.
+ :param expected_xml_tree: The expected tree.
+ '''
+
+ def assertNodeDictEqual(node_dict, expected_node_dict):
+ ''' Compare nodes created by the `_turn_node_as_dict_hierarchy` method.
+ :param node_dict: The node to compare with.
+ :param expected_node_dict: The expected node.
+ '''
+ # Check tag.
+ self.assertEqual(node_dict['tag'], expected_node_dict['tag'])
+
+ # Check attributes.
+ node_dict_attrib = {k: '___ignore___' if expected_node_dict['attrib'].get(k) == '___ignore___' else v
+ for k, v in node_dict['attrib'].items()}
+ expected_node_dict_attrib = {k: v for k, v in expected_node_dict['attrib'].items() if v != '___remove___'}
+ self.assertDictEqual(
+ node_dict_attrib,
+ expected_node_dict_attrib,
+ "Element attributes are different for node %s" % node_dict['tag'],
+ )
+
+ # Check text.
+ if expected_node_dict['text'] != '___ignore___':
+ self.assertEqual(
+ node_dict['text'],
+ expected_node_dict['text'],
+ "Element text are different for node %s" % node_dict['tag'],
+ )
+
+ # Check children.
+ self.assertEqual(
+ [child['tag'] for child in node_dict['children']],
+ [child['tag'] for child in expected_node_dict['children']],
+ "Number of children elements for node %s is different." % node_dict['tag'],
+ )
+
+ for child_node_dict, expected_child_node_dict in zip(node_dict['children'], expected_node_dict['children']):
+ assertNodeDictEqual(child_node_dict, expected_child_node_dict)
+
+ assertNodeDictEqual(
+ self._turn_node_as_dict_hierarchy(xml_tree),
+ self._turn_node_as_dict_hierarchy(expected_xml_tree),
+ )
+
+ def with_applied_xpath(self, xml_tree, xpath):
+ ''' Applies the xpath to the xml_tree passed as parameter.
+ :param xml_tree: An instance of etree.
+ :param xpath: The xpath to apply as a string.
+ :return: The resulting etree after applying the xpaths.
+ '''
+ diff_xml_tree = etree.fromstring('<data>%s</data>' % xpath)
+ return self.env['ir.ui.view'].apply_inheritance_specs(xml_tree, diff_xml_tree)
+
+ def get_xml_tree_from_attachment(self, attachment):
+ ''' Extract an instance of etree from an ir.attachment.
+ :param attachment: An ir.attachment.
+ :return: An instance of etree.
+ '''
+ return etree.fromstring(base64.b64decode(attachment.with_context(bin_size=False).datas))
+
+ def get_xml_tree_from_string(self, xml_tree_str):
+ ''' Convert the string passed as parameter to an instance of etree.
+ :param xml_tree_str: A string representing an xml.
+ :return: An instance of etree.
+ '''
+ return etree.fromstring(xml_tree_str)
+
@tagged('post_install', '-at_install')
class AccountTestInvoicingHttpCommon(AccountTestInvoicingCommon, HttpSavepointCase):
diff --git a/addons/account/views/account_bank_statement_views.xml b/addons/account/views/account_bank_statement_views.xml
index 129ececf2cf..55308646a95 100644
--- a/addons/account/views/account_bank_statement_views.xml
+++ b/addons/account/views/account_bank_statement_views.xml
@@ -196,6 +196,7 @@
<field name="is_reconciled" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="partner_bank_id" invisible="1"/>
+ <field name="country_code" invisible="1"/>
<!-- Visible fields -->
<field name="sequence" widget="handle"/>
diff --git a/addons/account/views/account_journal_views.xml b/addons/account/views/account_journal_views.xml
index 2567bf53215..7a829dfb76f 100644
--- a/addons/account/views/account_journal_views.xml
+++ b/addons/account/views/account_journal_views.xml
@@ -36,6 +36,7 @@
</group>
<group>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
+ <field name="country_code" invisible="1"/>
</group>
</group>
<notebook>
diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml
index 2a70e60d9a3..c9e12964093 100644
--- a/addons/account/views/account_move_views.xml
+++ b/addons/account/views/account_move_views.xml
@@ -523,7 +523,7 @@
<button name="button_cancel" string="Cancel Entry" type="object" groups="account.group_account_invoice"
attrs="{'invisible' : ['|', ('id', '=', False), ('state', '!=', 'draft')]}"/>
<button name="button_draft" string="Reset to Draft" type="object" groups="account.group_account_invoice"
- attrs="{'invisible' : ['|', ('restrict_mode_hash_table', '=', True), ('state', 'not in', ('posted', 'cancel'))]}"/>
+ attrs="{'invisible' : [('show_reset_to_draft_button', '=', False)]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted"/>
</header>
<!-- Invoice outstanding credits -->
@@ -584,6 +584,7 @@
<field name="commercial_partner_id" invisible="1"/>
<field name="bank_partner_id" invisible="1"/>
<field name="display_qr_code" invisible="1"/>
+ <field name="show_reset_to_draft_button" invisible="1"/>
<field name="invoice_has_outstanding" invisible="1"/>
<field name="is_move_sent" invisible="1"/>
@@ -671,7 +672,7 @@
<label for="journal_id"
groups="account.group_account_readonly"
invisible="context.get('default_journal_id') and context.get('move_type', 'entry') != 'entry'"/>
- <div class="d-flex" invisible="context.get('default_journal_id') and context.get('move_type', 'entry') != 'entry'">
+ <div name="journal_div" class="d-flex" invisible="context.get('default_journal_id') and context.get('move_type', 'entry') != 'entry'">
<field name="journal_id"
groups="account.group_account_readonly"
options="{'no_create': True}"
diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml
index 07334dad807..18894102b42 100644
--- a/addons/account/views/account_payment_view.xml
+++ b/addons/account/views/account_payment_view.xml
@@ -159,6 +159,7 @@
<field name="hide_payment_method" invisible="1"/>
<field name="available_payment_method_ids" invisible="1"/>
<field name="suitable_journal_ids" invisible="1"/>
+ <field name="country_code" invisible="1"/>
<div class="oe_button_box" name="button_box">
<!-- Invoice stat button -->
diff --git a/addons/account/views/account_tax_views.xml b/addons/account/views/account_tax_views.xml
index 26fcda464da..6baf91b75ae 100644
--- a/addons/account/views/account_tax_views.xml
+++ b/addons/account/views/account_tax_views.xml
@@ -134,6 +134,7 @@
<page string="Definition" name="definition">
<div attrs="{'invisible': [('amount_type', '=', 'group')]}">
<field name="tax_fiscal_country_id" invisible="1"/>
+ <field name="country_code" invisible="1"/>
<group string="Repartition for Invoices">
<field name="invoice_repartition_line_ids" nolabel="1" context="{'default_company_id': company_id}"/>
</group>
diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py
index 0d996f20ddc..459d8097f66 100644
--- a/addons/account/wizard/account_payment_register.py
+++ b/addons/account/wizard/account_payment_register.py
@@ -103,6 +103,7 @@ class AccountPaymentRegister(models.TransientModel):
require_partner_bank_account = fields.Boolean(
compute='_compute_show_require_partner_bank',
help="Technical field used to know whether the field `partner_bank_id` needs to be required or not in the payments form views")
+ country_code = fields.Char(related='company_id.country_id.code', readonly=True)
# -------------------------------------------------------------------------
# HELPERS
diff --git a/addons/account/wizard/account_payment_register_views.xml b/addons/account/wizard/account_payment_register_views.xml
index 15aee188635..edf4f5db2c0 100644
--- a/addons/account/wizard/account_payment_register_views.xml
+++ b/addons/account/wizard/account_payment_register_views.xml
@@ -18,6 +18,7 @@
<field name="source_currency_id" invisible="1" force_save="1"/>
<field name="company_id" invisible="1" force_save="1"/>
<field name="partner_id" invisible="1" force_save="1"/>
+ <field name="country_code" invisible="1" force_save="1"/>
<field name="show_partner_bank_account" invisible="1"/>
<field name="require_partner_bank_account" invisible="1"/>
diff --git a/addons/account_edi/__manifest__.py b/addons/account_edi/__manifest__.py
index 5234e79dc12..08efe0e3ca1 100644
--- a/addons/account_edi/__manifest__.py
+++ b/addons/account_edi/__manifest__.py
@@ -16,7 +16,11 @@ governements, etc.)
'data': [
'security/ir.model.access.csv',
'wizard/account_invoice_send_views.xml',
- 'views/account_journal_views.xml'
+ 'views/account_edi_document_views.xml',
+ 'views/account_move_views.xml',
+ 'views/account_payment_views.xml',
+ 'views/account_journal_views.xml',
+ 'data/cron.xml'
],
'installable': True,
'application': False,
diff --git a/addons/account_edi/data/cron.xml b/addons/account_edi/data/cron.xml
new file mode 100644
index 00000000000..2aebca66a78
--- /dev/null
+++ b/addons/account_edi/data/cron.xml
@@ -0,0 +1,11 @@
+<odoo>
+ <record id="ir_cron_edi_network" model="ir.cron">
+ <field name="name">EDI : Perform web services operations</field>
+ <field name="model_id" ref="model_account_edi_document"/>
+ <field name="state">code</field>
+ <field name="code">model.search([('state', 'in', ('to_send', 'to_cancel'))])._process_documents_web_services(job_count=20)</field>
+ <field name="interval_number">1</field>
+ <field name="interval_type">minutes</field>
+ <field name="numbercall">-1</field>
+ </record>
+</odoo>
diff --git a/addons/account_edi/models/__init__.py b/addons/account_edi/models/__init__.py
index dea47dee7b9..e2229d0dd2f 100644
--- a/addons/account_edi/models/__init__.py
+++ b/addons/account_edi/models/__init__.py
@@ -3,6 +3,7 @@
from . import account_move
from . import account_journal
from . import account_edi_format
+from . import account_edi_document
+from . import account_payment
from . import ir_actions_report
-from . import ir_attachment
from . import mail_template
diff --git a/addons/account_edi/models/account_edi_document.py b/addons/account_edi/models/account_edi_document.py
new file mode 100644
index 00000000000..9e5dd2fbfed
--- /dev/null
+++ b/addons/account_edi/models/account_edi_document.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, fields, api, _
+from psycopg2 import OperationalError
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class AccountEdiDocument(models.Model):
+ _name = 'account.edi.document'
+ _description = 'Electronic Document for an account.move'
+
+ # == Stored fields ==
+ move_id = fields.Many2one('account.move')
+ edi_format_id = fields.Many2one('account.edi.format')
+ attachment_id = fields.Many2one('ir.attachment', help='The file generated by edi_format_id when the invoice is posted (and this document is processed).')
+ state = fields.Selection([('to_send', 'To Send'), ('sent', 'Sent'), ('to_cancel', 'To Cancel'), ('cancelled', 'Cancelled')])
+ error = fields.Html()
+
+ # == Not stored fields ==
+ name = fields.Char(related='attachment_id.name')
+ edi_format_name = fields.Char(string='Format Name', related='edi_format_id.name')
+
+ _sql_constraints = [
+ (
+ 'unique_edi_document_by_move_by_format',
+ 'UNIQUE(edi_format_id, move_id)',
+ 'Only one edi document by move by format',
+ ),
+ ]
+
+ def _prepare_jobs(self):
+ """Creates a list of jobs to be performed by '_process_jobs' for the documents in self.
+ Each document represent a job, BUT if multiple documents have the same state, edi_format_id,
+ doc_type (invoice or payment) and company_id AND the edi_format_id supports batching, they are grouped
+ into a single job.
+
+ :returns: A list of tuples (key, documents)
+ * key: A tuple (edi_format_id, state, doc_type, company_id)
+ ** edi_format_id: The format to perform the operation with
+ ** state: The state of the documents of this job
+ ** doc_type: Are the moves of this job invoice or payments ?
+ ** company_id: The company the moves belong to
+ * documents: The documents related to this job. If edi_format_id does not support batch, length must be one
+ """
+
+ to_process = []
+ batches = {}
+ for edi_doc in self.filtered(lambda d: d.state in ('to_send', 'to_cancel')):
+ move = edi_doc.move_id
+ edi_format = edi_doc.edi_format_id
+ if move.is_invoice(include_receipts=True):
+ doc_type = 'invoice'
+ elif move.payment_id or move.statement_line_id:
+ doc_type = 'payment'
+ else:
+ continue
+
+ key = (edi_format, edi_doc.state, doc_type, move.company_id)
+ if edi_format._support_batching():
+ if not batches.get(key, None):
+ batches[key] = self.env['account.edi.document']
+ batches[key] |= edi_doc
+ else:
+ to_process.append((key, edi_doc))
+ to_process.extend(batches.items())
+ return to_process
+
+ @api.model
+ def _process_jobs(self, to_process):
+ """Post or cancel move_id (invoice or payment) by calling the related methods on edi_format_id.
+ Invoices are processed before payments.
+ """
+ def _postprocess_post_edi_results(documents, edi_result):
+ for document in documents:
+ move = document.move_id
+ move_result = edi_result.get(move, {})
+ if move_result.get('attachment'):
+ document.write({
+ 'attachment_id': move_result['attachment'].id,
+ 'state': 'sent',
+ 'error': False,
+ })
+ else:
+ document.error = move_result.get('error', _("Error when processing the journal entry."))
+
+ def _postprocess_cancel_edi_results(documents, edi_result):
+ invoice_ids_to_cancel = set() # Avoid duplicates
+ for document in documents:
+ move = document.move_id
+ move_result = edi_result.get(move, {})
+ if move_result.get('success'):
+ document.write({
+ 'state': 'cancelled',
+ 'error': False,
+ 'attachment_id': False,
+ })
+
+ # The user requested a cancellation of the EDI and it has been approved. Then, the invoice
+ # can be safely cancelled.
+ if move.is_invoice(include_receipts=True) and move.state == 'posted':
+ invoice_ids_to_cancel.add(move.id)
+
+ else:
+ document.error = move_result.get('error') or _("Error when cancelling the journal entry.")
+
+ if invoice_ids_to_cancel:
+ invoices = self.env['account.move'].browse(list(invoice_ids_to_cancel))
+ invoices.button_draft()
+ invoices.button_cancel()
+
+ test_mode = self._context.get('edi_test_mode', False)
+
+ # ==== Process invoices ====
+ payments = []
+ for key, batches in to_process:
+ edi_format, state, doc_type, company_id = key
+ if doc_type == 'payment':
+ payments.append((key, batches))
+ continue # payments are processed after invoices
+
+ for documents in batches:
+ try:
+ with self.env.cr.savepoint():
+ # Locks the documents in DB. Avoid sending an invoice twice (the documents can be processed by the CRON but also manually).
+ self._cr.execute('SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(self.ids)])
+
+ if state == 'to_send':
+ edi_result = edi_format._post_invoice_edi(documents.move_id, test_mode=test_mode)
+ _postprocess_post_edi_results(documents, edi_result)
+ elif state == 'to_cancel':
+ edi_result = edi_format._cancel_invoice_edi(documents.move_id, test_mode=test_mode)
+ _postprocess_cancel_edi_results(documents, edi_result)
+
+ except OperationalError as e:
+ if e.pgcode == '55P03':
+ _logger.debug('Another transaction already locked documents rows. Cannot process documents.')
+ else:
+ raise e
+
+ # ==== Process payments ====
+ for key, batches in payments:
+ edi_format, state, doc_type, company_id = key
+
+ for documents in batches:
+ try:
+ with self.env.cr.savepoint():
+ self._cr.execute('SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(self.ids)])
+
+ if state == 'to_send':
+ edi_result = edi_format._post_payment_edi(documents.move_id, test_mode=test_mode)
+ _postprocess_post_edi_results(documents, edi_result)
+ elif state == 'to_cancel':
+ edi_result = edi_format._cancel_payment_edi(documents.move_id, test_mode=test_mode)
+ _postprocess_cancel_edi_results(documents, edi_result)
+
+ except OperationalError as e:
+ if e.pgcode == '55P03':
+ _logger.debug('Another transaction already locked documents rows. Cannot process documents.')
+ else:
+ raise e
+
+ def _process_documents_no_web_services(self):
+ """ Post and cancel all the documents that don't need a web service.
+ """
+ jobs = self.filtered(lambda d: not d.edi_format_id._needs_web_services())._prepare_jobs()
+ self._process_jobs(jobs)
+
+ def _process_documents_web_services(self, job_count=None):
+ """ Post and cancel all the documents that need a web service. This is called by CRON.
+
+ :param job_count: Limit to the number of jobs to process among the ones that are available for treatment.
+ """
+ jobs = self.filtered(lambda d: d.edi_format_id._needs_web_services())._prepare_jobs()
+ self._process_jobs(jobs[0:job_count or len(jobs)])
diff --git a/addons/account_edi/models/account_edi_format.py b/addons/account_edi/models/account_edi_format.py
index c36f22e16a3..38babb16814 100644
--- a/addons/account_edi/models/account_edi_format.py
+++ b/addons/account_edi/models/account_edi_format.py
@@ -19,51 +19,140 @@ class AccountEdiFormat(models.Model):
name = fields.Char()
code = fields.Char()
- hide_on_journal = fields.Selection([('import_export', 'Import/Export'), ('import', 'Import Only')], default='import_export', help='used to hide this EDI format on journals')
_sql_constraints = [
('unique_code', 'unique (code)', 'This code already exists')
]
+
+ ####################################################
+ # Low-level methods
+ ####################################################
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ edi_formats = super().create(vals_list)
+
+ journals = self.env['account.journal'].search([])
+ for journal in journals:
+ for edi_format in edi_formats:
+ if edi_format._is_compatible_with_journal(journal):
+ journal.edi_format_ids += edi_format
+
+ return edi_formats
+
####################################################
# Export method to override based on EDI Format
####################################################
- def _export_invoice_to_attachment(self, invoice):
- """ Create the file content representing the invoice.
+ def _is_required_for_invoice(self, invoice):
+ """ Indicate if this EDI must be generated for the invoice passed as parameter.
- :param invoice: the invoice to encode.
- :returns: a dictionary (values are compatible to create an ir.attachment)
- * name : the name of the file
- * datas : the content of the file,
- * res_model : 'account.move',
- * res_id: the id of invoice
- * mimetype : the mimetype of the attachment
+ :param invoice: An account.move having the invoice type.
+ :returns: True if the EDI must be generated, False otherwise.
"""
# TO OVERRIDE
self.ensure_one()
+ return True
+
+ def _is_required_for_payment(self, payment):
+ """ Indicate if this EDI must be generated for the payment passed as parameter.
+
+ :param payment: An account.move linked to either an account.payment, either an account.bank.statement.line.
+ :returns: True if the EDI must be generated, False otherwise.
+ """
+ # TO OVERRIDE
+ self.ensure_one()
+ return False
+
+ def _needs_web_services(self):
+ """ Indicate if the EDI must be generated asynchronously through to some web services.
+
+ :return: True if such a web service is available, False otherwise.
+ """
+ self.ensure_one()
+ return False
+
+ def _is_compatible_with_journal(self, journal):
+ """ Indicate if the EDI format should appear on the journal passed as parameter to be selected by the user.
+ If True, this EDI format will be selected by default on the journal.
+
+ :param journal: The journal.
+ :returns: True if this format can be enabled by default on the journal, False otherwise.
+ """
+ # TO OVERRIDE
+ self.ensure_one()
+ return journal.type == 'sale'
+
+ def _is_embedding_to_invoice_pdf_needed(self):
+ """ Indicate if the EDI must be embedded inside the PDF report.
+
+ :returns: True if the documents need to be embedded, False otherwise.
+ """
+ # TO OVERRIDE
return False
- def _export_invoice_to_embed_to_pdf(self, pdf_content, invoice):
- """ Create the file content representing the invoice when it's destined
- to be embed into a pdf.
- - default: creates the default EDI document (_export_invoice_to_attachment).
- - Should return False if this EDI format should not be embedded.
- - Should be overriden only if a specific behavior (for example,
- include the pdf content inside the file).
-
- :param pdf_content: the pdf before any EDI format was added.
- :param invoice: the invoice to add.
- :returns: a dictionary or False if this EDI format must not be embedded to pdf.
- * name : the name of the file
- * datas : the content of the file,
- * res_model : 'account.move',
- * res_id: the id of invoice
- * mimetype : the mimetype of the attachment
+ def _support_batching(self):
+ """ Indicate if we can send multiple documents in the same time to the web services.
+ If True, the _post_%s_edi methods will get multiple documents in the same time.
+ Otherwise, these methods will be called with only one record at a time.
+
+ :returns: True if batching is supported, False otherwise.
+ """
+ # TO OVERRIDE
+ return False
+
+ def _post_invoice_edi(self, invoices, test_mode=False):
+ """ Create the file content representing the invoice (and calls web services if necessary).
+
+ :param invoices: A list of invoices to post.
+ :param test_mode: A flag indicating the EDI should only simulate the EDI without sending data.
+ :returns: A dictionary with the invoice as key and as value, another dictionary:
+ * attachment: The attachment representing the invoice in this edi_format if the edi was successfully posted.
+ * error: An error if the edi was not successfully posted.
+ """
+ # TO OVERRIDE
+ self.ensure_one()
+ return {}
+
+ def _cancel_invoice_edi(self, invoices, test_mode=False):
+ """Calls the web services to cancel the invoice of this document.
+
+ :param invoices: A list of invoices to cancel.
+ :param test_mode: A flag indicating the EDI should only simulate the EDI without sending data.
+ :returns: A dictionary with the invoice as key and as value, another dictionary:
+ * success: True if the invoice was successfully cancelled.
+ * error: An error if the edi was not successfully cancelled.
"""
# TO OVERRIDE
self.ensure_one()
- return self._export_invoice_to_attachment(invoice)
+ return {invoice: {'success': True} for invoice in invoices} # By default, cancel succeeds doing nothing.
+
+ def _post_payment_edi(self, payments, test_mode=False):
+ """ Create the file content representing the payment (and calls web services if necessary).
+
+ :param payments: The payments to post.
+ :param test_mode: A flag indicating the EDI should only simulate the EDI without sending data.
+ :returns: A dictionary with the payment as key and as value, another dictionary:
+ * attachment: The attachment representing the payment in this edi_format if the edi was successfully posted.
+ * error: An error if the edi was not successfully posted.
+ """
+ # TO OVERRIDE
+ self.ensure_one()
+ return {}
+
+ def _cancel_payment_edi(self, payments, test_mode=False):
+ """Calls the web services to cancel the payment of this document.
+
+ :param payments: A list of payments to cancel.
+ :param test_mode: A flag indicating the EDI should only simulate the EDI without sending data.
+ :returns: A dictionary with the payment as key and as value, another dictionary:
+ * success: True if the payment was successfully cancelled.
+ * error: An error if the edi was not successfully cancelled.
+ """
+ # TO OVERRIDE
+ self.ensure_one()
+ return {payment: {'success': True} for payment in payments} # By default, cancel succeeds doing nothing.
####################################################
# Import methods to override based on EDI Format
@@ -129,12 +218,10 @@ class AccountEdiFormat(models.Model):
"""
attachments = []
for edi_format in self:
- try:
- vals = edi_format._export_invoice_to_embed_to_pdf(pdf_content, invoice)
- except:
- continue
- if vals:
- attachments.append(vals)
+ attachment = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == edi_format).attachment_id
+ if attachment and edi_format._is_embedding_to_invoice_pdf_needed():
+ datas = base64.b64decode(attachment.with_context(bin_size=False).datas)
+ attachments.append({'name': attachment.name, 'datas': datas})
if attachments:
# Add the attachments to the pdf file
@@ -151,23 +238,6 @@ class AccountEdiFormat(models.Model):
buffer.close()
return pdf_content
- def _create_ir_attachments(self, invoice):
- """ Create ir.attachment for the EDIs from invoice.
-
- :param invoice: the invoice to generate the EDI from.
- :returns: the newly created attachments.
- """
- attachment_vals_list = []
- for edi_format in self:
- vals = edi_format._export_invoice_to_attachment(invoice)
- if vals:
- vals['datas'] = base64.encodebytes(vals['datas'])
- vals['edi_format_id'] = edi_format._origin.id
- attachment_vals_list.append(vals)
- res = self.env['ir.attachment'].create(attachment_vals_list)
- invoice.edi_document_ids |= res
- return res
-
####################################################
# Import Internal methods (not meant to be overridden)
####################################################
@@ -268,7 +338,7 @@ class AccountEdiFormat(models.Model):
res = edi_format._create_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'])
elif file_data['type'] == 'pdf':
res = edi_format._create_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'])
- file_data['pdf_reader'].stream.close()
+ file_data['pdf_reader'].streamS.close()
if res:
if 'extract_state' in res:
# Bypass the OCR to prevent overwriting data when an EDI was succesfully imported.
diff --git a/addons/account_edi/models/account_journal.py b/addons/account_edi/models/account_journal.py
index c160d8da249..ecc82632f99 100644
--- a/addons/account_edi/models/account_journal.py
+++ b/addons/account_edi/models/account_journal.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import models, fields
+from odoo import api, models, fields, _
+from odoo.exceptions import UserError
class AccountJournal(models.Model):
@@ -10,12 +11,52 @@ class AccountJournal(models.Model):
edi_format_ids = fields.Many2many(comodel_name='account.edi.format',
string='Electronic invoicing',
help='Send XML/EDI invoices',
- domain="[('hide_on_journal', '=', 'import_export')]")
+ domain="[('id', 'in', compatible_edi_ids)]",
+ compute='_compute_edi_format_ids',
+ readonly=False, store=True)
+
+ compatible_edi_ids = fields.Many2many(comodel_name='account.edi.format',
+ compute='_compute_compatible_edi_ids',
+ help='EDI format that support moves in this journal')
+
+ def write(self, vals):
+ # OVERRIDE
+ # Don't allow the user to deactivate an edi format having at least one document to be processed.
+ if vals.get('edi_format_ids'):
+ old_edi_format_ids = self.edi_format_ids
+ res = super().write(vals)
+ diff_edi_format_ids = old_edi_format_ids - self.edi_format_ids
+ documents = self.env['account.edi.document'].search([
+ ('move_id.journal_id', 'in', self.ids),
+ ('edi_format_id', 'in', diff_edi_format_ids.ids),
+ ('state', 'in', ('to_cancel', 'to_send')),
+ ])
+ if documents:
+ raise UserError(_('Cannot deactivate (%s) on this journal because not all documents are synchronized', ', '.join(documents.edi_format_id.mapped('display_name'))))
+ return res
+ else:
+ return super().write(vals)
+
+ @api.depends('type', 'company_id', 'company_id.country_id')
+ def _compute_compatible_edi_ids(self):
+ edi_formats = self.env['account.edi.format'].search([])
+
+ for journal in self:
+ compatible_edis = edi_formats.filtered(lambda e: e._is_compatible_with_journal(journal))
+ journal.compatible_edi_ids += compatible_edis
+
+ @api.depends('type', 'company_id', 'company_id.country_id')
+ def _compute_edi_format_ids(self):
+ edi_formats = self.env['account.edi.format'].search([])
+
+ for journal in self:
+ journal.edi_format_ids += edi_formats.filtered(lambda e: e._is_compatible_with_journal(journal))
def _create_invoice_from_single_attachment(self, attachment):
+ # OVERRIDE
invoice = self.env['account.edi.format'].search([])._create_invoice_from_attachment(attachment)
if invoice:
# with_context: we don't want to import the attachment since the invoice was just created from it.
- invoice.with_context(no_new_invoice=True).message_post(attachment_ids=[attachment.id])
+ invoice.with_context(no_new_invoice=True).message_post(attachment_ids=attachment.ids)
return invoice
return super()._create_invoice_from_single_attachment(attachment)
diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py
index 76bc9309611..2f35c80a017 100644
--- a/addons/account_edi/models/account_move.py
+++ b/addons/account_edi/models/account_move.py
@@ -1,34 +1,214 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, models, fields
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = 'account.move'
- edi_document_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'account.move'), ('edi_format_id', '!=', False)])
+ edi_document_ids = fields.One2many(
+ comodel_name='account.edi.document',
+ inverse_name='move_id')
+ edi_state = fields.Selection(
+ selection=[('to_send', 'To Send'), ('sent', 'Sent'), ('to_cancel', 'To Cancel'), ('cancelled', 'Cancelled')],
+ string="Electronic invoicing",
+ store=True,
+ compute='_compute_edi_state',
+ help='The aggregated state of all the EDIs of this move')
+ edi_error_count = fields.Integer(
+ compute='_compute_edi_error_count',
+ help='How many EDIs are in error for this move ?')
+ edi_web_services_to_process = fields.Text(
+ compute='_compute_edi_web_services_to_process',
+ help="Technical field to display the documents that will be processed by the CRON")
+ edi_show_cancel_button = fields.Boolean(
+ compute='_compute_edi_show_cancel_button')
+
+ @api.depends('edi_document_ids.state')
+ def _compute_edi_state(self):
+ for move in self:
+ all_states = set(move.edi_document_ids.mapped('state'))
+ if all_states == {'sent'}:
+ move.edi_state = 'sent'
+ elif all_states == {'cancelled'}:
+ move.edi_state = 'cancelled'
+ elif 'to_send' in all_states:
+ move.edi_state = 'to_send'
+ elif 'to_cancel' in all_states:
+ move.edi_state = 'to_cancel'
+ else:
+ move.edi_state = False
+
+ @api.depends('edi_document_ids.error')
+ def _compute_edi_error_count(self):
+ for move in self:
+ move.edi_error_count = len(move.edi_document_ids.filtered(lambda d: d.error))
+
+ @api.depends(
+ 'edi_document_ids',
+ 'edi_document_ids.state',
+ 'edi_document_ids.edi_format_id',
+ 'edi_document_ids.edi_format_id.name')
+ def _compute_edi_web_services_to_process(self):
+ for move in self:
+ to_process = move.edi_document_ids.filtered(lambda d: d.state in ['to_send', 'to_cancel'])
+ format_web_services = to_process.edi_format_id.filtered(lambda f: f._needs_web_services())
+ move.edi_web_services_to_process = ', '.join(f.name for f in format_web_services)
+
+ @api.depends('restrict_mode_hash_table', 'state')
+ def _compute_show_reset_to_draft_button(self):
+ # OVERRIDE
+ super()._compute_show_reset_to_draft_button()
+
+ for move in self:
+ for doc in move.edi_document_ids:
+ if doc.edi_format_id._needs_web_services() \
+ and doc.attachment_id \
+ and doc.state in ('sent', 'to_cancel') \
+ and move.is_invoice(include_receipts=True) \
+ and doc.edi_format_id._is_required_for_invoice(move):
+ move.show_reset_to_draft_button = False
+ break
+
+ @api.depends(
+ 'state',
+ 'edi_document_ids.state',
+ 'edi_document_ids.attachment_id')
+ def _compute_edi_show_cancel_button(self):
+ for move in self:
+ if move.state != 'posted':
+ move.edi_show_cancel_button = False
+ continue
+
+ move.edi_show_cancel_button = any([doc.edi_format_id._needs_web_services()
+ and doc.attachment_id
+ and doc.state == 'sent'
+ and move.is_invoice(include_receipts=True)
+ and doc.edi_format_id._is_required_for_invoice(move)
+ for doc in move.edi_document_ids])
+
+ ####################################################
+ # Export Electronic Document
+ ####################################################
+
+ def _update_payments_edi_documents(self):
+ ''' Update the edi documents linked to the current journal entries. These journal entries must be linked to an
+ account.payment of an account.bank.statement.line. This additional method is needed because the payment flow is
+ not the same as the invoice one. Indeed, the edi documents must be updated when the reconciliation with some
+ invoices is changing.
+ '''
+ edi_document_vals_list = []
+ for payment in self:
+ edi_formats = payment._get_reconciled_invoices().journal_id.edi_format_ids + payment.edi_document_ids.edi_format_id
+ edi_formats = self.env['account.edi.format'].browse(edi_formats.ids) # Avoid duplicates
+ for edi_format in edi_formats:
+ existing_edi_document = payment.edi_document_ids.filtered(lambda x: x.edi_format_id == edi_format)
+
+ if edi_format._is_required_for_payment(payment):
+ if existing_edi_document:
+ existing_edi_document.write({
+ 'state': 'to_send',
+ 'error': False,
+ })
+ else:
+ edi_document_vals_list.append({
+ 'edi_format_id': edi_format.id,
+ 'move_id': payment.id,
+ 'state': 'to_send',
+ })
+ elif existing_edi_document:
+ existing_edi_document.write({
+ 'state': False,
+ 'error': False,
+ })
+
+ self.env['account.edi.document'].create(edi_document_vals_list)
+ self.edi_document_ids._process_documents_no_web_services()
def _post(self, soft=True):
# OVERRIDE
- # Generate the electronic documents for the move.
- existing_attachments = self.env['ir.attachment'].search([
- ('res_model', '=', 'account.move'),
- ('res_id', 'in', self.ids),
- ('edi_format_id', 'in', self.journal_id.edi_format_ids.ids)])
- existing_attachments.unlink()
- posted = super()._post(soft)
- for move in posted:
- move.journal_id.edi_format_ids._create_ir_attachments(move)
-
- return posted
+ # Set the electronic document to be posted and post immediately for synchronous formats.
+ res = super()._post(soft=soft)
+
+ edi_document_vals_list = []
+ for move in self:
+ for edi_format in move.journal_id.edi_format_ids:
+ is_edi_needed = move.is_invoice(include_receipts=False) and edi_format._is_required_for_invoice(move)
+
+ if is_edi_needed:
+ existing_edi_document = move.edi_document_ids.filtered(lambda x: x.edi_format_id == edi_format)
+ if existing_edi_document:
+ existing_edi_document.write({
+ 'state': 'to_send',
+ 'attachment_id': False,
+ })
+ else:
+ edi_document_vals_list.append({
+ 'edi_format_id': edi_format.id,
+ 'move_id': move.id,
+ 'state': 'to_send',
+ })
+
+ self.env['account.edi.document'].create(edi_document_vals_list)
+ self.edi_document_ids._process_documents_no_web_services()
+ return res
+
+ def button_cancel(self):
+ # OVERRIDE
+ # Set the electronic document to be canceled and cancel immediately for synchronous formats.
+ res = super().button_cancel()
+
+ self.edi_document_ids.filtered(lambda doc: doc.attachment_id).write({'state': 'to_cancel', 'error': False})
+ self.edi_document_ids.filtered(lambda doc: not doc.attachment_id).write({'state': 'cancelled', 'error': False})
+
+ return res
+
+ def button_draft(self):
+ # OVERRIDE
+ for move in self:
+ if move.edi_show_cancel_button:
+ raise UserError(_(
+ "You can't edit the following journal entry %s because an electronic document has already been "
+ "sent. Please use the 'Request EDI Cancellation' button instead."
+ ) % move.display_name)
+
+ res = super().button_draft()
+
+ self.edi_document_ids.write({'state': False, 'error': False})
+
+ return res
+
+ def button_cancel_posted_moves(self):
+ '''Mark the edi.document related to this move to be canceled.
+ '''
+ to_cancel_documents = self.env['account.edi.document']
+ for move in self:
+ is_move_marked = False
+ for doc in move.edi_document_ids:
+ if doc.edi_format_id._needs_web_services() \
+ and doc.attachment_id \
+ and doc.state == 'sent' \
+ and move.is_invoice(include_receipts=True) \
+ and doc.edi_format_id._is_required_for_invoice(move):
+ to_cancel_documents |= doc
+ is_move_marked = True
+ if is_move_marked:
+ move.message_post(body=_("A cancellation of the EDI has been requested."))
+
+ to_cancel_documents.write({'state': 'to_cancel', 'error': False})
+
+ ####################################################
+ # Import Electronic Document
+ ####################################################
@api.returns('mail.message', lambda value: value.id)
def message_post(self, **kwargs):
# OVERRIDE
# When posting a message, analyse the attachment to check if it is an EDI document and update the invoice
- # with the data.
- res = super(AccountMove, self).message_post(**kwargs)
+ # with the imported data.
+ res = super().message_post(**kwargs)
if len(self) != 1 or self.env.context.get('no_new_invoice') or not self.is_invoice(include_receipts=True):
return res
@@ -48,8 +228,69 @@ class AccountMove(models.Model):
author_id=odoobot.id)
return res
+ edi_formats = self.env['account.edi.format'].search([])
for attachment in attachments:
- invoice = self.env['account.edi.format'].search([])._update_invoice_from_attachment(attachment, self)
- break
+ invoice = edi_formats._update_invoice_from_attachment(attachment, self)
+ if invoice:
+ break
+
+ return res
+
+ ####################################################
+ # Business operations
+ ####################################################
+
+ def action_process_edi_web_services(self):
+ self.edi_document_ids.filtered(lambda d: d.state in ('to_send', 'to_cancel'))._process_documents_web_services()
+
+
+class AccountMoveLine(models.Model):
+ _inherit = 'account.move.line'
+
+ ####################################################
+ # Export Electronic Document
+ ####################################################
+
+ def reconcile(self):
+ # OVERRIDE
+ # In some countries, the payments must be sent to the government under some condition. One of them could be
+ # there is at least one reconciled invoice to the payment. Then, we need to update the state of the edi
+ # documents during the reconciliation.
+ all_lines = self + self.matched_debit_ids.debit_move_id + self.matched_credit_ids.credit_move_id
+ payments = all_lines.move_id.filtered(lambda move: move.payment_id or move.statement_line_id)
+
+ invoices_per_payment_before = {pay: pay._get_reconciled_invoices() for pay in payments}
+ res = super().reconcile()
+ invoices_per_payment_after = {pay: pay._get_reconciled_invoices() for pay in payments}
+
+ changed_payments = self.env['account.move']
+ for payment, invoices_after in invoices_per_payment_after.items():
+ invoices_before = invoices_per_payment_before[payment]
+
+ if set(invoices_after.ids) != set(invoices_before.ids):
+ changed_payments |= payment
+ changed_payments._update_payments_edi_documents()
+
+ return res
+
+ def remove_move_reconcile(self):
+ # OVERRIDE
+ # When a payment has been sent to the government, it usually contains some information about reconciled
+ # invoices. If the user breaks a reconciliation, the related payments must be cancelled properly and then, a new
+ # electronic document must be generated.
+ all_lines = self + self.matched_debit_ids.debit_move_id + self.matched_credit_ids.credit_move_id
+ payments = all_lines.move_id.filtered(lambda move: move.payment_id or move.statement_line_id)
+
+ invoices_per_payment_before = {pay: pay._get_reconciled_invoices() for pay in payments}
+ res = super().remove_move_reconcile()
+ invoices_per_payment_after = {pay: pay._get_reconciled_invoices() for pay in payments}
+
+ changed_payments = self.env['account.move']
+ for payment, invoices_after in invoices_per_payment_after.items():
+ invoices_before = invoices_per_payment_before[payment]
+
+ if set(invoices_after.ids) != set(invoices_before.ids):
+ changed_payments |= payment
+ changed_payments._update_payments_edi_documents()
return res
diff --git a/addons/account_edi/models/account_payment.py b/addons/account_edi/models/account_payment.py
new file mode 100644
index 00000000000..8b8a691d63e
--- /dev/null
+++ b/addons/account_edi/models/account_payment.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, fields, api, _
+
+
+class AccountPayment(models.Model):
+ _inherit = 'account.payment'
+
+ def action_process_edi_web_services(self):
+ return self.move_id.action_process_edi_web_services()
diff --git a/addons/account_edi/models/ir_attachment.py b/addons/account_edi/models/ir_attachment.py
deleted file mode 100644
index 146411bc28e..00000000000
--- a/addons/account_edi/models/ir_attachment.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import models, fields
-
-
-class IrAttachment(models.Model):
- _inherit = 'ir.attachment'
-
- edi_format_id = fields.Many2one('account.edi.format')
diff --git a/addons/account_edi/models/mail_template.py b/addons/account_edi/models/mail_template.py
index 209b9160e0e..3b294b1ef9c 100644
--- a/addons/account_edi/models/mail_template.py
+++ b/addons/account_edi/models/mail_template.py
@@ -17,16 +17,18 @@ class MailTemplate(models.Model):
if self.model != 'account.move':
return res
- existing_attachments = self.env['ir.attachment'].search([
- ('res_model', '=', 'account.move'),
- ('res_id', 'in', res_ids),
- ('edi_format_id', '!=', False)])
-
- for record in self.env[self.model].browse(res_ids):
- available_formats = existing_attachments.filtered(lambda a: a.res_id == record.id)
- missing_formats = record.journal_id.edi_format_ids.filtered(lambda f: f._origin.id not in available_formats.edi_format_id.ids)
- new_attachments = missing_formats._create_ir_attachments(record)
- available_formats |= new_attachments
- (res[record.id] if multi_mode else res).setdefault('attachments', []).extend([(a.name, a.datas) for a in available_formats])
+ records = self.env[self.model].browse(res_ids)
+ for record in records:
+ record_data = (res[record.id] if multi_mode else res)
+ for doc in record.edi_document_ids:
+
+ # The EDI format will be embedded directly inside the PDF and then, don't need to be added to the
+ # wizard.
+ if doc.edi_format_id._is_embedding_to_invoice_pdf_needed():
+ continue
+
+ attachment = doc.attachment_id
+ record_data.setdefault('attachments', [])
+ record_data['attachments'].append((attachment.name, attachment.datas))
return res
diff --git a/addons/account_edi/security/ir.model.access.csv b/addons/account_edi/security/ir.model.access.csv
index 71c1f8bda06..907e686054c 100644
--- a/addons/account_edi/security/ir.model.access.csv
+++ b/addons/account_edi/security/ir.model.access.csv
@@ -1,2 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_edi_format_readonly,account.edi.format,model_account_edi_format,,1,0,0,0
+access_account_edi_format_group_invoice,account.edi.format,model_account_edi_format,account.group_account_invoice,1,1,1,1
+access_account_edi_document_readonly,account.edi.document,model_account_edi_document,,1,0,0,0
+access_account_edi_document_group_invoice,account.edi.document,model_account_edi_document,account.group_account_invoice,1,1,1,1
diff --git a/addons/account_edi/tests/__init__.py b/addons/account_edi/tests/__init__.py
new file mode 100644
index 00000000000..eea5d7edcd2
--- /dev/null
+++ b/addons/account_edi/tests/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import common
+from . import test_edi
diff --git a/addons/account_edi/tests/common.py b/addons/account_edi/tests/common.py
new file mode 100644
index 00000000000..d6fe883fecd
--- /dev/null
+++ b/addons/account_edi/tests/common.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.modules.module import get_module_resource
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+
+from contextlib import contextmanager
+from unittest.mock import patch
+from unittest import mock
+
+import base64
+
+
+class AccountEdiTestCommon(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None, edi_format_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ # ==== EDI ====
+ if edi_format_ref:
+ cls.edi_format = cls.env.ref(edi_format_ref)
+ else:
+ cls.edi_format = cls.env['account.edi.format'].sudo().create({
+ 'name': 'Test EDI format',
+ 'code': 'test_edi',
+ })
+ cls.journal = cls.company_data['default_journal_sale']
+ cls.journal.edi_format_ids = [(6, 0, cls.edi_format.ids)]
+
+ ####################################################
+ # EDI helpers
+ ####################################################
+
+ def edi_cron(self):
+ self.env['account.edi.document'].sudo().with_context(edi_test_mode=True).search([('state', 'in', ('to_send', 'to_cancel'))])._process_documents_web_services()
+
+ def update_invoice_from_file(self, module_name, subfolder, filename, invoice):
+ file_path = get_module_resource(module_name, subfolder, filename)
+ file = open(file_path, 'rb').read()
+
+ attachment = self.env['ir.attachment'].create({
+ 'name': filename,
+ 'datas': base64.encodebytes(file),
+ 'res_id': invoice.id,
+ 'res_model': 'account.move',
+ })
+
+ invoice.message_post(attachment_ids=[attachment.id])
+
+ def create_invoice_from_file(self, module_name, subfolder, filename):
+ file_path = get_module_resource(module_name, 'test_file', filename)
+ file = open(file_path, 'rb').read()
+
+ attachment = self.env['ir.attachment'].create({
+ 'name': filename,
+ 'datas': base64.encodebytes(file),
+ 'res_model': 'account.move',
+ })
+
+ journal_id = self.company_data['default_journal_sale']
+ invoice = journal_id.with_context(default_move_type='in_invoice')._create_invoice_from_single_attachment(attachment)
+
+ def assert_generated_file_equal(self, invoice, expected_values, applied_xpath=None):
+ invoice.action_post()
+ invoice.edi_document_ids._process_documents_web_services() # synchronous are called in post, but there's no CRON in tests for asynchronous
+ attachment = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == self.edi_format).attachment_id
+ xml_content = base64.b64decode(attachment.with_context(bin_size=False).datas)
+ current_etree = self.get_xml_tree_from_string(xml_content)
+ expected_etree = self.get_xml_tree_from_string(expected_values)
+ if applied_xpath:
+ expected_etree = self.with_applied_xpath(expected_etree, applied_xpath)
+ self.assertXmlTreeEqual(current_etree, expected_etree)
+
+ def _process_documents_web_services(self, moves):
+ moves.edi_document_ids.with_context(edi_test_mode=True)._process_documents_web_services()
+
+ attachments = moves.edi_document_ids.attachment_id
+
+ data_str_list = []
+ for attachment in attachments.with_context(bin_size=False):
+ data_str_list.append(base64.decodebytes(attachment.datas))
+ return data_str_list
diff --git a/addons/account_edi/tests/test_edi.py b/addons/account_edi/tests/test_edi.py
new file mode 100644
index 00000000000..9d15b5ffec4
--- /dev/null
+++ b/addons/account_edi/tests/test_edi.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
+from unittest.mock import patch
+
+
+class TestAccountEdi(AccountEdiTestCommon):
+
+ def test_export_edi(self):
+ invoice = self.init_invoice('out_invoice')
+ self.assertEqual(len(invoice.edi_document_ids), 0)
+ invoice.action_post()
+ self.assertEqual(len(invoice.edi_document_ids), 1)
+
+ def test_prepare_jobs(self):
+ def create_edi_document(edi_format, state, move=None, move_type=None):
+ move = move or self.init_invoice(move_type or 'out_invoice')
+ return self.env['account.edi.document'].create({
+ 'edi_format_id': edi_format.id,
+ 'move_id': move.id,
+ 'state': state
+ })
+
+ edi_docs = self.env['account.edi.document']
+ edi_docs |= create_edi_document(self.edi_format, 'to_send')
+ edi_docs |= create_edi_document(self.edi_format, 'to_send')
+
+ to_process = edi_docs._prepare_jobs()
+ self.assertEqual(len(to_process), 2)
+
+ with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching', return_value=True):
+ to_process = edi_docs._prepare_jobs()
+ self.assertEqual(len(to_process), 1)
+
+ other_edi = self.env['account.edi.format'].sudo().create({
+ 'name': 'Batchable EDI format 2',
+ 'code': 'test_batch_edi_2',
+ })
+
+ edi_docs |= create_edi_document(other_edi, 'to_send')
+ edi_docs |= create_edi_document(other_edi, 'to_send')
+
+ with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching', return_value=True):
+ to_process = edi_docs._prepare_jobs()
+ self.assertEqual(len(to_process), 2)
diff --git a/addons/account_edi/views/account_edi_document_views.xml b/addons/account_edi/views/account_edi_document_views.xml
new file mode 100644
index 00000000000..9b16ff60deb
--- /dev/null
+++ b/addons/account_edi/views/account_edi_document_views.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <record id="view_tree_account_edi_document" model="ir.ui.view">
+ <field name="name">Account.edi.document.tree</field>
+ <field name="model">account.edi.document</field>
+ <field name="arch" type="xml">
+ <tree create="false" delete="false" edit="false">
+ <field name="edi_format_name" />
+ <field name="error" />
+ </tree>
+ </field>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/account_edi/views/account_journal_views.xml b/addons/account_edi/views/account_journal_views.xml
index 5ed896b54bc..09ba292e7bb 100644
--- a/addons/account_edi/views/account_journal_views.xml
+++ b/addons/account_edi/views/account_journal_views.xml
@@ -6,9 +6,10 @@
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.view_account_journal_form" />
<field name="arch" type="xml">
- <xpath expr="//page[@name='advanced_settings']//group[@name='group_alias_edit']" position="after">
- <group string="Electronic Data Interchange" attrs="{'invisible': [('type', '!=', 'sale')]}">
- <field name="edi_format_ids" widget="many2many_tags"/>
+ <xpath expr="//group[@name='group_alias_edit']" position="after">
+ <group string="Electronic Data Interchange" attrs="{'invisible': [('compatible_edi_ids', '=', [])]}">
+ <field name="compatible_edi_ids" invisible="1" />
+ <field name="edi_format_ids" widget="many2many_checkboxes"/>
</group>
</xpath>
</field>
diff --git a/addons/account_edi/views/account_move_views.xml b/addons/account_edi/views/account_move_views.xml
new file mode 100644
index 00000000000..a5d8acdfe1a
--- /dev/null
+++ b/addons/account_edi/views/account_move_views.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="account_edi.action_open_edi_documents" model="ir.actions.act_window">
+ <field name="name">Electronic invoicing</field>
+ <field name="res_model">account.edi.document</field>
+ <field name="view_mode">tree</field>
+ <field name="domain">[('move_id', '=', active_id), ('error', '!=', False)]</field>
+ </record>
+
+ <record id="view_move_form_inherit" model="ir.ui.view">
+ <field name="name">account.move.form.inherit</field>
+ <field name="model">account.move</field>
+ <field name="inherit_id" ref="account.view_move_form" />
+ <field name="arch" type="xml">
+ <xpath expr="//button[@name='button_cancel']" position="after">
+ <field name="edi_show_cancel_button" invisible="1"/>
+ <button name="button_cancel_posted_moves"
+ string="Request EDI Cancellation"
+ type="object"
+ groups="account.group_account_invoice"
+ attrs="{'invisible' : [('edi_show_cancel_button', '=', False)]}"/>
+ </xpath>
+ <xpath expr="//header" position="after">
+ <div class="alert alert-info" role="alert" style="margin-bottom:0px;"
+ attrs="{'invisible': [('edi_web_services_to_process', '=', '')]}">
+ <div>The invoice will be sent asynchronously to :
+ <field name="edi_web_services_to_process" class="oe_inline"/>
+ </div>
+ <button name="action_process_edi_web_services" type="object" class="oe_link" string="Send now" />
+ </div>
+ <div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
+ attrs="{'invisible': [('edi_error_count', '=', 0)]}">
+ <field name="edi_error_count"/>
+ Electronic invoicing error(s)
+ <button name="%(account_edi.action_open_edi_documents)d" type="action" class="oe_link oe_inline" string="⇒ See errors" />
+ </div>
+ </xpath>
+ <xpath expr="//div[@name='journal_div']" position="after">
+ <field name="edi_document_ids" invisible="1" />
+ <field name="edi_state" invisible="1" />
+ <field name="edi_state" attrs="{'invisible': ['|', ('edi_document_ids', '=', []), ('state', '=', 'draft')]}"/>
+ </xpath>
+ </field>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/account_edi/views/account_payment_views.xml b/addons/account_edi/views/account_payment_views.xml
new file mode 100644
index 00000000000..355001eb02e
--- /dev/null
+++ b/addons/account_edi/views/account_payment_views.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="account_edi.action_open_payment_edi_documents" model="ir.actions.act_window">
+ <field name="name">Electronic invoicing</field>
+ <field name="res_model">account.edi.document</field>
+ <field name="view_mode">tree</field>
+ <field name="domain">[('move_id.payment_id', '=', active_id), ('error', '!=', False)]</field>
+ </record>
+
+ <record id="view_payment_form_inherit" model="ir.ui.view">
+ <field name="name">account.payment.form.inherit</field>
+ <field name="model">account.payment</field>
+ <field name="inherit_id" ref="account.view_account_payment_form" />
+ <field name="arch" type="xml">
+ <xpath expr="//header" position="after">
+ <div class="alert alert-info" role="alert" style="margin-bottom:0px;"
+ attrs="{'invisible': ['|', ('edi_web_services_to_process', '=', ''), ('state', '=', 'draft')]}">
+ <div>The payment will be sent asynchronously to :
+ <field name="edi_web_services_to_process" class="oe_inline"/>
+ </div>
+ <button name="action_process_edi_web_services" type="object" class="oe_link" string="Send now"/>
+ </div>
+ <div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
+ attrs="{'invisible': [('edi_error_count', '=', 0)]}">
+ <field name="edi_error_count"/>
+ Electronic invoicing error(s)
+ <button name="%(account_edi.action_open_payment_edi_documents)d" type="action" class="oe_link oe_inline" string="⇒ See errors" />
+ </div>
+ </xpath>
+ <xpath expr="//field[@name='journal_id']" position="after">
+ <field name="edi_document_ids" invisible="1" />
+ <field name="edi_state" invisible="1" />
+ <field name="edi_state" attrs="{'invisible': ['|', ('edi_document_ids', '=', []), ('state', '=', 'draft')]}"/>
+ </xpath>
+ </field>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/account_edi_facturx/models/account_edi_format.py b/addons/account_edi_facturx/models/account_edi_format.py
index a275a4eb2be..ed2db035fee 100644
--- a/addons/account_edi_facturx/models/account_edi_format.py
+++ b/addons/account_edi_facturx/models/account_edi_format.py
@@ -8,6 +8,7 @@ from odoo.exceptions import UserError
from datetime import datetime
from lxml import etree
from PyPDF2 import PdfFileReader
+import base64
import io
@@ -22,10 +23,29 @@ DEFAULT_FACTURX_DATE_FORMAT = '%Y%m%d'
class AccountEdiFormat(models.Model):
_inherit = 'account.edi.format'
- def _export_invoice_to_attachment(self, invoice):
+ def _is_compatible_with_journal(self, journal):
self.ensure_one()
+ res = super()._is_compatible_with_journal(journal)
if self.code != 'facturx_1_0_05':
- return super()._export_invoice_to_attachment(invoice)
+ return res
+ return journal.type == 'sale'
+
+ def _post_invoice_edi(self, invoices, test_mode=False):
+ self.ensure_one()
+ if self.code != 'facturx_1_0_05':
+ return super()._post_invoice_edi(invoices, test_mode=test_mode)
+ res = {}
+ for invoice in invoices:
+ attachment = self._export_facturx(invoice)
+ res[invoice] = {'attachment': attachment}
+ return res
+
+ def _is_embedding_to_invoice_pdf_needed(self):
+ # OVERRIDE
+ self.ensure_one()
+ return True if self.code == 'facturx_1_0_05' else super()._is_embedding_to_invoice_pdf_needed()
+
+ def _export_facturx(self, invoice):
def format_date(dt):
# Format the date in the Factur-x standard.
@@ -36,6 +56,7 @@ class AccountEdiFormat(models.Model):
# Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001).
return float_repr(number, currency.decimal_places)
+ self.ensure_one()
# Create file content.
template_values = {
'record': invoice,
@@ -88,12 +109,13 @@ class AccountEdiFormat(models.Model):
xml_content = b"<?xml version='1.0' encoding='UTF-8'?>"
xml_content += self.env.ref('account_edi_facturx.account_invoice_facturx_export')._render(template_values)
xml_name = '%s_facturx.xml' % (invoice.name.replace('/', '_'))
- return {'name': xml_name,
- 'datas': xml_content,
- 'res_model': 'account.move',
- 'res_id': invoice._origin.id,
- 'mimetype': 'application/xml'
- }
+ return self.env['ir.attachment'].create({
+ 'name': xml_name,
+ 'datas': base64.encodebytes(xml_content),
+ 'res_model': 'account.move',
+ 'res_id': invoice._origin.id,
+ 'mimetype': 'application/xml'
+ })
def _is_facturx(self, filename, tree):
return self.code == 'facturx_1_0_05' and tree.tag == '{urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100}CrossIndustryInvoice'
diff --git a/addons/account_edi_facturx/test_file/test_facturx.pdf b/addons/account_edi_facturx/test_file/test_facturx.pdf
new file mode 100644
index 00000000000..48f6cfa0a56
--- /dev/null
+++ b/addons/account_edi_facturx/test_file/test_facturx.pdf
Binary files differ
diff --git a/addons/account_edi_facturx/test_file/test_facturx.xml b/addons/account_edi_facturx/test_file/test_facturx.xml
new file mode 100644
index 00000000000..22b3ffdfcbd
--- /dev/null
+++ b/addons/account_edi_facturx/test_file/test_facturx.xml
@@ -0,0 +1,148 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<rsm:CrossIndustryInvoice xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
+ xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
+ xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">
+ <rsm:ExchangedDocumentContext>
+ <ram:GuidelineSpecifiedDocumentContextParameter>
+ <ram:ID>urn:cen.eu:en16931:2017</ram:ID>
+ </ram:GuidelineSpecifiedDocumentContextParameter>
+ </rsm:ExchangedDocumentContext>
+
+ <rsm:ExchangedDocument>
+ <ram:TypeCode>380</ram:TypeCode>
+ <ram:IssueDateTime>
+ <udt:DateTimeString format="102">20200501</udt:DateTimeString>
+ </ram:IssueDateTime>
+ </rsm:ExchangedDocument>
+
+ <rsm:SupplyChainTradeTransaction>
+ <ram:IncludedSupplyChainTradeLineItem>
+ <ram:AssociatedDocumentLineDocument>
+ <ram:LineID>1</ram:LineID>
+ </ram:AssociatedDocumentLineDocument>
+
+ <ram:SpecifiedTradeProduct>
+ <ram:SellerAssignedID>FURN_6741</ram:SellerAssignedID>
+ <ram:Name>[FURN_6741] Large Meeting Table
+Conference room table</ram:Name>
+ </ram:SpecifiedTradeProduct>
+
+ <ram:SpecifiedLineTradeAgreement>
+ <ram:GrossPriceProductTradePrice>
+ <ram:ChargeAmount currencyID="USD">3210.00</ram:ChargeAmount>
+ </ram:GrossPriceProductTradePrice>
+ </ram:SpecifiedLineTradeAgreement>
+
+ <ram:SpecifiedLineTradeDelivery>
+ <ram:BilledQuantity>5.0</ram:BilledQuantity>
+ </ram:SpecifiedLineTradeDelivery>
+
+ <ram:SpecifiedLineTradeSettlement>
+
+ <ram:SpecifiedTradeSettlementLineMonetarySummation>
+ <ram:LineTotalAmount currencyID="USD">3210.00</ram:LineTotalAmount>
+ </ram:SpecifiedTradeSettlementLineMonetarySummation>
+ </ram:SpecifiedLineTradeSettlement>
+
+ </ram:IncludedSupplyChainTradeLineItem>
+
+ <ram:IncludedSupplyChainTradeLineItem>
+ <ram:AssociatedDocumentLineDocument>
+ <ram:LineID>2</ram:LineID>
+ </ram:AssociatedDocumentLineDocument>
+
+ <ram:SpecifiedTradeProduct>
+ <ram:SellerAssignedID>FURN_8220</ram:SellerAssignedID>
+ <ram:Name>[FURN_8220] Four Person Desk
+Four person modern office workstation</ram:Name>
+ </ram:SpecifiedTradeProduct>
+
+ <ram:SpecifiedLineTradeAgreement>
+ <ram:GrossPriceProductTradePrice>
+ <ram:ChargeAmount currencyID="USD">1400.00</ram:ChargeAmount>
+ </ram:GrossPriceProductTradePrice>
+ </ram:SpecifiedLineTradeAgreement>
+
+ <ram:SpecifiedLineTradeDelivery>
+ <ram:BilledQuantity>5.0</ram:BilledQuantity>
+ </ram:SpecifiedLineTradeDelivery>
+
+ <ram:SpecifiedLineTradeSettlement>
+ <ram:SpecifiedTradeSettlementLineMonetarySummation>
+ <ram:LineTotalAmount currencyID="USD">1400.00</ram:LineTotalAmount>
+ </ram:SpecifiedTradeSettlementLineMonetarySummation>
+ </ram:SpecifiedLineTradeSettlement>
+
+ </ram:IncludedSupplyChainTradeLineItem>
+
+ <ram:ApplicableHeaderTradeAgreement>
+ <ram:SellerTradeParty>
+ <ram:Name>YourCompany</ram:Name>
+ <ram:DefinedTradeContact>
+ <ram:PersonName>YourCompany</ram:PersonName>
+ <ram:TelephoneUniversalCommunication>
+ <ram:CompleteNumber>+1 (650) 691-3277 </ram:CompleteNumber>
+ </ram:TelephoneUniversalCommunication>
+ <ram:EmailURIUniversalCommunication>
+ <ram:URIID schemeID="SMTP">info@yourcompany.example.com</ram:URIID>
+ </ram:EmailURIUniversalCommunication>
+ </ram:DefinedTradeContact>
+
+ <ram:PostalTradeAddress>
+ <ram:PostcodeCode>94134</ram:PostcodeCode>
+ <ram:LineOne>250 Executive Park Blvd, Suite 3400</ram:LineOne>
+
+ <ram:CityName>San Francisco</ram:CityName>
+ <ram:CountryID>US</ram:CountryID>
+ </ram:PostalTradeAddress>
+ </ram:SellerTradeParty>
+
+ <ram:BuyerTradeParty>
+ <ram:Name>Azure Interior</ram:Name>
+ <ram:DefinedTradeContact>
+ <ram:PersonName>Azure Interior</ram:PersonName>
+ <ram:TelephoneUniversalCommunication>
+ <ram:CompleteNumber>(870)-931-0505</ram:CompleteNumber>
+ </ram:TelephoneUniversalCommunication>
+ <ram:EmailURIUniversalCommunication>
+ <ram:URIID schemeID="SMTP">azure.Interior24@example.com</ram:URIID>
+ </ram:EmailURIUniversalCommunication>
+ </ram:DefinedTradeContact>
+
+ <ram:PostalTradeAddress>
+ <ram:PostcodeCode>94538</ram:PostcodeCode>
+ <ram:LineOne>4557 De Silva St</ram:LineOne>
+
+ <ram:CityName>Fremont</ram:CityName>
+ <ram:CountryID>US</ram:CountryID>
+ </ram:PostalTradeAddress>
+ </ram:BuyerTradeParty>
+
+ <ram:BuyerOrderReferencedDocument>
+ <ram:IssuerAssignedID>INV/2020/05/0001: INV/2020/05/0001</ram:IssuerAssignedID>
+ </ram:BuyerOrderReferencedDocument>
+ </ram:ApplicableHeaderTradeAgreement>
+
+ <ram:ApplicableHeaderTradeDelivery>
+ </ram:ApplicableHeaderTradeDelivery>
+
+ <ram:ApplicableHeaderTradeSettlement>
+ <ram:SpecifiedTradePaymentTerms>
+ <ram:Description>End of Following Month</ram:Description>
+ <ram:DueDateDateTime>
+ <udt:DateTimeString>20200630</udt:DateTimeString>
+ </ram:DueDateDateTime>
+ </ram:SpecifiedTradePaymentTerms>
+
+ <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
+ <ram:LineTotalAmount currencyID="USD">4610.00</ram:LineTotalAmount>
+ <ram:TaxBasisTotalAmount currencyID="USD">0.00</ram:TaxBasisTotalAmount>
+ <ram:TaxTotalAmount currencyID="USD">0.00</ram:TaxTotalAmount>
+ <ram:GrandTotalAmount currencyID="USD">4610.00</ram:GrandTotalAmount>
+ <ram:TotalPrepaidAmount currencyID="USD">0.00</ram:TotalPrepaidAmount>
+ <ram:DuePayableAmount currencyID="USD">4610.00</ram:DuePayableAmount>
+ </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
+ </ram:ApplicableHeaderTradeSettlement>
+
+ </rsm:SupplyChainTradeTransaction>
+</rsm:CrossIndustryInvoice>
diff --git a/addons/account_edi_facturx/tests/test_facturx.py b/addons/account_edi_facturx/tests/test_facturx.py
index bf7f03b1c74..f36382f121f 100644
--- a/addons/account_edi_facturx/tests/test_facturx.py
+++ b/addons/account_edi_facturx/tests/test_facturx.py
@@ -1,16 +1,15 @@
# -*- coding: utf-8 -*-
from freezegun import freeze_time
-
-from odoo.addons.account.tests.account_test_xml import AccountTestEdiCommon
+from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
-class TestAccountEdiFacturx(AccountTestEdiCommon):
+class TestAccountEdiFacturx(AccountEdiTestCommon):
@classmethod
- def setUpClass(cls, chart_template_ref=None):
- super().setUpClass(chart_template_ref=chart_template_ref)
+ def setUpClass(cls, chart_template_ref=None, edi_format_ref='account_edi_facturx.edi_facturx_1_0_05'):
+ super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
# ==== Init ====
@@ -40,12 +39,6 @@ class TestAccountEdiFacturx(AccountTestEdiCommon):
'children_tax_ids': [(6, 0, (cls.tax_10_include + cls.tax_20).ids)],
})
- # ==== EDI ====
-
- cls.facturx_edi_format = cls.env.ref('account_edi_facturx.edi_facturx_1_0_05')
- cls.journal = cls.company_data['default_journal_sale']
- cls.journal.edi_format_ids = [(6, 0, cls.facturx_edi_format.ids)]
-
# ==== Invoice ====
cls.invoice = cls.env['account.move'].create({
@@ -153,16 +146,13 @@ class TestAccountEdiFacturx(AccountTestEdiCommon):
</CrossIndustryInvoice>
'''
- @freeze_time('2017-02-01')
+ ####################################################
+ # Test export
+ ####################################################
+
def test_facturx(self):
''' Test the generated Facturx Edi attachment without any modification of the invoice. '''
-
- self.invoice.action_post()
-
- xml_content = self.facturx_edi_format._export_invoice_to_attachment(self.invoice)['datas']
- current_etree = self.get_xml_tree_from_string(xml_content)
- expected_etree = self.get_xml_tree_from_string(self.expected_invoice_facturx_values)
- self.assertXmlTreeEqual(current_etree, expected_etree)
+ self.assert_generated_file_equal(self.invoice, self.expected_invoice_facturx_values)
@freeze_time('2017-02-01')
def test_facturx_group_of_taxes(self):
@@ -171,56 +161,80 @@ class TestAccountEdiFacturx(AccountTestEdiCommon):
'invoice_line_ids': [(1, self.invoice.invoice_line_ids.id, {'tax_ids': [(6, 0, self.tax_group.ids)]})],
})
- self.invoice.action_post()
-
- xml_content = self.facturx_edi_format._export_invoice_to_attachment(self.invoice)['datas']
- current_etree = self.get_xml_tree_from_string(xml_content)
- expected_etree = self.with_applied_xpath(
- self.get_xml_tree_from_string(self.expected_invoice_facturx_values),
- '''
- <xpath expr="//GrossPriceProductTradePrice/ChargeAmount" position="replace">
- <ChargeAmount currencyID="Gol">275.000</ChargeAmount>
- </xpath>
- <xpath expr="//SpecifiedLineTradeSettlement" position="replace">
- <SpecifiedLineTradeSettlement>
- <ApplicableTradeTax>
- <RateApplicablePercent>10.0</RateApplicablePercent>
- </ApplicableTradeTax>
- <ApplicableTradeTax>
- <RateApplicablePercent>20.0</RateApplicablePercent>
- </ApplicableTradeTax>
- <SpecifiedTradeSettlementLineMonetarySummation>
- <LineTotalAmount currencyID="Gol">1000.000</LineTotalAmount>
- </SpecifiedTradeSettlementLineMonetarySummation>
- </SpecifiedLineTradeSettlement>
- </xpath>
- <xpath expr="//ApplicableHeaderTradeSettlement" position="replace">
- <ApplicableHeaderTradeSettlement>
- <ApplicableTradeTax>
- <CalculatedAmount currencyID="Gol">220.000</CalculatedAmount>
- <BasisAmount currencyID="Gol">1100.000</BasisAmount>
- <RateApplicablePercent>20.0</RateApplicablePercent>
- </ApplicableTradeTax>
- <ApplicableTradeTax>
- <CalculatedAmount currencyID="Gol">100.000</CalculatedAmount>
- <BasisAmount currencyID="Gol">1000.000</BasisAmount>
- <RateApplicablePercent>10.0</RateApplicablePercent>
- </ApplicableTradeTax>
- <SpecifiedTradePaymentTerms>
- <DueDateDateTime>
- <DateTimeString>20170101</DateTimeString>
- </DueDateDateTime>
- </SpecifiedTradePaymentTerms>
- <SpecifiedTradeSettlementHeaderMonetarySummation>
- <LineTotalAmount currencyID="Gol">1000.000</LineTotalAmount>
- <TaxBasisTotalAmount currencyID="Gol">1000.000</TaxBasisTotalAmount>
- <TaxTotalAmount currencyID="Gol">320.000</TaxTotalAmount>
- <GrandTotalAmount currencyID="Gol">1320.000</GrandTotalAmount>
- <TotalPrepaidAmount currencyID="Gol">0.000</TotalPrepaidAmount>
- <DuePayableAmount currencyID="Gol">1320.000</DuePayableAmount>
- </SpecifiedTradeSettlementHeaderMonetarySummation>
- </ApplicableHeaderTradeSettlement>
- </xpath>
- ''',
- )
- self.assertXmlTreeEqual(current_etree, expected_etree)
+ applied_xpath = '''
+ <xpath expr="//GrossPriceProductTradePrice/ChargeAmount" position="replace">
+ <ChargeAmount currencyID="Gol">275.000</ChargeAmount>
+ </xpath>
+ <xpath expr="//SpecifiedLineTradeSettlement" position="replace">
+ <SpecifiedLineTradeSettlement>
+ <ApplicableTradeTax>
+ <RateApplicablePercent>10.0</RateApplicablePercent>
+ </ApplicableTradeTax>
+ <ApplicableTradeTax>
+ <RateApplicablePercent>20.0</RateApplicablePercent>
+ </ApplicableTradeTax>
+ <SpecifiedTradeSettlementLineMonetarySummation>
+ <LineTotalAmount currencyID="Gol">1000.000</LineTotalAmount>
+ </SpecifiedTradeSettlementLineMonetarySummation>
+ </SpecifiedLineTradeSettlement>
+ </xpath>
+ <xpath expr="//ApplicableHeaderTradeSettlement" position="replace">
+ <ApplicableHeaderTradeSettlement>
+ <ApplicableTradeTax>
+ <CalculatedAmount currencyID="Gol">220.000</CalculatedAmount>
+ <BasisAmount currencyID="Gol">1100.000</BasisAmount>
+ <RateApplicablePercent>20.0</RateApplicablePercent>
+ </ApplicableTradeTax>
+ <ApplicableTradeTax>
+ <CalculatedAmount currencyID="Gol">100.000</CalculatedAmount>
+ <BasisAmount currencyID="Gol">1000.000</BasisAmount>
+ <RateApplicablePercent>10.0</RateApplicablePercent>
+ </ApplicableTradeTax>
+ <SpecifiedTradePaymentTerms>
+ <DueDateDateTime>
+ <DateTimeString>20170101</DateTimeString>
+ </DueDateDateTime>
+ </SpecifiedTradePaymentTerms>
+ <SpecifiedTradeSettlementHeaderMonetarySummation>
+ <LineTotalAmount currencyID="Gol">1000.000</LineTotalAmount>
+ <TaxBasisTotalAmount currencyID="Gol">1000.000</TaxBasisTotalAmount>
+ <TaxTotalAmount currencyID="Gol">320.000</TaxTotalAmount>
+ <GrandTotalAmount currencyID="Gol">1320.000</GrandTotalAmount>
+ <TotalPrepaidAmount currencyID="Gol">0.000</TotalPrepaidAmount>
+ <DuePayableAmount currencyID="Gol">1320.000</DuePayableAmount>
+ </SpecifiedTradeSettlementHeaderMonetarySummation>
+ </ApplicableHeaderTradeSettlement>
+ </xpath>
+ '''
+
+ self.assert_generated_file_equal(self.invoice, self.expected_invoice_facturx_values, applied_xpath)
+
+ ####################################################
+ # Test import
+ ####################################################
+
+ def test_invoice_edi_pdf(self):
+ invoice = self.env['account.move'].with_context(default_move_type='in_invoice').create({})
+ invoice_count = len(self.env['account.move'].search([]))
+ self.update_invoice_from_file('account_edi_facturx', 'test_file', 'test_facturx.pdf', invoice)
+
+ self.assertEqual(len(self.env['account.move'].search([])), invoice_count)
+ self.assertEqual(invoice.amount_total, 525)
+
+ self.create_invoice_from_file('account_edi_facturx', 'test_file', 'test_facturx.pdf')
+
+ self.assertEqual(invoice.amount_total, 525)
+ self.assertEqual(len(self.env['account.move'].search([])), invoice_count + 1)
+
+ def test_invoice_edi_xml(self):
+ invoice = self.env['account.move'].with_context(default_move_type='in_invoice').create({})
+ invoice_count = len(self.env['account.move'].search([]))
+ self.update_invoice_from_file('account_edi_facturx', 'test_file', 'test_facturx.xml', invoice)
+
+ self.assertEqual(len(self.env['account.move'].search([])), invoice_count)
+ self.assertEqual(invoice.amount_total, 4610)
+
+ self.create_invoice_from_file('account_edi_facturx', 'test_file', 'test_facturx.xml')
+
+ self.assertEqual(invoice.amount_total, 4610)
+ self.assertEqual(len(self.env['account.move'].search([])), invoice_count + 1)
diff --git a/addons/l10n_be_edi/models/account_edi_format.py b/addons/l10n_be_edi/models/account_edi_format.py
index 23bdf93c953..2c34d7f5fd6 100644
--- a/addons/l10n_be_edi/models/account_edi_format.py
+++ b/addons/l10n_be_edi/models/account_edi_format.py
@@ -3,6 +3,8 @@
from odoo import api, models, fields
from odoo.tools import float_repr
+import base64
+
class AccountEdiFormat(models.Model):
_inherit = 'account.edi.format'
@@ -22,25 +24,40 @@ class AccountEdiFormat(models.Model):
return self._import_ubl(tree, invoice)
return super()._update_invoice_from_xml_tree(filename, tree, invoice)
- def _export_invoice_to_attachment(self, invoice):
+ def _is_compatible_with_journal(self, journal):
self.ensure_one()
+ res = super()._is_compatible_with_journal(journal)
if self.code != 'efff_1':
- return super()._export_invoice_to_attachment(invoice)
+ return res
+ return journal.type == 'sale' and journal.country_code == 'BE'
+ def _post_invoice_edi(self, invoices, test_mode=False):
+ self.ensure_one()
+ if self.code != 'efff_1':
+ return super()._post_invoice_edi(invoices, test_mode=test_mode)
+ res = {}
+ for invoice in invoices:
+ attachment = self._export_efff(invoice)
+ res[invoice] = {'attachment': attachment}
+ return res
+
+ def _export_efff(self, invoice):
+ self.ensure_one()
# Create file content.
xml_content = b"<?xml version='1.0' encoding='UTF-8'?>"
xml_content += self.env.ref('account_edi_ubl.export_ubl_invoice')._render(invoice._get_ubl_values())
vat = invoice.company_id.partner_id.commercial_partner_id.vat
xml_name = 'efff-%s%s%s.xml' % (vat or '', '-' if vat else '', invoice.name.replace('/', '_')) # official naming convention
- return {'name': xml_name,
- 'datas': xml_content,
- 'res_model': 'account.move',
- 'res_id': invoice._origin.id,
- 'mimetype': 'application/xml'
- }
-
- def _export_invoice_to_embed_to_pdf(self, pdf_content, invoice):
+ return self.env['ir.attachment'].create({
+ 'name': xml_name,
+ 'datas': base64.encodebytes(xml_content),
+ 'res_model': 'account.move',
+ 'res_id': invoice._origin.id,
+ 'mimetype': 'application/xml'
+ })
+
+ def _is_embedding_to_invoice_pdf_needed(self):
self.ensure_one()
if self.code != 'efff_1':
- return super()._export_invoice_to_embed_to_pdf(pdf_content, invoice)
+ return super()._is_embedding_to_invoice_pdf_needed()
return False # ubl must not be embedded to PDF.
diff --git a/addons/l10n_be_edi/tests/test_ubl.py b/addons/l10n_be_edi/tests/test_ubl.py
index 1c4348a2aa7..ae99c754c6d 100644
--- a/addons/l10n_be_edi/tests/test_ubl.py
+++ b/addons/l10n_be_edi/tests/test_ubl.py
@@ -1,35 +1,20 @@
# -*- coding: utf-8 -*-
-from odoo import tools, _
-from odoo.tests import common
-import base64
-from odoo.modules.module import get_module_resource
+from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
-class TestUBL(common.TransactionCase):
- def setUp(self):
- super(TestUBL, self).setUp()
- # Force user Belgium country.
- self.env.user.company_id = self.env['res.company'].create({'name': 'MyCompany'})
- self.env.user.company_id.country_id = self.env.ref('base.be')
- self.env.ref('l10n_be.l10nbe_chart_template').try_loading()
- self.partner_id = self.env['res.partner'].create({'name': 'TestUser', 'vat': 'BE0123456789'})
- def test_ubl_invoice_import(self):
- xml_file_path = get_module_resource('l10n_be_edi', 'test_xml_file', 'efff_test.xml')
- xml_file = open(xml_file_path, 'rb').read()
- invoice = self.env['account.move'].with_context(default_move_type='in_invoice').create({})
+class TestUBL(AccountEdiTestCommon):
+ @classmethod
+ def setUpClass(cls, chart_template_ref='l10n_be.l10nbe_chart_template', edi_format_ref='l10n_be_edi.edi_efff_1'):
+ super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
- attachment_id = self.env['ir.attachment'].create({
- 'name': 'efff_test.xml',
- 'datas': base64.encodebytes(xml_file),
- 'res_id': invoice.id,
- 'res_model': 'account.move',
- })
+ cls.partner_a.vat = 'BE0123456789'
+ def test_invoice_edi_xml(self):
+ invoice = self.env['account.move'].with_context(default_move_type='in_invoice').create({})
invoice_count = len(self.env['account.move'].search([]))
-
- invoice.message_post(attachment_ids=[attachment_id.id])
+ self.update_invoice_from_file('l10n_be_edi', 'test_xml_file', 'efff_test.xml', invoice)
self.assertEqual(len(self.env['account.move'].search([])), invoice_count)
self.assertEqual(invoice.amount_total, 666.50)
self.assertEqual(invoice.amount_tax, 115.67)
- self.assertEqual(invoice.partner_id, self.partner_id)
+ self.assertEqual(invoice.partner_id, self.partner_a)
diff --git a/addons/l10n_it_edi/data/account_edi_data.xml b/addons/l10n_it_edi/data/account_edi_data.xml
index 9dbd1dec98d..fb454fffbd1 100644
--- a/addons/l10n_it_edi/data/account_edi_data.xml
+++ b/addons/l10n_it_edi/data/account_edi_data.xml
@@ -5,7 +5,6 @@
<record id="edi_fatturaPA" model="account.edi.format">
<field name="name">Fattura PA (IT)</field>
<field name="code">fattura_pa</field>
- <field name="hide_on_journal">import</field>
</record>
</data>
diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py
index e24f3efa2b6..2b53e09b322 100644
--- a/addons/l10n_it_edi/models/account_edi_format.py
+++ b/addons/l10n_it_edi/models/account_edi_format.py
@@ -20,6 +20,17 @@ DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
class AccountEdiFormat(models.Model):
_inherit = 'account.edi.format'
+ def _is_embedding_to_invoice_pdf_needed(self):
+ # OVERRIDE
+ self.ensure_one()
+ return True if self.code == 'fattura_pa' else super()._is_embedding_to_invoice_pdf_needed()
+
+ def _is_compatible_with_journal(self, journal):
+ self.ensure_one()
+ if self.code != 'fattura_pa':
+ return super()._is_compatible_with_journal(journal)
+ return False # edi does not support generic export
+
def _check_filename_is_fattura_pa(self, filename):
return re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", filename)