diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 1e39c7eeaad..4f4d5c70afb 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -2852,9 +2852,9 @@ class AccountMove(models.Model): """Main entry point to extend/enhance invoices with attachments. Either coming from the chatter or the journal. It will unwrap all - attachemnts by priority then try to decode untill it succeed. + attachments by priority then try to decode until it succeed. - :returns: True if at least one doccument successfully imported + :returns: True if at least one document is successfully imported """ success = False @@ -2864,9 +2864,11 @@ class AccountMove(models.Model): try: if decoder and not success: - with self._get_edi_creation() as invoice: + with self.env.cr.savepoint(), self._get_edi_creation() as invoice: # pylint: disable=not-callable success = decoder(invoice, file_data, new) + if success: + invoice._link_bill_origin_to_purchase_orders(timeout=4) except RedirectWarning as rw: raise rw @@ -3503,6 +3505,12 @@ class AccountMove(models.Model): # hook to be used with purchase, so that vendor bills are sync/autocompleted with purchase orders self.ensure_one() + def _link_bill_origin_to_purchase_orders(self, timeout=10): + for move in self.filtered(lambda m: m.move_type in self.get_purchase_types()): + references = [move.invoice_origin] if move.invoice_origin else [] + move._find_and_set_purchase_orders(references, move.partner_id.id, move.amount_total, timeout) + return self + # ------------------------------------------------------------------------- # PUBLIC ACTIONS # ------------------------------------------------------------------------- diff --git a/addons/account_edi/models/account_journal.py b/addons/account_edi/models/account_journal.py index 268fe38ed17..be5d975d79f 100644 --- a/addons/account_edi/models/account_journal.py +++ b/addons/account_edi/models/account_journal.py @@ -81,12 +81,3 @@ class AccountJournal(models.Model): protected_edi_formats = journal.edi_format_ids.filtered(lambda e: e.id in protected_edi_format_ids) journal.edi_format_ids = enabled_edi_formats + protected_edi_formats - - def _create_document_from_attachment(self, attachment_ids): - # tries to match purchasing orders - moves = super()._create_document_from_attachment(attachment_ids) - for move in moves: - if move.move_type == 'in_invoice': - references = [move.invoice_origin] if move.invoice_origin else [] - move._find_and_set_purchase_orders(references, move.partner_id.id, move.amount_total, timeout=4) - return moves diff --git a/addons/account_edi_proxy_client/__manifest__.py b/addons/account_edi_proxy_client/__manifest__.py index 6e5104bcfa9..478a52fe9ed 100644 --- a/addons/account_edi_proxy_client/__manifest__.py +++ b/addons/account_edi_proxy_client/__manifest__.py @@ -17,6 +17,7 @@ Odoo database. }, 'data': [ 'security/ir.model.access.csv', + 'security/account_edi_proxy_client_security.xml', ], 'installable': True, 'license': 'LGPL-3', diff --git a/addons/account_edi_proxy_client/models/account_edi_proxy_user.py b/addons/account_edi_proxy_client/models/account_edi_proxy_user.py index 57b2f825f39..8250d38d170 100644 --- a/addons/account_edi_proxy_client/models/account_edi_proxy_user.py +++ b/addons/account_edi_proxy_client/models/account_edi_proxy_user.py @@ -99,8 +99,7 @@ class AccountEdiProxyClientUser(models.Model): error_code = proxy_error['code'] if error_code == 'refresh_token_expired': self._renew_token() - if not self.env.context.get('test_skip_commit'): - self.env.cr.commit() # We do not want to lose it if in the _make_request below something goes wrong + self.env.cr.commit() # We do not want to lose it if in the _make_request below something goes wrong return self._make_request(url, params) if error_code == 'no_such_user': # This error is also raised if the user didn't exchange data and someone else claimed the edi_identificaiton. diff --git a/addons/account_edi_proxy_client/security/account_edi_proxy_client_security.xml b/addons/account_edi_proxy_client/security/account_edi_proxy_client_security.xml new file mode 100644 index 00000000000..8ac46b1145c --- /dev/null +++ b/addons/account_edi_proxy_client/security/account_edi_proxy_client_security.xml @@ -0,0 +1,10 @@ + + + + + Account EDI Proxy Client User + + [('company_id', 'in', company_ids)] + + + diff --git a/addons/l10n_it_edi/i18n/it.po b/addons/l10n_it_edi/i18n/it.po index a0b5da21802..eae7f36afd0 100644 --- a/addons/l10n_it_edi/i18n/it.po +++ b/addons/l10n_it_edi/i18n/it.po @@ -638,13 +638,13 @@ msgstr "Notifica esito: %s
%s" #. module: l10n_it_edi #: model:ir.model.fields,field_description:l10n_it_edi.field_res_partner__l10n_it_pa_index #: model:ir.model.fields,field_description:l10n_it_edi.field_res_users__l10n_it_pa_index -msgid "PA index" -msgstr "Codice univoco" +msgid "Destination Code" +msgstr "Codice Destinatario" #. module: l10n_it_edi #: model:ir.model.constraint,message:l10n_it_edi.constraint_res_partner_l10n_it_pa_index -msgid "PA index must have between 6 and 7 characters." -msgstr "Il codice univoco deve avere 6/7 caratteri" +msgid "Destination Code must have between 6 and 7 characters." +msgstr "Il Codice Destinatario deve avere 6/7 caratteri" #. module: l10n_it_edi #: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_it_FatturaPA_export diff --git a/addons/l10n_it_edi/i18n/l10n_it_edi.pot b/addons/l10n_it_edi/i18n/l10n_it_edi.pot index 6f668e4515f..387f04d4f0c 100644 --- a/addons/l10n_it_edi/i18n/l10n_it_edi.pot +++ b/addons/l10n_it_edi/i18n/l10n_it_edi.pot @@ -509,12 +509,12 @@ msgstr "" #. module: l10n_it_edi #: model:ir.model.fields,field_description:l10n_it_edi.field_res_partner__l10n_it_pa_index #: model:ir.model.fields,field_description:l10n_it_edi.field_res_users__l10n_it_pa_index -msgid "PA index" +msgid "Destination Code" msgstr "" #. module: l10n_it_edi #: model:ir.model.constraint,message:l10n_it_edi.constraint_res_partner_l10n_it_pa_index -msgid "PA index must have between 6 and 7 characters." +msgid "Destination Code must have between 6 and 7 characters." msgstr "" #. module: l10n_it_edi diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py index 55cf020820d..41a2c7a307b 100644 --- a/addons/l10n_it_edi/models/account_edi_format.py +++ b/addons/l10n_it_edi/models/account_edi_format.py @@ -290,63 +290,76 @@ class AccountEdiFormat(models.Model): # ------------------------------------------------------------------------- def _cron_receive_fattura_pa(self): - ''' Check the proxy for incoming invoices. + ''' Check the proxy for incoming invoices for all companies. ''' - proxy_users = self.env['account_edi_proxy_client.user'].search([('edi_format_id', '=', self.env.ref('l10n_it_edi.edi_fatturaPA').id)]) - - if proxy_users._get_demo_state() == 'demo': + if self.env['account_edi_proxy_client.user']._get_demo_state() == 'demo': return - for proxy_user in proxy_users: - company = proxy_user.company_id + for proxy_user in self.env['account_edi_proxy_client.user'].search([('edi_format_code', '=', 'fattura_pa')]): + self._receive_fattura_pa(proxy_user) + + def _receive_fattura_pa(self, proxy_user): + ''' Check the proxy for incoming invoices for a specified proxy user. + ''' + try: + res = proxy_user._make_request( + proxy_user._get_server_url() + '/api/l10n_it_edi/1/in/RicezioneInvoice', + params={'recipient_codice_fiscale': proxy_user.company_id.l10n_it_codice_fiscale}) + except AccountEdiProxyError as e: + res = {} + _logger.error('Error while receiving file from SdiCoop: %s', e) + + proxy_acks = [] + for id_transaction, fattura in res.items(): + if self._save_incoming_attachment_fattura_pa(proxy_user, id_transaction, fattura['filename'], fattura['file'], fattura['key']): + proxy_acks.append(id_transaction) + + if proxy_acks: try: - res = proxy_user._make_request(proxy_user._get_server_url() + '/api/l10n_it_edi/1/in/RicezioneInvoice', - params={'recipient_codice_fiscale': company.l10n_it_codice_fiscale}) + proxy_user._make_request( + proxy_user._get_server_url() + '/api/l10n_it_edi/1/ack', + params={'transaction_ids': proxy_acks}) except AccountEdiProxyError as e: - res = {} _logger.error('Error while receiving file from SdiCoop: %s', e) - proxy_acks = [] - for id_transaction, fattura in res.items(): - if self.env['ir.attachment'].search([('name', '=', fattura['filename']), ('res_model', '=', 'account.move')], limit=1): - # name should be unique, the invoice already exists - _logger.info('E-invoice already exists: %s', fattura['filename']) - proxy_acks.append(id_transaction) - continue + def _save_incoming_attachment_fattura_pa(self, proxy_user, id_transaction, filename, content, key): + ''' Save an incoming file from the SdI as an attachment. + Commits if successful. - file = proxy_user._decrypt_data(fattura['file'], fattura['key']) + :param proxy_user: the user that saves the attachment. + :param id_transaction: id of the SdI transaction for communication with the IAP proxy. + :param filename: name of the file to be saved. + :param content: encrypted content of the file to be saved. + :param key: key to decrypt the file. + :returns: True if everything went well, or the file already exists. + False if the file cannot be parsed as an XML. + ''' - try: - tree = etree.fromstring(file) - except Exception: - # should not happen as the file has been checked by SdiCoop - _logger.info('Received file badly formatted, skipping: \n %s', file) - continue + if self.env['ir.attachment'].search([('name', '=', filename), ('res_model', '=', 'account.move')], limit=1): + # name should be unique, the invoice already exists + _logger.info('E-invoice already exists: %s', filename) + return True - attachment = self.env['ir.attachment'].create({ - 'name': fattura['filename'], - 'raw': file, - 'type': 'binary', - }) + decrypted_content = proxy_user._decrypt_data(content, key) + attachment = self.env['ir.attachment'].create({ + 'name': filename, + 'raw': decrypted_content, + 'type': 'binary' + }) - if not self.env.context.get('test_skip_commit'): - self.env.cr.commit() # In case something fails after, we still have the attachment + # Import the move from the attachment. + # `_create_document_from_attachment` will create an empty move + # then try and fill it with the content imported from the attachment. + # Should the import fail, thanks to try..except and savepoint, + # we will anyway end up with an empty `in_invoice` with the attachment posted on it. + (self.env['account.journal'].with_company(proxy_user.company_id) + .with_context(default_move_type='in_invoice') + ._create_document_from_attachment(attachment.ids)) - self.env['account.journal'].with_company(company)\ - .with_context(default_move_type='in_invoice')\ - ._create_document_from_attachment(attachment.ids) + # Commit the created move, be it correctly imported or empty. + self.env.cr.commit() - proxy_acks.append(id_transaction) - if not self.env.context.get('test_skip_commit'): - self.env.cr.commit() - - - if proxy_acks: - try: - proxy_user._make_request(proxy_user._get_server_url() + '/api/l10n_it_edi/1/ack', - params={'transaction_ids': proxy_acks}) - except AccountEdiProxyError as e: - _logger.error('Error while receiving file from SdiCoop: %s', e) + return True 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}.((?i:xml.p7m|xml))", filename) @@ -525,6 +538,13 @@ class AccountEdiFormat(models.Model): invoice._compose_info_message(elements[0], '.') )) + # Information related to the purchase order <2.1.2> + po_refs = [] + elements = tree.xpath('//DatiGenerali/DatiOrdineAcquisto/IdDocumento') + if elements: + po_refs = [element.text.strip() for element in elements] + invoice.invoice_origin = ", ".join(po_refs) + # Total amount. <2.4.2.6> elements = tree.xpath('.//ImportoPagamento') amount_total_import = 0 @@ -575,7 +595,10 @@ class AccountEdiFormat(models.Model): elements = tree.xpath('.//DatiBeniServizi') for element in (elements or []): - invoice_line_form = invoice.invoice_line_ids.create({'move_id': invoice.id}) + invoice_line_form = invoice.invoice_line_ids.create({ + 'move_id': invoice.id, + 'tax_ids': [fields.Command.clear()], + }) if invoice_line_form: message_to_log += self._import_fattura_pa_line(element, invoice_line_form, extra_info) diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py index 6b5d122f6b0..ec7e356d82d 100644 --- a/addons/l10n_it_edi/models/account_invoice.py +++ b/addons/l10n_it_edi/models/account_invoice.py @@ -333,7 +333,7 @@ class AccountMove(models.Model): self.env['account.edi.format']._import_fattura_pa(tree_list[0], invoice) # there might be other invoices inside the EDI. - for tree in tree_list[:1]: + for tree in tree_list[1:]: other_invoice = invoice.create({ 'journal_id': invoice.journal_id.id, }) diff --git a/addons/l10n_it_edi/models/res_partner.py b/addons/l10n_it_edi/models/res_partner.py index 300c6645fdf..36e8cd26904 100644 --- a/addons/l10n_it_edi/models/res_partner.py +++ b/addons/l10n_it_edi/models/res_partner.py @@ -14,7 +14,7 @@ class ResPartner(models.Model): l10n_it_pec_email = fields.Char(string="PEC e-mail") l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16) - l10n_it_pa_index = fields.Char(string="PA index", + l10n_it_pa_index = fields.Char(string="Destination Code", size=7, help="Must contain the 6-character (or 7) code, present in the PA\ Index in the information relative to the electronic invoicing service,\ @@ -28,7 +28,7 @@ class ResPartner(models.Model): ('l10n_it_pa_index', "CHECK(l10n_it_pa_index IS NULL OR l10n_it_pa_index = '' OR LENGTH(l10n_it_pa_index) >= 6)", - "PA index must have between 6 and 7 characters."), + "Destination Code must have between 6 and 7 characters."), ] @api.model diff --git a/addons/l10n_it_edi/tests/common.py b/addons/l10n_it_edi/tests/common.py index de95ea23cdf..07beff11288 100644 --- a/addons/l10n_it_edi/tests/common.py +++ b/addons/l10n_it_edi/tests/common.py @@ -9,14 +9,6 @@ from odoo.tests import tagged from odoo.addons.account_edi.tests.common import AccountEdiTestCommon from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyClientUser -def patch_proxy_user(func): - @patch.object(AccountEdiProxyClientUser, '_make_request', MagicMock(spec=AccountEdiProxyClientUser._make_request)) - @patch.object(AccountEdiProxyClientUser, '_decrypt_data', MagicMock(spec=AccountEdiProxyClientUser._decrypt_data)) - @patch.object(AccountEdiProxyClientUser, '_get_demo_state', MagicMock(spec=AccountEdiProxyClientUser._get_demo_state)) - def patched(self, *args, **kwargs): - return func(self, *args, **kwargs) - return patched - @tagged('post_install_l10n', 'post_install', '-at_install') class TestItEdi(AccountEdiTestCommon): diff --git a/addons/l10n_it_edi/tests/test_edi_import.py b/addons/l10n_it_edi/tests/test_edi_import.py index 0f0917062be..daf398306d1 100644 --- a/addons/l10n_it_edi/tests/test_edi_import.py +++ b/addons/l10n_it_edi/tests/test_edi_import.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields +from unittest.mock import MagicMock, patch + +from odoo import fields, sql_db from odoo.tests import tagged -from odoo.addons.l10n_it_edi.tests.common import TestItEdi, patch_proxy_user +from odoo.addons.l10n_it_edi.tests.common import TestItEdi @tagged('post_install_l10n', 'post_install', '-at_install') @@ -68,31 +70,31 @@ class TestItEdiImport(TestItEdi): }], }]) - @patch_proxy_user def test_receive_same_vendor_bill_twice(self): """ Test that the second time we are receiving an SdiCoop invoice, the second is discarded """ - # The make_request function is called twice when running the _cron_receive_fattura_pa - # first to the /in/RicezioneInvoice endpoint (to find new incoming invoices) - # second to the /api/l10n_it_edi/1/ack to acknowledge the invoices have been recieved + fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA') content = self.fake_test_content.encode() - fake_responses = [ - # Response of the format id_transaction: fattura dict - {'9999999999': {'filename': 'IT01234567890_FPR02.xml', 'key': '123', 'file': content}}, - # The response from the _make_request for the ack can be None - None, - ] * 2 # Since the cron is run twice, and we want the fake results both times - self.proxy_user._make_request.side_effect = fake_responses - # When calling the decrypt function, the file we're accessing is already decrypted, just return the file - self.proxy_user._decrypt_data.side_effect = lambda file, _key: file - # In order for the cron function to progress to the point that it imports, we cannot be in demo mode - self.proxy_user._get_demo_state.return_value = 'unit_test' - # TODO: we need to see it does not apply to non-test companies which it does right now (missing security rule on client user) + # Our test content is not encrypted + proxy_user = MagicMock() + proxy_user.company_id = self.company + proxy_user._decrypt_data.return_value = content + + def mock_commit(self): + pass + + with patch.object(sql_db.Cursor, "commit", mock_commit): + for dummy in range(2): + fattura_pa._save_incoming_attachment_fattura_pa( + proxy_user=proxy_user, + id_transaction='9999999999', + filename='IT01234567890_FPR02.xml', + content=content, + key=None) - #self.edi_format.with_context({'test_skip_commit': True}).sudo()._cron_receive_fattura_pa() # There should be one attachement with this filename - #attachment = self.env['ir.attachment'].search([('name', '=', self.invoice_filename2)]) - #self.assertEqual(len(attachment), 1) - #invoice = self.env['account.move'].search([('payment_reference', '=', 'TWICE_TEST')]) - #self.assertEqual(len(invoice), 1) + attachments = self.env['ir.attachment'].search([('name', '=', 'IT01234567890_FPR02.xml')]) + self.assertEqual(len(attachments), 1) + invoices = self.env['account.move'].search([('payment_reference', '=', 'TWICE_TEST')]) + self.assertEqual(len(invoices), 1) diff --git a/addons/l10n_it_edi_withholding/models/account_edi_format.py b/addons/l10n_it_edi_withholding/models/account_edi_format.py index e8f118b75e0..7ea839bdc74 100644 --- a/addons/l10n_it_edi_withholding/models/account_edi_format.py +++ b/addons/l10n_it_edi_withholding/models/account_edi_format.py @@ -11,6 +11,14 @@ _logger = logging.getLogger(__name__) class AccountEdiFormat(models.Model): _inherit = 'account.edi.format' + def _l10n_it_edi_search_tax_for_import(self, company, percentage, extra_domain=None, vat_only=True): + """ In case no withholding_type or pension_fund is specified, exclude taxes that have it. + It means that we're searching for VAT taxes, especially in the base l10n_it_edi module + """ + if vat_only: + extra_domain += [('l10n_it_withholding_type', '=', False), ('l10n_it_pension_fund_type', '=', False)] + return super()._l10n_it_edi_search_tax_for_import(company, percentage, extra_domain) + def _l10n_it_edi_check_taxes_configuration(self, invoice): """ Override to also allow pension_fund, withholding taxes. @@ -38,9 +46,12 @@ class AccountEdiFormat(models.Model): withholding_type = tipo_ritenuta.text if tipo_ritenuta is not None else "RT02" withholding_reason = reason.text if reason is not None else "A" withholding_percentage = -float(percentage.text if percentage is not None else "0.0") - withholding_tax = self._l10n_it_edi_search_tax_for_import(company, withholding_percentage, + withholding_tax = self._l10n_it_edi_search_tax_for_import( + company, + withholding_percentage, [('l10n_it_withholding_type', '=', withholding_type), - ('l10n_it_withholding_reason', '=', withholding_reason)]) + ('l10n_it_withholding_reason', '=', withholding_reason)], + vat_only=False) if withholding_tax: withholding_taxes.append(withholding_tax) else: @@ -59,8 +70,11 @@ class AccountEdiFormat(models.Model): pension_fund_type = pension_fund_type.text if pension_fund_type is not None else "" tax_factor_percent = float(tax_factor_percent.text or "0.0") vat_tax_factor_percent = float(vat_tax_factor_percent.text or "0.0") - pension_fund_tax = self._l10n_it_edi_search_tax_for_import(company, tax_factor_percent, - [('l10n_it_pension_fund_type', '=', pension_fund_type)]) + pension_fund_tax = self._l10n_it_edi_search_tax_for_import( + company, + tax_factor_percent, + [('l10n_it_pension_fund_type', '=', pension_fund_type)], + vat_only=False) if pension_fund_tax: pension_fund_taxes.append(pension_fund_tax) else: @@ -100,9 +114,11 @@ class AccountEdiFormat(models.Model): continue enasarco_amount = float(number_text) enasarco_percentage = -self.env.company.currency_id.round(enasarco_amount / price_subtotal * 100) - enasarco_tax = self._l10n_it_edi_search_tax_for_import(company, enasarco_percentage, [ - ('l10n_it_pension_fund_type', '=', 'TC07'), - ]) + enasarco_tax = self._l10n_it_edi_search_tax_for_import( + company, + enasarco_percentage, + [('l10n_it_pension_fund_type', '=', 'TC07')], + vat_only=False) if enasarco_tax: invoice_line_form.tax_ids |= enasarco_tax else: