[IMP] l10n_it_edi: remove account_edi from depends

The account_edi framework is no more a dependancy.

- Models account.edi.document and account.edi.format have been removed.
  All the functions have been moved to the new account.move.send,
  account.move, account.edi.proxy.client.user
- The move now holds the l10n_it_edi_state which admits all values that the SdI sends us as a status update
- The sending is now not handled by the cron but by the Send and Print dialog.
- Vendor bills Tax Integration sending by dedicated button
- The move form will now hold the state of the EDI transaction, its number and the EDI attachment as a Binary field on the move itself.
- A new banner will keep the latest message from the SdI, that's not the account_edi old banner.
- All the user messages have been simplified.

IAP-apps PR: odoo/iap-apps#652
Upgrade PR: odoo/upgrade#4893
Task link: https://www.odoo.com/web#id=3339971&cids=1&model=project.task
Task-3339971

Part-of: odoo/odoo#122194
This commit is contained in:
Paolo Gatti (pgi)
2023-09-19 16:37:09 +00:00
parent 43fc30a3f7
commit 6ffcda5677
56 changed files with 2866 additions and 2462 deletions
+1
View File
@@ -96,6 +96,7 @@ class IrAttachment(models.Model):
This function is meant to be overriden to add formats.
:returns: A list of dictionary.
* format: Optional but helps debugging.
There are other methods that require the attachment
to be an XML other than the standard one.
+2 -5
View File
@@ -7,9 +7,6 @@
'version': '0.3',
'depends': [
'l10n_it',
# Although account_edi is a dependency of account_edi_proxy_client,
# it is here because it's in the auto-install
'account_edi',
'account_edi_proxy_client',
],
'auto_install': ['l10n_it'],
@@ -20,13 +17,13 @@ E-invoice implementation
'website': 'http://www.odoo.com/',
'data': [
'security/ir.model.access.csv',
'data/account_edi_data.xml',
'data/invoice_it_template.xml',
'data/invoice_it_simplified_template.xml',
'data/ir_cron.xml',
'views/res_config_settings_views.xml',
'views/l10n_it_view.xml',
],
'wizard/account_move_send_views.xml',
],
'demo': [
'data/account_invoice_demo.xml',
],
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="edi_fatturaPA" model="account.edi.format">
<field name="name">Fattura PA (IT)</field>
<field name="code">fattura_pa</field>
</record>
</data>
</odoo>
@@ -19,7 +19,7 @@
t-out="format_alphanumeric(line.product_uom_id.name, 10)"/>
<PrezzoUnitario t-out="'%.06f' % (line_dict['unit_price'])"/>
<ScontoMaggiorazione t-if="line.discount != 0">
<Tipo t-out="discount_type(line.discount)"/>
<Tipo t-out="format_alphanumeric(line_dict['discount_type'])"/>
<Percentuale t-out="format_numbers(abs(line.discount))"/>
</ScontoMaggiorazione>
<PrezzoTotale t-out="format_monetary(line_dict['subtotal_price'], currency)"/>
+4 -4
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_cron_receive_fattura_pa_invoice" model="ir.cron">
<field name="name">FatturaPA: Receive invoices from the exchange system</field>
<record id="ir_cron_l10n_it_edi_download_and_update" model="ir.cron">
<field name="name">IT EDI: Receive invoices from the SdI</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="model_id" ref="account_edi.model_account_edi_format"/>
<field name="code">model._cron_receive_fattura_pa()</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="code">model.cron_l10n_it_edi_download_and_update()</field>
<field name="doall" eval="False"/>
<field name="state">code</field>
</record>
File diff suppressed because it is too large Load Diff
+341 -166
View File
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.4alpha1+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-31 09:04+0000\n"
"PO-Revision-Date: 2023-05-31 09:04+0000\n"
"POT-Creation-Date: 2023-06-09 15:24+0000\n"
"PO-Revision-Date: 2023-06-09 15:24+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -17,81 +17,96 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s has an amount of 0.0, you must indicate the kind of exoneration."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a VAT number"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a city."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a codice fiscale number"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a country"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a country."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a post code of length 5."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a post code."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a street."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_invoice.py:0
#: code:addons/l10n_it_edi/models/account_tax.py:0
#, python-format
msgid ""
"'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'"
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_form_l10n_it
msgid ""
"<i class=\"fa fa-warning\" role=\"img\" title=\"EDI (Italy)\"/><span "
"class=\"mx-1\">E-invoicing (Italy)</span>"
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_move_send_inherit_l10n_it_edi
msgid ""
"<span attrs=\"{'invisible': [('l10n_it_edi_warning_message', '=', False)]}\">\n"
" <b>Tax Agency (Italy)</b>\n"
" </span>"
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_config_settings_view_form
msgid ""
@@ -111,16 +126,6 @@ msgstr ""
msgid "A Demo service is in use."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#, python-format
msgid ""
"A simplified invoice was created instead of an ordinary one. This is because"
" the invoice is a domestic invoice with a total amount of "
"less than or equal to 400€ and the customer's address is incomplete."
msgstr ""
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_account_chart_template
msgid "Account Chart Template"
@@ -151,7 +156,16 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_proxy_user.py:0
#: code:addons/l10n_it_edi/models/account_edi_proxy_user.py:0
#, python-format
msgid ""
"An error occurred while downloading updates from the Proxy Server: (%s) %s"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_proxy_user.py:0
#, python-format
msgid "Attached file is empty"
msgstr ""
@@ -163,19 +177,24 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Attachment from XML"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Bank account not found, useful informations from XML file:"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__being_sent
msgid "Being Sent To SdI"
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_config_settings_view_form
msgid "By checking this box, I accept that Odoo may process my invoices."
@@ -183,13 +202,18 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"Cannot apply Reverse Charge to a bill which contains both services and "
"goods."
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_form_l10n_it
msgid "Check Sending"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_company__l10n_it_codice_fiscale
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_partner__l10n_it_codice_fiscale
@@ -232,13 +256,6 @@ msgstr ""
msgid "Created by"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_invoice.py:0
#, python-format
msgid "Created from attachment in %s"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_l10n_it_ddt__create_date
msgid "Created on"
@@ -270,7 +287,7 @@ msgid "Dati Bollo"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_config_settings__l10n_it_edi_sdicoop_demo_mode__demo
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_config_settings__l10n_it_edi_demo_mode__demo
msgid "Demo"
msgstr ""
@@ -292,16 +309,9 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Document date invalid in XML file:"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_invoice.py:0
#, python-format
msgid "E-Invoice is generated on %s by %s"
msgid "Document date invalid in XML file: %s"
msgstr ""
#. module: l10n_it_edi
@@ -309,11 +319,6 @@ msgstr ""
msgid "EAN"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_account_edi_format
msgid "EDI format"
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_company_form_l10n_it
msgid "Economic and Administrative Index"
@@ -324,11 +329,6 @@ msgstr ""
msgid "Electronic Document Invoicing"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_account_edi_document
msgid "Electronic Document for an account.move"
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_form_l10n_it
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_company_form_l10n_it
@@ -336,10 +336,21 @@ msgid "Electronic Invoicing"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_bank_statement_line__l10n_it_einvoice_id
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_einvoice_id
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_einvoice_id
msgid "Electronic invoice"
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"Error uploading the e-invoice file %s.\n"
"%s"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#: code:addons/l10n_it_edi/wizard/account_move_send.py:0
#, python-format
msgid "Errors occured while creating the e-invoice file."
msgstr ""
#. module: l10n_it_edi
@@ -376,11 +387,6 @@ msgstr ""
msgid "FatturaPA Transaction"
msgstr ""
#. module: l10n_it_edi
#: model:ir.actions.server,name:l10n_it_edi.ir_cron_receive_fattura_pa_invoice_ir_actions_server
msgid "FatturaPA: Receive invoices from the exchange system"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_res_company__l10n_it_codice_fiscale
msgid "Fiscal code of your company"
@@ -401,6 +407,18 @@ msgstr ""
msgid "INTERNAL"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "IT EDI e-move: %s"
msgstr ""
#. module: l10n_it_edi
#: model:ir.actions.server,name:l10n_it_edi.ir_cron_l10n_it_edi_download_and_update_ir_actions_server
msgid "IT EDI: Receive invoices from the SdI"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/res_company.py:0
@@ -412,7 +430,7 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_invoice.py:0
#: code:addons/l10n_it_edi/models/account_tax.py:0
#, python-format
msgid ""
"If the tax has exoneration, you must enter a kind of exoneration, a law "
@@ -430,7 +448,7 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "In line %s, you must select one and only one tax."
msgstr ""
@@ -449,15 +467,6 @@ msgstr ""
msgid "Invoice Reference"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#, python-format
msgid ""
"Invoices for PA are not managed by Odoo, you can download the document and "
"send it on your own."
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_config_settings__is_edi_proxy_active
msgid "Is Edi Proxy Active"
@@ -469,16 +478,39 @@ msgid "Italian EDI"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_invoice.py:0
#, python-format
msgid "Italian invoice: %s"
#: model:ir.model,name:l10n_it_edi.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_account_move
msgid "Journal Entry"
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_bank_statement_line__l10n_it_edi_attachment_file
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_edi_attachment_file
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_edi_attachment_file
msgid "L10N It Edi Attachment File"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_config_settings__l10n_it_edi_demo_mode
msgid "L10N It Edi Demo Mode"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move_send__l10n_it_edi_enable_send
msgid "L10N It Edi Enable Send"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_bank_statement_line__l10n_it_edi_header
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_edi_header
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_edi_header
msgid "L10N It Edi Header"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_bank_statement_line__l10n_it_edi_is_self_invoice
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_edi_is_self_invoice
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_edi_is_self_invoice
msgid "L10N It Edi Is Self Invoice"
msgstr ""
#. module: l10n_it_edi
@@ -487,20 +519,30 @@ msgid "L10N It Edi Proxy Current State"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_config_settings__l10n_it_edi_sdicoop_demo_mode
msgid "L10N It Edi Sdicoop Demo Mode"
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_company__l10n_it_edi_proxy_user_id
msgid "L10N It Edi Proxy User"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_config_settings__l10n_it_edi_sdicoop_register
msgid "L10N It Edi Sdicoop Register"
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move_send__l10n_it_edi_readonly
msgid "L10N It Edi Readonly"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_bank_statement_line__l10n_it_einvoice_name
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_einvoice_name
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_einvoice_name
msgid "L10N It Einvoice Name"
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_config_settings__l10n_it_edi_register
msgid "L10N It Edi Register"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_bank_statement_line__l10n_it_edi_state
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_edi_state
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_edi_state
msgid "L10N It Edi State"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move_send__l10n_it_edi_warning_message
msgid "L10N It Edi Warning Message"
msgstr ""
#. module: l10n_it_edi
@@ -550,11 +592,10 @@ msgstr ""
#: model:ir.model.fields,help:l10n_it_edi.field_res_partner__l10n_it_pa_index
#: model:ir.model.fields,help:l10n_it_edi.field_res_users__l10n_it_pa_index
msgid ""
"Must contain the 6-character (or 7) code, present in the PA "
"Index in the information relative to the electronic invoicing service,"
" associated with the office which, within the addressee "
"administration, deals with receiving (and processing) the "
"invoice."
"Must contain the 6-character (or 7) code, present in the PA Index in the "
"information relative to the electronic invoicing service, associated with "
"the office which, within the addressee administration, deals with receiving "
"(and processing) the invoice."
msgstr ""
#. module: l10n_it_edi
@@ -573,7 +614,7 @@ msgid "Numero DDT"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_config_settings__l10n_it_edi_sdicoop_demo_mode__prod
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_config_settings__l10n_it_edi_demo_mode__prod
msgid "Official"
msgstr ""
@@ -591,9 +632,9 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Payment due date invalid in XML file:"
msgid "Payment due date invalid in XML file: %s"
msgstr ""
#. module: l10n_it_edi
@@ -603,7 +644,7 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_proxy_user.py:0
#, python-format
msgid ""
"Please fill your codice fiscale to be able to receive invoices from "
@@ -625,14 +666,81 @@ msgstr ""
msgid "Proxy type"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__requires_user_signature
msgid "Requires user signature"
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_it_FatturaPA_export
msgid "SI"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__accepted_by_pa_partner
msgid "SdI Accepted, Accepted by the PA Partner"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__forward_failed
msgid "SdI Accepted, Forward to Partner Failed"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__forwarded
msgid "SdI Accepted, Forwarded to Partner"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__forward_attempt
msgid "SdI Accepted, Forwarding to Partner"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__accepted_by_pa_partner_after_expiry
msgid "SdI Accepted, PA Partner Expired Terms"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__rejected_by_pa_partner
msgid "SdI Accepted, Rejected by the PA Partner"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__processing
msgid "SdI Processing"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_move__l10n_it_edi_state__rejected
msgid "SdI Rejected"
msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_form_l10n_it
msgid "Send Tax Integration"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_account_move_send__l10n_it_edi_checkbox_send
msgid ""
"Send the invoice to the Italian Tax Agency.\n"
"It is set as readonly if a report has already been created, to avoid inconsistencies.\n"
"To re-enable it, delete the PDF attachment."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"Sending invoices to Public Administration partners is not supported.\n"
"The IT EDI XML file is generated, please sign the document and upload it through the 'Fatture e Corrispettivi' portal of the Tax Agency."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_proxy_user.py:0
#, python-format
msgid "Service momentarily unavailable"
msgstr ""
@@ -674,6 +782,11 @@ msgstr ""
msgid "Tax"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move_send__l10n_it_edi_checkbox_send
msgid "Tax Agency (Italy)"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_company__l10n_it_tax_system
msgid "Tax System"
@@ -686,7 +799,7 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Tax not found for line with description '%s'"
msgstr ""
@@ -703,19 +816,19 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Tax representative partner %s of %s must have a tax number."
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_config_settings__l10n_it_edi_sdicoop_demo_mode__test
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_config_settings__l10n_it_edi_demo_mode__test
msgid "Test (experimental)"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The buyer, %s, or his company must have a VAT number and/or a tax code "
@@ -743,86 +856,135 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The filename is duplicated. Try again (or adjust the FatturaPA Filename "
"sequence). Original message from the SDI: %s"
"The e-invoice file %s can't be forward to %s (Public Administration) by the SdI at the moment.\n"
"It will try again for 10 days, after which it will be considered accepted, but you will still have to send it by post or e-mail."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The invoice has been correctly issued. The Public Administration recipient "
"had 15 days to either accept or refused this document, but they did not "
"reply, so from now on we consider it accepted."
"The e-invoice file %s couldn't be forwarded to %s.\n"
"Please remember to send it via post or e-mail."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The invoice has been issued, but the delivery to the Addressee has failed. "
"You will be required to send a courtesy copy of the invoice to your customer"
" through another channel, outside of the Exchange System, and promptly "
"notify him that the original is deposited in his personal area on the portal"
" \"Invoices and Fees\" of the Revenue Agency."
"The e-invoice file %s has been accepted by %s (Public Administration), a "
"payment will be issued soon"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The invoice has been issued, but the delivery to the Public Administration "
"has failed. The Exchange System will contact them to report the problem and "
"request that they provide a solution. During the following 10 days, the "
"Exchange System will try to forward the FatturaPA file to the Public "
"Administration in question again. Should this also fail, the System will "
"notify Odoo of the failed delivery, and you will be required to send the "
"invoice to the Administration through another channel, outside of the "
"Exchange System."
"The e-invoice file %s has been accepted by the SdI.\n"
"The SdI is trying to forward it to %s.\n"
"It will try for up to 2 days, after which you'll eventually need to send it the invoice to the partner by post or e-mail."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#, python-format
msgid "The invoice has been refused by the Exchange System"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The invoice has been succesfully transmitted. The addressee has 15 days to "
"accept or reject it."
"The e-invoice file %s has been refused by %s (Public Administration).\n"
"You have 5 days from now to issue a full refund for this invoice, then contact the PA partner to create a new one according to their requests and submit it."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "The invoice was refused by the addressee."
msgid "The e-invoice file %s has been sent in Demo EDI mode."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "The e-invoice file %s has not been found on the EDI Proxy server."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The invoice was sent to FatturaPA, but we are still awaiting a response. "
"Click the link above to check for an update."
"The e-invoice file %s is duplicated.\n"
"Original message from the SdI: %s"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The e-invoice file %s is succesfully sent to the SdI. The invoice is now considered fiscally relevant.\n"
"The %s (Public Administration) had 15 days to either accept or refused this document,but since they did not reply, it's now considered accepted."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The e-invoice file %s was accepted and succesfully forwarded it to %s by the"
" SdI."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "The e-invoice file %s was sent to the SdI for processing."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The e-invoice file %s was sent to the SdI for validation.\n"
"It is not yet considered accepted, please wait further notifications."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The e-invoice file %s was succesfully sent to the SdI.\n"
"%s has 15 days to accept or reject it."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The e-invoice filename %s is duplicated. Please check the FatturaPA Filename sequence.\n"
"Original message from the SdI: %s"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "The e-invoice has been refused by the SdI."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The maximum length for VAT number is 30. %s have a VAT number too long: %s."
@@ -830,7 +992,7 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "The seller's company must have a tax system."
msgstr ""
@@ -875,16 +1037,14 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"This invoice number had already been submitted to the SdI, so it is set as Sent. Please verify that the system is correctly configured, because the correct flow does not need to send the same invoice twice for any reason.\n"
" Original message from the SDI: %s"
msgid "This move is not waiting for updates from the SdI."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Total amount from the XML File: %s"
msgstr ""
@@ -907,18 +1067,33 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Transport informations from XML file:"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_proxy_user.py:0
#, python-format
msgid "Unauthorized user"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_proxy_user.py:0
#, python-format
msgid "Unknown error"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_account_bank_statement_line__l10n_it_edi_header
#: model:ir.model.fields,help:l10n_it_edi.field_account_move__l10n_it_edi_header
#: model:ir.model.fields,help:l10n_it_edi.field_account_payment__l10n_it_edi_header
msgid ""
"User description of the current state, with hints to make the flow progress"
msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_tax__l10n_it_vat_due_date
msgid "VAT due date"
@@ -926,7 +1101,7 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"Vendor bills sent as self-invoices to the SdI require a valid PA Index "
@@ -935,26 +1110,26 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Vendor not found, useful informations from XML file:"
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "You are not allowed to check the status of this invoice."
msgid ""
"We are simulating the sending of the e-invoice file %s, as we are in demo "
"mode."
msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/wizard/account_move_send.py:0
#, python-format
msgid ""
"You must accept the terms and conditions in the settings to use FatturaPA."
"You must accept the terms and conditions in the Settings to use the IT EDI."
msgstr ""
#. module: l10n_it_edi
@@ -1229,7 +1404,7 @@ msgstr ""
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "from XML file:"
msgstr ""
msgstr ""
+2 -3
View File
@@ -5,9 +5,8 @@ from . import res_partner
from . import res_company
from . import res_config_settings
from . import account_chart_template
from . import account_invoice
from . import account_edi_document
from . import account_edi_format
from . import account_move
from . import account_tax
from . import ddt
from . import ir_attachment
from . import account_edi_proxy_user
@@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models
class AccountEdiDocument(models.Model):
_inherit = "account.edi.document"
def _filter_edi_attachments_for_mailing(self):
self.ensure_one()
if self.edi_format_id.code == 'fattura_pa':
return {}
return super()._filter_edi_attachments_for_mailing()
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import _, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class AccountEdiProxyClientUser(models.Model):
@@ -18,16 +22,9 @@ class AccountEdiProxyClientUser(models.Model):
}
return urls
def _compute_proxy_type(self):
# Extends account_edi_proxy_client
super()._compute_proxy_type()
for user in self:
if user.company_id.country_code == 'IT':
user.proxy_type = 'l10n_it_edi'
def _get_proxy_identification(self, company, proxy_type):
if proxy_type == 'l10n_it_edi':
if not company.l10n_it_codice_fiscale:
raise UserError(_('Please fill your codice fiscale to be able to receive invoices from FatturaPA'))
return self.env['res.partner']._l10n_it_edi_normalized_codice_fiscale(company.l10n_it_codice_fiscale)
return company.partner_id._l10n_it_edi_normalized_codice_fiscale()
return super()._get_proxy_identification(company, proxy_type)
@@ -1,458 +0,0 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from functools import reduce
import logging
import re
from datetime import datetime
from markupsafe import escape
from odoo import api, fields, models, _
from odoo.tools import float_repr, float_compare
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_it_edi_transaction = fields.Char(copy=False, string="FatturaPA Transaction")
l10n_it_edi_attachment_id = fields.Many2one('ir.attachment', copy=False, string="FatturaPA Attachment", ondelete="restrict")
l10n_it_stamp_duty = fields.Float(default=0, string="Dati Bollo")
l10n_it_ddt_id = fields.Many2one('l10n_it.ddt', string='DDT', copy=False)
l10n_it_einvoice_name = fields.Char(compute='_compute_l10n_it_einvoice')
l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", compute='_compute_l10n_it_einvoice')
def _get_l10n_it_amount_split_payment(self):
self.ensure_one()
amount = 0.0
if self.is_invoice(True):
for line in [line for line in self.line_ids if line.tax_line_id]:
if line.tax_line_id._l10n_it_is_split_payment() and line.credit > 0.0:
amount += line.credit
return amount
@api.depends('edi_document_ids', 'edi_document_ids.attachment_id')
def _compute_l10n_it_einvoice(self):
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
for invoice in self:
einvoice = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == fattura_pa).sudo()
invoice.l10n_it_einvoice_id = einvoice.attachment_id
invoice.l10n_it_einvoice_name = einvoice.attachment_id.name
@api.depends('l10n_it_edi_transaction')
def _compute_show_reset_to_draft_button(self):
super(AccountMove, self)._compute_show_reset_to_draft_button()
for move in self.filtered(lambda m: m.l10n_it_edi_transaction):
move.show_reset_to_draft_button = False
def invoice_generate_xml(self):
self.ensure_one()
report_name = self.env['account.edi.format']._l10n_it_edi_generate_electronic_invoice_filename(self)
data = "<?xml version='1.0' encoding='UTF-8'?>" + str(self._l10n_it_edi_export_invoice_as_xml())
description = _('Italian invoice: %s', self.move_type)
attachment = self.env['ir.attachment'].create({
'name': report_name,
'res_id': self.id,
'res_model': self._name,
'raw': data.encode(),
'description': description,
'type': 'binary',
})
self.message_post(
body=(_("E-Invoice is generated on %s by %s") % (fields.Datetime.now(), self.env.user.display_name))
)
return {'attachment': attachment}
def _is_commercial_partner_pa(self):
"""
Returns True if the destination of the FatturaPA belongs to the Public Administration.
"""
return len(self.commercial_partner_id.l10n_it_pa_index or '') == 6
def _l10n_it_edi_prepare_fatturapa_line_details(self, reverse_charge_refund=False, is_downpayment=False, convert_to_euros=True):
""" Returns a list of dictionaries passed to the template for the invoice lines (DettaglioLinee)
"""
invoice_lines = []
lines = self.invoice_line_ids.filtered(lambda l: not l.display_type in ('line_note', 'line_section'))
for num, line in enumerate(lines):
sign = -1 if line.move_id.is_inbound() else 1
price_subtotal = (line.balance * sign) if convert_to_euros else line.price_subtotal
# The price_subtotal should be inverted when the line is a reverse charge refund.
if reverse_charge_refund:
price_subtotal = -price_subtotal
# Unit price
price_unit = 0
if line.quantity and line.discount != 100.0:
price_unit = price_subtotal / ((1 - (line.discount or 0.0) / 100.0) * abs(line.quantity))
else:
price_unit = line.price_unit
description = line.name
# Down payment lines:
# If there was a down paid amount that has been deducted from this move,
# we need to put a reference to the down payment invoice in the DatiFattureCollegate tag
downpayment_moves = self.env['account.move']
if not is_downpayment and line.price_subtotal < 0:
downpayment_moves = line._get_downpayment_lines().mapped("move_id")
if downpayment_moves:
downpayment_moves_description = ', '.join([m.name for m in downpayment_moves])
sep = ', ' if description else ''
description = f"{description}{sep}{downpayment_moves_description}"
vat_tax = line.tax_ids.flatten_taxes_hierarchy().filtered(lambda t: t._l10n_it_filter_kind('vat') and t.amount >= 0)
invoice_lines.append({
'line': line,
'line_number': num + 1,
'description': description or 'NO NAME',
'unit_price': price_unit,
'subtotal_price': price_subtotal,
'vat_tax': vat_tax,
'downpayment_moves': downpayment_moves,
})
return invoice_lines
def _l10n_it_edi_prepare_fatturapa_tax_details(self, tax_details):
""" Returns a list of dictionaries passed to the template for the invoice lines (DatiRiepilogo)
"""
tax_lines = []
for _tax_name, tax_dict in tax_details['tax_details'].items():
# The assumption is that the company currency is EUR.
tax = tax_dict['tax']
base_amount = tax_dict['base_amount']
tax_amount = tax_dict['tax_amount']
tax_rate = tax.amount
tax_exigibility_code = (
'S' if tax._l10n_it_is_split_payment()
else 'D' if tax.tax_exigibility == 'on_payment'
else 'I' if tax.tax_exigibility == 'on_invoice'
else False
)
expected_base_amount = tax_amount * 100 / tax_rate if tax_rate else False
# Constraints within the edi make local rounding on price included taxes a problem.
# To solve this there is a <Arrotondamento> or 'rounding' field, such that:
# taxable base = sum(taxable base for each unit) + Arrotondamento
if tax.price_include and tax.amount_type == 'percent':
if expected_base_amount and float_compare(base_amount, expected_base_amount, 2):
tax_dict['rounding'] = base_amount - (tax_amount * 100 / tax_rate)
tax_dict['base_amount'] = base_amount - tax_dict['rounding']
tax_line_dict = {
'tax': tax,
'rounding': tax_dict.get('rounding', False),
'base_amount': tax_dict['base_amount'],
'tax_amount': tax_dict['tax_amount'],
'exigibility_code': tax_exigibility_code,
}
tax_lines.append(tax_line_dict)
return tax_lines
def _l10n_it_edi_filter_fatturapa_tax_details(self, line, tax_values):
"""Filters tax details to only include the positive amounted lines regarding VAT taxes."""
repartition_line = tax_values['tax_repartition_line']
return (repartition_line.factor_percent >= 0 and repartition_line.tax_id.amount >= 0)
def _prepare_fatturapa_export_values(self):
self.ensure_one()
def format_date(dt):
# Format the date in the italian standard.
dt = dt or datetime.now()
return dt.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
def format_monetary(number, currency):
# Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001).
return float_repr(number, min(2, currency.decimal_places))
def format_numbers(number):
#format number to str with between 2 and 8 decimals (event if it's .00)
number_splited = str(number).split('.')
if len(number_splited) == 1:
return "%.02f" % number
cents = number_splited[1]
if len(cents) > 8:
return "%.08f" % number
return float_repr(number, max(2, len(cents)))
def format_numbers_two(number):
#format number to str with 2 (event if it's .00)
return "%.02f" % number
def discount_type(discount):
return 'SC' if discount > 0 else 'MG'
def format_alphanumeric(text, maxlen=None):
if not text:
return False
text = text.encode('latin-1', 'replace').decode('latin-1')
if maxlen and maxlen > 0:
text = text[:maxlen]
elif maxlen and maxlen < 0:
text = text[maxlen:]
return text
def format_phone(number):
if not number:
return False
number = number.replace(' ', '').replace('/', '').replace('.', '')
if len(number) > 4 and len(number) < 13:
return format_alphanumeric(number)
return False
def format_address(street, street2, maxlen=60):
street, street2 = street or '', street2 or ''
if street and len(street) >= maxlen:
street2 = ''
sep = ' ' if street and street2 else ''
return format_alphanumeric(f"{street}{sep}{street2}", maxlen)
formato_trasmissione = "FPA12" if self._is_commercial_partner_pa() else "FPR12"
# Flags
is_self_invoice = self.env['account.edi.format']._l10n_it_edi_is_self_invoice(self)
document_type = self.env['account.edi.format']._l10n_it_get_document_type(self)
if self.env['account.edi.format']._l10n_it_is_simplified_document_type(document_type):
formato_trasmissione = "FSM10"
# Represent if the document is a reverse charge refund in a single variable
reverse_charge = document_type in ['TD17', 'TD18', 'TD19']
is_downpayment = document_type in ['TD02']
reverse_charge_refund = self.move_type == 'in_refund' and reverse_charge
convert_to_euros = self.currency_id.name != 'EUR'
# b64encode returns a bytestring, the template tries to turn it to string,
# but only gets the repr(pdf) --> "b'<base64_data>'"
pdf = self.env['ir.actions.report']._render_qweb_pdf("account.account_invoices", self.id)[0]
pdf = base64.b64encode(pdf).decode()
pdf_name = re.sub(r'\W+', '', self.name) + '.pdf'
tax_details = self._prepare_edi_tax_details(filter_to_apply=self._l10n_it_edi_filter_fatturapa_tax_details)
company = self.company_id
partner = self.commercial_partner_id
sender = company
buyer = partner if not is_self_invoice else company
seller = company if not is_self_invoice else partner
sender_info_values = company.partner_id._l10n_it_edi_get_values()
buyer_info_values = (partner if not is_self_invoice else company.partner_id)._l10n_it_edi_get_values()
seller_info_values = (company.partner_id if not is_self_invoice else partner)._l10n_it_edi_get_values()
representative_info_values = company.l10n_it_tax_representative_partner_id._l10n_it_edi_get_values()
# Self-invoices are technically -100%/+100% repartitioned
# but functionally need to be exported as 100%
document_total = self.amount_total
if is_self_invoice:
document_total += sum([abs(v['tax_amount_currency']) for k, v in tax_details['tax_details'].items()])
if reverse_charge_refund:
document_total = -abs(document_total)
split_payment_amount = self._get_l10n_it_amount_split_payment()
if split_payment_amount:
document_total += split_payment_amount
# Reference line for finding the conversion rate used in the document
conversion_line = self.invoice_line_ids.sorted(lambda l: abs(l.balance), reverse=True)[0] if self.invoice_line_ids else None
conversion_rate = float_repr(
abs(conversion_line.balance / conversion_line.amount_currency), precision_digits=5,
) if convert_to_euros and conversion_line else None
invoice_lines = self._l10n_it_edi_prepare_fatturapa_line_details(reverse_charge_refund, is_downpayment, convert_to_euros)
tax_lines = self._l10n_it_edi_prepare_fatturapa_tax_details(tax_details)
# Reduce downpayment views to a single recordset
downpayment_moves = [l.get('downpayment_moves', self.env['account.move']) for l in invoice_lines]
downpayment_moves = self.browse(move.id for moves in downpayment_moves for move in moves)
# Create file content.
template_values = {
'record': self,
'company': company,
'partner': partner,
'sender': sender,
'buyer': buyer,
'seller': seller,
'representative': company.l10n_it_tax_representative_partner_id,
'sender_info': sender_info_values,
'buyer_info': buyer_info_values,
'seller_info': seller_info_values,
'representative_info': representative_info_values,
'origin_document_type': False, # see module l10n_it_edi_pa, will be merged in master
'currency': self.currency_id or self.company_currency_id if not convert_to_euros else self.env.ref('base.EUR'),
'document_total': document_total,
'regime_fiscale': company.l10n_it_tax_system if not is_self_invoice else 'RF18',
'is_self_invoice': is_self_invoice,
'partner_bank': self.partner_bank_id,
'discount_type': discount_type,
'formato_trasmissione': formato_trasmissione,
'document_type': document_type,
'pdf': pdf,
'pdf_name': pdf_name,
'tax_details': tax_details,
'downpayment_moves': downpayment_moves,
'rc_refund': reverse_charge_refund,
'invoice_lines': invoice_lines,
'tax_lines': tax_lines,
'conversion_rate': conversion_rate,
'balance_multiplicator': -1 if self.is_inbound() else 1,
'abs': abs,
'format_date': format_date,
'format_monetary': format_monetary,
'format_numbers': format_numbers,
'format_numbers_two': format_numbers_two,
'format_phone': format_phone,
'format_alphanumeric': format_alphanumeric,
'format_address': format_address,
}
return template_values
def _post(self, soft=True):
# OVERRIDE
posted = super()._post(soft=soft)
return posted
def _compose_info_message(self, tree, element_tags):
output_str = ""
elements = tree.xpath(element_tags)
for element in elements:
output_str += "<ul>"
for line in element.iter():
if line.text:
text = " ".join(line.text.split())
if text:
output_str += "<li>%s: %s</li>" % (line.tag, text)
output_str += "</ul>"
return output_str
def _compose_multi_info_message(self, tree, element_tags):
output_str = "<ul>"
for element_tag in element_tags:
elements = tree.xpath(element_tag)
if not elements:
continue
for element in elements:
text = " ".join(element.text.split())
if text:
output_str += "<li>%s: %s</li>" % (element.tag, text)
return output_str + "</ul>"
# -------------------------------------------------------------------------
# Import invoice
# -------------------------------------------------------------------------
@api.model
def _import_invoice_fattura_pa(self, invoice, file_data, new=False):
tree_list = file_data['xml_tree'].xpath('//FatturaElettronicaBody')
if not tree_list:
return
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:]:
other_invoice = invoice.create({
'journal_id': invoice.journal_id.id,
})
with other_invoice._get_edi_creation() as other_invoice:
self.env['account.edi.format']._import_fattura_pa(tree, other_invoice)
other_invoice.message_post(body=escape(_("Created from attachment in %s")) % invoice._get_html_link())
return True
def _get_edi_decoder(self, file_data, new=False):
# EXTENDS 'account'
def is_fattura_pa(filename): # TODO: maybe check on etree?
return re.search('[A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.((?i:xml.p7m|xml))', filename)
if file_data['type'] in ('xml', 'xml_p7m') and is_fattura_pa(file_data['filename']):
return self._import_invoice_fattura_pa
return super()._get_edi_decoder(file_data, new=new)
class AccountTax(models.Model):
_name = "account.tax"
_inherit = "account.tax"
l10n_it_vat_due_date = fields.Selection([
("I", "[I] IVA ad esigibilità immediata"),
("D", "[D] IVA ad esigibilità differita"),
("S", "[S] Scissione dei pagamenti")], default="I", string="VAT due date")
l10n_it_has_exoneration = fields.Boolean(string="Has exoneration of tax (Italy)", help="Tax has a tax exoneration.")
l10n_it_kind_exoneration = fields.Selection(selection=[
("N1", "[N1] Escluse ex art. 15"),
("N2", "[N2] Non soggette"),
("N2.1", "[N2.1] Non soggette ad IVA ai sensi degli artt. Da 7 a 7-septies del DPR 633/72"),
("N2.2", "[N2.2] Non soggette – altri casi"),
("N3", "[N3] Non imponibili"),
("N3.1", "[N3.1] Non imponibili – esportazioni"),
("N3.2", "[N3.2] Non imponibili – cessioni intracomunitarie"),
("N3.3", "[N3.3] Non imponibili – cessioni verso San Marino"),
("N3.4", "[N3.4] Non imponibili – operazioni assimilate alle cessioni all’esportazione"),
("N3.5", "[N3.5] Non imponibili – a seguito di dichiarazioni d’intento"),
("N3.6", "[N3.6] Non imponibili – altre operazioni che non concorrono alla formazione del plafond"),
("N4", "[N4] Esenti"),
("N5", "[N5] Regime del margine / IVA non esposta in fattura"),
("N6", "[N6] Inversione contabile (per le operazioni in reverse charge ovvero nei casi di autofatturazione per acquisti extra UE di servizi ovvero per importazioni di beni nei soli casi previsti)"),
("N6.1", "[N6.1] Inversione contabile – cessione di rottami e altri materiali di recupero"),
("N6.2", "[N6.2] Inversione contabile – cessione di oro e argento puro"),
("N6.3", "[N6.3] Inversione contabile – subappalto nel settore edile"),
("N6.4", "[N6.4] Inversione contabile – cessione di fabbricati"),
("N6.5", "[N6.5] Inversione contabile – cessione di telefoni cellulari"),
("N6.6", "[N6.6] Inversione contabile – cessione di prodotti elettronici"),
("N6.7", "[N6.7] Inversione contabile – prestazioni comparto edile esettori connessi"),
("N6.8", "[N6.8] Inversione contabile – operazioni settore energetico"),
("N6.9", "[N6.9] Inversione contabile – altri casi"),
("N7", "[N7] IVA assolta in altro stato UE (prestazione di servizi di telecomunicazioni, tele-radiodiffusione ed elettronici ex art. 7-octies, comma 1 lett. a, b, art. 74-sexies DPR 633/72)")],
string="Exoneration",
help="Exoneration type",
default="N1")
l10n_it_law_reference = fields.Char(string="Law Reference", size=100)
@api.constrains('l10n_it_has_exoneration',
'l10n_it_kind_exoneration',
'l10n_it_law_reference',
'amount',
'invoice_repartition_line_ids',
'refund_repartition_line_ids')
def _check_exoneration_with_no_tax(self):
for tax in self:
if tax.l10n_it_has_exoneration:
if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0:
raise ValidationError(_("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0."))
if tax.l10n_it_kind_exoneration == 'N6' and tax._l10n_it_is_split_payment():
raise UserError(_("Split Payment is not compatible with exoneration of kind 'N6'"))
def _l10n_it_filter_kind(self, kind):
""" This can be overridden by l10n_it_edi_withholding for different kind of taxes (withholding, pension_fund)."""
return self if kind == 'vat' else self.env['account.tax']
def _l10n_it_is_split_payment(self):
""" Split payment means that the Public Administration buyer will pay VAT
to the tax agency instead of the vendor
"""
self.ensure_one()
tax_tags = self.get_tax_tags(is_refund=False, repartition_type='tax')
if not tax_tags:
return False
it_tax_report_ve38_lines = self.env['account.report.line'].search([
('report_id.country_id.code', '=', 'IT'),
('code', '=', 'VE38'),
])
if not it_tax_report_ve38_lines:
return False
ve38_lines_tags = it_tax_report_ve38_lines.expression_ids._get_matching_tags()
return bool(tax_tags & ve38_lines_tags)
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError, UserError
class AccountTax(models.Model):
_inherit = "account.tax"
l10n_it_vat_due_date = fields.Selection(
selection=[
("I", "[I] IVA ad esigibilità immediata"),
("D", "[D] IVA ad esigibilità differita"),
("S", "[S] Scissione dei pagamenti")],
default="I",
string="VAT due date",
)
l10n_it_has_exoneration = fields.Boolean(string="Has exoneration of tax (Italy)", help="Tax has a tax exoneration.")
l10n_it_kind_exoneration = fields.Selection(
selection=[
("N1", "[N1] Escluse ex art. 15"),
("N2", "[N2] Non soggette"),
("N2.1", "[N2.1] Non soggette ad IVA ai sensi degli artt. Da 7 a 7-septies del DPR 633/72"),
("N2.2", "[N2.2] Non soggette - altri casi"),
("N3", "[N3] Non imponibili"),
("N3.1", "[N3.1] Non imponibili - esportazioni"),
("N3.2", "[N3.2] Non imponibili - cessioni intracomunitarie"),
("N3.3", "[N3.3] Non imponibili - cessioni verso San Marino"),
("N3.4", "[N3.4] Non imponibili - operazioni assimilate alle cessioni all'esportazione"),
("N3.5", "[N3.5] Non imponibili - a seguito di dichiarazioni d'intento"),
("N3.6", "[N3.6] Non imponibili - altre operazioni che non concorrono alla formazione del plafond"),
("N4", "[N4] Esenti"),
("N5", "[N5] Regime del margine / IVA non esposta in fattura"),
("N6", "[N6] Inversione contabile (per le operazioni in reverse charge ovvero nei casi di autofatturazione per acquisti extra UE di servizi ovvero per importazioni di beni nei soli casi previsti)"),
("N6.1", "[N6.1] Inversione contabile - cessione di rottami e altri materiali di recupero"),
("N6.2", "[N6.2] Inversione contabile - cessione di oro e argento puro"),
("N6.3", "[N6.3] Inversione contabile - subappalto nel settore edile"),
("N6.4", "[N6.4] Inversione contabile - cessione di fabbricati"),
("N6.5", "[N6.5] Inversione contabile - cessione di telefoni cellulari"),
("N6.6", "[N6.6] Inversione contabile - cessione di prodotti elettronici"),
("N6.7", "[N6.7] Inversione contabile - prestazioni comparto edile esettori connessi"),
("N6.8", "[N6.8] Inversione contabile - operazioni settore energetico"),
("N6.9", "[N6.9] Inversione contabile - altri casi"),
("N7", "[N7] IVA assolta in altro stato UE (prestazione di servizi di telecomunicazioni, tele-radiodiffusione ed elettronici ex art. 7-octies, comma 1 lett. a, b, art. 74-sexies DPR 633/72)")
],
string="Exoneration",
help="Exoneration type",
default="N1",
)
l10n_it_law_reference = fields.Char(string="Law Reference", size=100)
@api.constrains('l10n_it_has_exoneration',
'l10n_it_kind_exoneration',
'l10n_it_law_reference',
'amount',
'invoice_repartition_line_ids',
'refund_repartition_line_ids')
def _l10n_it_edi_check_exoneration_with_no_tax(self):
for tax in self:
if tax.l10n_it_has_exoneration:
if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0:
raise ValidationError(_("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0."))
if tax.l10n_it_kind_exoneration == 'N6' and tax._l10n_it_is_split_payment():
raise UserError(_("Split Payment is not compatible with exoneration of kind 'N6'"))
def _l10n_it_filter_kind(self, kind):
""" This can be overridden by l10n_it_edi_withholding for different kind of taxes (withholding, pension_fund)."""
return self if kind == 'vat' else self.env['account.tax']
def _l10n_it_is_split_payment(self):
""" Split payment means that the Public Administration buyer will pay VAT
to the tax agency instead of the vendor
"""
self.ensure_one()
tax_tags = self.get_tax_tags(is_refund=False, repartition_type='tax')
if not tax_tags:
return False
it_tax_report_ve38_lines = self.env['account.report.line'].search([
('report_id.country_id.code', '=', 'IT'),
('code', '=', 'VE38'),
])
if not it_tax_report_ve38_lines:
return False
ve38_lines_tags = it_tax_report_ve38_lines.expression_ids._get_matching_tags()
return bool(tax_tags & ve38_lines_tags)
+36 -25
View File
@@ -1,49 +1,60 @@
# -*- coding: utf-8 -*-
from odoo import api, models
from odoo.addons.l10n_it_edi.tools.remove_signature import remove_signature
from lxml import etree
import logging
import re
_logger = logging.getLogger(__name__)
FATTURAPA_FILENAME_RE = "[A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.((?i:xml.p7m|xml))"
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
@api.model
def _decode_edi_xml_p7m(self, filename, content):
"""Decodes an xml.p7m into a list of one dictionary representing an attachment.
:returns: A list with a dictionary.
def _decode_edi_l10n_it_edi(self, name, content):
""" Decodes a into a list of one dictionary representing an attachment.
:returns: A list with a dictionary.
"""
decoded_content = remove_signature(content)
if not decoded_content:
return
if not (decoded_content := remove_signature(content)):
return []
parser = etree.XMLParser(recover=True, resolve_entities=False)
try:
parser = etree.XMLParser(recover=True)
xml_tree = etree.fromstring(decoded_content, parser)
except Exception as e:
except etree.ParseError as e:
_logger.exception("Error when converting the xml content to etree: %s", e)
return []
to_process = []
if xml_tree is not None:
to_process.append({
'filename': filename,
'content': content,
'xml_tree': xml_tree,
'type': 'xml_p7m',
'sort_weight': 11,
})
return to_process
return [{
'filename': name,
'content': content,
'xml_tree': xml_move_tree,
'type': 'l10n_it_edi',
'sort_weight': 11,
} for xml_move_tree in xml_tree.xpath('//FatturaElettronicaBody')]
def _is_l10n_it_edi_import_file(self):
is_xml = (
self.name.endswith('.xml')
or self.mimetype.endswith('/xml')
or 'text/plain' in self.mimetype
and self.raw
and self.raw.startswith(b'<?xml'))
is_p7m = self.mimetype == 'application/pkcs7-mime'
return (is_xml or is_p7m) and re.search(FATTURAPA_FILENAME_RE, self.name)
@api.model
def _get_edi_supported_formats(self):
""" XML files could be l10n_it_edi related or not, so check it
before demanding the decoding to the the standard XML methods.
"""
# EXTENDS 'account'
decoders = super()._get_edi_supported_formats()
decoders.append({
'check': lambda attachment: attachment.name.endswith('.xml.p7m'),
'decoder': self._decode_edi_xml_p7m,
})
return decoders
return [{
'format': 'l10n_it_edi',
'check': lambda a: a._is_l10n_it_edi_import_file(),
'decoder': self._decode_edi_l10n_it_edi,
}] + super()._get_edi_supported_formats()
+10 -1
View File
@@ -1,4 +1,3 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
@@ -33,6 +32,10 @@ class ResCompany(models.Model):
store=True, readonly=False, help="Fiscal code of your company")
l10n_it_tax_system = fields.Selection(selection=TAX_SYSTEM, string="Tax System",
help="Please select the Tax system to which you are subjected.")
l10n_it_edi_proxy_user_id = fields.Many2one(
comodel_name="account_edi_proxy_client.user",
compute="_compute_l10n_it_edi_proxy_user_id",
)
# Economic and Administrative Index
l10n_it_has_eco_index = fields.Boolean(default=False,
@@ -107,3 +110,9 @@ class ResCompany(models.Model):
raise ValidationError(_("Your tax representative partner must have a tax number."))
if not record.l10n_it_tax_representative_partner_id.country_id:
raise ValidationError(_("Your tax representative partner must have a country."))
@api.depends("account_edi_proxy_client_ids")
def _compute_l10n_it_edi_proxy_user_id(self):
self.ensure_one()
for company in self:
company.l10n_it_edi_proxy_user_id = company.account_edi_proxy_client_ids.filtered(lambda x: x.proxy_type == 'l10n_it_edi')
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
@@ -9,30 +8,33 @@ class ResConfigSettings(models.TransientModel):
is_edi_proxy_active = fields.Boolean(compute='_compute_is_edi_proxy_active')
l10n_it_edi_proxy_current_state = fields.Char(compute='_compute_l10n_it_edi_proxy_current_state')
l10n_it_edi_sdicoop_register = fields.Boolean(compute='_compute_l10n_it_edi_sdicoop_register', inverse='_set_l10n_it_edi_sdicoop_register_demo_mode')
l10n_it_edi_sdicoop_demo_mode = fields.Selection(
l10n_it_edi_register = fields.Boolean(compute='_compute_l10n_it_edi_register', inverse='_set_l10n_it_edi_register_demo_mode')
l10n_it_edi_demo_mode = fields.Selection(
[('demo', 'Demo'),
('test', 'Test (experimental)'),
('prod', 'Official')],
compute='_compute_l10n_it_edi_sdicoop_demo_mode',
inverse='_set_l10n_it_edi_sdicoop_register_demo_mode',
compute='_compute_l10n_it_edi_demo_mode',
inverse='_set_l10n_it_edi_register_demo_mode',
readonly=False)
def _create_proxy_user(self, company_id, edi_mode):
self.env['account_edi_proxy_client.user']._register_proxy_user(company_id, 'l10n_it_edi', edi_mode)
def button_create_proxy_user(self):
self._create_proxy_user(self.company_id, self.l10n_it_edi_demo_mode)
@api.depends('company_id.account_edi_proxy_client_ids', 'company_id.account_edi_proxy_client_ids.active')
def _compute_l10n_it_edi_sdicoop_demo_mode(self):
def _compute_l10n_it_edi_demo_mode(self):
for config in self:
edi_user = self.env['account_edi_proxy_client.user'].search([
('company_id', '=', config.company_id.id),
('proxy_type', '=', 'l10n_it_edi'),
], limit=1)
config.l10n_it_edi_sdicoop_demo_mode = edi_user.edi_mode
config.l10n_it_edi_demo_mode = edi_user.edi_mode
def _set_l10n_it_edi_sdicoop_demo_mode(self):
def _set_l10n_it_edi_demo_mode(self):
for config in self:
self.env['ir.config_parameter'].set_param('account_edi_proxy_client.demo', config.l10n_it_edi_sdicoop_demo_mode)
self.env['ir.config_parameter'].set_param('account_edi_proxy_client.demo', config.l10n_it_edi_demo_mode)
@api.depends('company_id.account_edi_proxy_client_ids', 'company_id.account_edi_proxy_client_ids.active')
def _compute_is_edi_proxy_active(self):
@@ -50,19 +52,11 @@ class ResConfigSettings(models.TransientModel):
config.l10n_it_edi_proxy_current_state = 'inactive' if not proxy_user else 'demo' if proxy_user.id_client[:4] == 'demo' else 'active'
@api.depends('company_id')
def _compute_l10n_it_edi_sdicoop_register(self):
def _compute_l10n_it_edi_register(self):
"""Needed because it expects a compute"""
self.l10n_it_edi_sdicoop_register = False
self.l10n_it_edi_register = False
def button_create_proxy_user(self):
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
edi_identification = fattura_pa._get_proxy_identification(self.company_id)
if not edi_identification:
return
self.env['account_edi_proxy_client.user']._register_proxy_user(self.company_id, 'l10n_it_edi', self.l10n_it_edi_sdicoop_demo_mode, edi_identification)
def _set_l10n_it_edi_sdicoop_register_demo_mode(self):
def _set_l10n_it_edi_register_demo_mode(self):
for config in self:
proxy_user = self.env['account_edi_proxy_client.user'].search([
@@ -76,14 +70,14 @@ class ResConfigSettings(models.TransientModel):
# Update the config as per the selected radio button
previous_demo_state = proxy_user.edi_mode
edi_mode = config.l10n_it_edi_sdicoop_demo_mode
edi_mode = config.l10n_it_edi_demo_mode
self.env['ir.config_parameter'].set_param('account_edi_proxy_client.demo', edi_mode)
# If the user is trying to change from a state in which they have a registered official or testing proxy client
# to another state, we should stop them
if real_proxy_users and previous_demo_state != edi_mode:
raise UserError(_("The company has already registered with the service as 'Test' or 'Official', it cannot change."))
if config.l10n_it_edi_sdicoop_register:
if config.l10n_it_edi_register:
# There should only be one user at a time, if there are no users, register one
if not proxy_user:
self._create_proxy_user(config.company_id, edi_mode)
+15 -8
View File
@@ -1,12 +1,12 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from stdnum.it import codicefiscale, iva
from odoo import api, fields, models, _
from odoo.exceptions import UserError
import re
class ResPartner(models.Model):
_name = 'res.partner'
@@ -14,12 +14,14 @@ 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="Destination Code",
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,\
associated with the office which, within the addressee administration, deals\
with receiving (and processing) the invoice.")
help="Must contain the 6-character (or 7) code, present in the PA Index "
"in the information relative to the electronic invoicing service, "
"associated with the office which, within the addressee administration, deals "
"with receiving (and processing) the invoice.",
)
_sql_constraints = [
('l10n_it_codice_fiscale',
@@ -31,6 +33,11 @@ class ResPartner(models.Model):
"Destination Code must have between 6 and 7 characters."),
]
def _l10n_it_edi_is_public_administration(self):
""" Returns True if the destination of the FatturaPA belongs to the Public Administration. """
self.ensure_one()
return len(self.l10n_it_pa_index or '') == 6
def _l10n_it_edi_get_values(self):
""" Generates all partner values needed by l10n_it_edi XML export.
@@ -122,7 +129,7 @@ class ResPartner(models.Model):
If the Tax Code is equal to the Italian VAT, it may mistakenly have the country prefix,
so we try and remove it if we can
"""
if not l10n_it_codice_fiscale:
if l10n_it_codice_fiscale is None:
self.ensure_one()
l10n_it_codice_fiscale = self.l10n_it_codice_fiscale
if l10n_it_codice_fiscale and re.match(r'^IT[0-9]{11}$', l10n_it_codice_fiscale):
+8 -19
View File
@@ -5,14 +5,14 @@ from lxml import etree
from odoo import tools
from odoo.tests import tagged
from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestItEdi(AccountEdiTestCommon):
class TestItEdi(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref='it', edi_format_ref="l10n_it_edi.edi_fatturaPA"):
super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
def setUpClass(cls, chart_template_ref='it'):
super().setUpClass(chart_template_ref=chart_template_ref)
# Company data ------
cls.company = cls.company_data_2['company']
@@ -104,24 +104,14 @@ class TestItEdi(AccountEdiTestCommon):
path = f'{self.module}/tests/export_xmls/{filename}'
with tools.file_open(path, mode='rb') as fd:
expected_tree = etree.fromstring(fd.read())
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(invoice))
xml = invoice._l10n_it_edi_render_xml()
invoice_etree = etree.fromstring(xml)
try:
self.assertXmlTreeEqual(invoice_etree, expected_tree)
except AssertionError as ae:
ae.args = (ae.args[0] + f"\nFile used for comparison: {filename}", )
raise
def _cleanup_etree(self, content, xpaths=None):
xpaths = {
**(xpaths or {}),
'//FatturaElettronicaBody/Allegati': 'Allegati',
'//DatiTrasmissione/ProgressivoInvio': 'ProgressivoInvio',
}
return self.with_applied_xpath(
etree.fromstring(content),
"".join([f"<xpath expr='{x}' position='replace'>{y}</xpath>" for x, y in xpaths.items()])
)
def _assert_import_invoice(self, filename, expected_values_list, xml_to_apply=None):
path = f'{self.module}/tests/import_xmls/{filename}'
with tools.file_open(path, mode='rb') as fd:
@@ -138,9 +128,8 @@ class TestItEdi(AccountEdiTestCommon):
'name': filename,
'raw': import_content,
})
invoices = self.company_data_2['default_journal_purchase']\
.with_context(default_move_type='in_invoice')\
._create_document_from_attachment(attachment.ids)
purchase_journal = self.company_data_2['default_journal_purchase'].with_context(default_move_type='in_invoice')
invoices = purchase_journal._create_document_from_attachment(attachment.ids)
expected_invoice_values_list = []
expected_invoice_line_ids_values_list = []
@@ -106,10 +106,5 @@
<ImportoPagamento>1600.80</ImportoPagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>BILL2022030001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -102,10 +102,5 @@
<ImportoPagamento>1600.80</ImportoPagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>BILL2022030001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -102,10 +102,5 @@
<ImportoPagamento>1600.80</ImportoPagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>BILL2022030001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -107,10 +107,5 @@
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
<Allegati>
<NomeAttachment>RINV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -106,10 +106,5 @@
<ImportoPagamento>1600.80</ImportoPagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>RBILL2022030001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -63,10 +63,5 @@
<Imposta>4.40</Imposta>
</DatiIVA>
</DatiBeniServizi>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronicaSemplificata>
@@ -94,10 +94,5 @@
<CodicePagamento>INV/2022/00001</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -112,10 +112,5 @@
<CodicePagamento>INV/2022/00001</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -76,7 +76,7 @@
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>?-</Descrizione>
<Descrizione>--</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
@@ -106,10 +106,5 @@
<CodicePagamento>INV/2022/00001</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -114,10 +114,5 @@
<CodicePagamento>INV/2022/00001</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -113,10 +113,5 @@
<CodicePagamento>INV/2022/00001</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -101,10 +101,5 @@
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -53,10 +53,5 @@
<Imposta>72.13</Imposta>
</DatiIVA>
</DatiBeniServizi>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronicaSemplificata>
@@ -102,10 +102,5 @@
<CodicePagamento>INV/2022/00001</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>INV202200001.pdf</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
+7 -10
View File
@@ -1,12 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
from odoo import Command
from odoo.tests import tagged
from odoo.addons.l10n_it_edi.tests.common import TestItEdi
from odoo.exceptions import UserError
@tagged('post_install_l10n', 'post_install', '-at_install')
@@ -145,7 +141,7 @@ class TestItEdiExport(TestItEdi):
'tax_ids': [Command.set(self.default_tax.ids)],
}),
Command.create({
'name': '–-',
'name': '--',
'price_unit': 800.40,
'tax_ids': [Command.set(self.default_tax.ids)],
}),
@@ -214,8 +210,9 @@ class TestItEdiExport(TestItEdi):
],
})
with self.assertRaises(UserError):
invoice.action_post()
expected = ['Alessi must have a street.', 'Alessi must have a country.', 'Alessi must have a post code.', 'Alessi must have a city.']
actual = invoice._l10n_it_edi_export_data_check()
self.assertEqual(expected, actual)
def test_invoice_non_domestic_simplified(self):
invoice = self.env['account.move'].with_company(self.company).create({
@@ -231,9 +228,9 @@ class TestItEdiExport(TestItEdi):
}),
],
})
with self.assertRaises(UserError):
invoice.action_post()
expected = ['Alessi must have a street.', 'Alessi must have a post code.', 'Alessi must have a city.']
actual = invoice._l10n_it_edi_export_data_check()
self.assertEqual(expected, actual)
def test_invoice_zero_percent_taxes(self):
tax_zero_percent_hundred_percent_repartition = self.env['account.tax'].with_company(self.company).create({
+29 -16
View File
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import MagicMock, patch
import uuid
from unittest.mock import patch
from odoo import fields, sql_db
from odoo import fields, sql_db, tools
from odoo.tests import tagged
from odoo.addons.l10n_it_edi.tests.common import TestItEdi
@@ -73,28 +74,40 @@ class TestItEdiImport(TestItEdi):
def test_receive_same_vendor_bill_twice(self):
""" Test that the second time we are receiving an SdiCoop invoice, the second is discarded """
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
content = self.fake_test_content.encode()
# Our test content is not encrypted
proxy_user = MagicMock()
proxy_user.company_id = self.company
proxy_user._decrypt_data.return_value = content
ProxyUser = self.env['account_edi_proxy_client.user']
proxy_user = ProxyUser.create({
'company_id': self.company.id,
'proxy_type': 'l10n_it_edi',
'id_client': str(uuid.uuid4()),
'edi_identification': ProxyUser._get_proxy_identification(self.company, 'l10n_it_edi'),
'private_key': str(uuid.uuid4()),
})
filename = 'IT01234567890_FPR02.xml'
def mock_commit(self):
pass
with patch.object(sql_db.Cursor, "commit", mock_commit):
with (patch.object(proxy_user.__class__, '_decrypt_data', return_value=self.fake_test_content),
patch.object(sql_db.Cursor, "commit", mock_commit),
tools.mute_logger("odoo.addons.l10n_it_edi.models.account_move")):
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.env['account.move']._l10n_it_edi_process_downloads({
'999999999': {
'filename': filename,
'file': self.fake_test_content,
'key': str(uuid.uuid4()),
}},
proxy_user,
)
# There should be one attachement with this filename
attachments = self.env['ir.attachment'].search([('name', '=', 'IT01234567890_FPR02.xml')])
attachments = self.env['ir.attachment'].search([
('name', '=', 'IT01234567890_FPR02.xml'),
('res_model', '=', 'account.move'),
('res_field', '=', 'l10n_it_edi_attachment_file'),
])
self.assertEqual(len(attachments), 1)
invoices = self.env['account.move'].search([('payment_reference', '=', 'TWICE_TEST')])
self.assertEqual(len(invoices), 1)
+1 -1
View File
@@ -39,7 +39,7 @@ def remove_signature(content):
try:
loaded_data = ssl_crypto.load_pkcs7_data(ssl_crypto.FILETYPE_ASN1, content)
except ssl_crypto.Error:
_logger.warning("Error reading the content, PKCS#7 signature missing or invalid. Content will be tentatively used as it is.")
_logger.debug("PKCS#7 signature missing or invalid. Content will be tentatively used as plain text.")
return content
# Verify the signature
+51 -53
View File
@@ -91,6 +91,7 @@
<field name="state" position="before">
<field name="l10n_it_edi_transaction" optional="hide"/>
<field name="l10n_it_edi_attachment_id" optional="hide"/>
<field name="l10n_it_edi_state" optional="hide"/>
</field>
</field>
</record>
@@ -98,48 +99,12 @@
<record id="view_account_invoice_filter" model="ir.ui.view">
<field name="name">account.invoice.select.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_edi.view_account_invoice_filter"/>
<field name="inherit_id" ref="account.view_account_invoice_filter"/>
<field name="arch" type="xml">
<xpath expr="//search/field[@name='journal_id']" position="after">
<field name="l10n_it_edi_transaction" groups="base.group_no_one"/>
<field name="l10n_it_edi_attachment_id" groups="base.group_no_one"/>
</xpath>
</field>
</record>
<record id="view_in_bill_tree_inherit" model="ir.ui.view">
<field name="name">account.move.tree.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_in_invoice_bill_tree" />
<field name="arch" type="xml">
<field name="state" position="before">
<field name="edi_state" optional="hide"/>
<field name="edi_blocking_level" optional="hide"/>
<field name="edi_error_message" optional="hide"/>
</field>
</field>
</record>
<record id="view_out_invoice_tree_inherit" model="ir.ui.view">
<field name="name">account.move.tree.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_edi.view_out_invoice_tree_inherit" />
<field name="arch" type="xml">
<field name="state" position="before">
<field name="l10n_it_edi_transaction" optional="hide" invisible="1"/>
<field name="l10n_it_edi_attachment_id" optional="hide" invisible="1"/>
</field>
</field>
</record>
<record id="view_account_invoice_filter" model="ir.ui.view">
<field name="name">account.invoice.select.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_edi.view_account_invoice_filter"/>
<field name="arch" type="xml">
<xpath expr="//search/field[@name='journal_id']" position="after">
<field name="l10n_it_edi_transaction" groups="base.group_no_one" invisible="1"/>
<field name="l10n_it_edi_attachment_id" groups="base.group_no_one" invisible="1"/>
<field name="l10n_it_edi_state" groups="base.group_no_one"/>
</xpath>
</field>
</record>
@@ -150,23 +115,56 @@
<field name="priority">20</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//page[@name='other_info']" position="after">
<page string="Electronic Invoicing"
name="electronic_invoicing"
invisible="move_type not in ('out_invoice', 'out_refund', 'in_invoice', 'in_refund') or country_code != 'IT'">
<group>
<data>
<xpath expr="//header" position="inside">
<button name="action_l10n_it_edi_send"
type="object"
string="Send Tax Integration"
invisible="state != 'posted' or not l10n_it_edi_is_self_invoice or is_move_sent or country_code != 'IT'"
data-hotkey="y"/>
</xpath>
<xpath expr="//sheet" position="before">
<field name="l10n_it_edi_is_self_invoice" invisible="1"/>
<field name="l10n_it_edi_attachment_id" invisible="1"/>
<div class="alert alert-warning" role="alert"
invisible="not l10n_it_edi_header
or state == 'draft'
or l10n_it_edi_state in (False, 'forwarded', 'accepted_by_pa_partner', 'accepted_by_pa_partner_after_expiry', 'forward_failed')">
<div class="p-0 m-0"><i class='fa fa-warning' role="img" title="EDI (Italy)"/><span class="mx-1">E-invoicing (Italy)</span></div>
<field name="l10n_it_edi_header"/>
</div>
</xpath>
<xpath expr="//button[@name='action_invoice_sent']" position="before">
<button
name="action_check_l10n_it_edi"
type="object"
string="Check Sending"
class="oe_highlight"
data-hotkey="c"
invisible="l10n_it_edi_state not in ('processing', 'forward_attempt')"
/>
</xpath>
<xpath expr="//page[@name='other_info']" position="after">
<page string="Electronic Invoicing"
name="electronic_invoicing"
invisible="move_type not in ('out_invoice', 'out_refund', 'in_invoice', 'in_refund') or country_code != 'IT'">
<group>
<field name="l10n_it_edi_transaction" groups="base.group_no_one" readonly="1"/>
<field name="l10n_it_stamp_duty" readonly="state != 'draft'"/>
<field name="l10n_it_ddt_id"
invisible="move_type not in ('out_invoice', 'out_refund')"
readonly="state != 'draft'"/>
<group>
<field name="l10n_it_edi_transaction" groups="base.group_no_one" readonly="1"/>
<field name="l10n_it_edi_attachment_id" groups="base.group_no_one" readonly="1"/>
<field name="l10n_it_stamp_duty" readonly="state != 'draft'"/>
<field name="l10n_it_ddt_id" readonly="state != 'draft'" invisible="move_type not in ('out_invoice', 'out_refund')"/>
</group>
</group>
</group>
</page>
</xpath>
</data>
</page>
</xpath>
<xpath expr="//div[@name='journal_div']" position="after">
<label for="l10n_it_edi_state" invisible="not l10n_it_edi_state"/>
<div name="l10n_it_edi_div" class="d-flex" invisible="not l10n_it_edi_state">
<field name="l10n_it_edi_state" class="oe_inline"/>
</div>
</xpath>
</data>
</field>
</record>
@@ -21,24 +21,24 @@
Saving this change will direct all companies on this database to this use this configuration.
Once registered for testing or official, the mode cannot be changed.
</div>
<field name="l10n_it_edi_sdicoop_demo_mode"
<field name="l10n_it_edi_demo_mode"
widget="radio"
options="{'horizontal': true}"/>
</div>
<div class="mt8 content-group" invisible="l10n_it_edi_proxy_current_state == 'active' or l10n_it_edi_proxy_current_state == 'demo' and l10n_it_edi_sdicoop_demo_mode == 'demo'">
<div class="mt8 content-group" invisible="l10n_it_edi_proxy_current_state == 'active' or l10n_it_edi_proxy_current_state == 'demo' and l10n_it_edi_demo_mode == 'demo'">
<span class="o_form_label">Allow Odoo to process invoices</span>
<div class="text-muted">
By checking this box, I accept that Odoo may process my invoices.
</div>
<div class="content-group">
<field name="l10n_it_edi_sdicoop_register"/>
<field name="l10n_it_edi_register"/>
</div>
</div>
<div class="text-success mt8" invisible="l10n_it_edi_proxy_current_state in ['inactive', 'demo']">
An Official or Test service has been registered.
</div>
<div class="text-success mt8" invisible="l10n_it_edi_proxy_current_state != 'demo' or l10n_it_edi_sdicoop_demo_mode != 'demo'">
<div class="text-success mt8" invisible="l10n_it_edi_proxy_current_state != 'demo' or l10n_it_edi_demo_mode != 'demo'">
A Demo service is in use.
</div>
</setting>
+2
View File
@@ -1,2 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move_send
+96 -6
View File
@@ -1,12 +1,102 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import _, api, fields, models
class AccountMoveSend(models.Model):
_inherit = 'account.move.send'
def _get_mail_attachment_from_doc(self, doc):
if doc.edi_format_id.code == 'fattura_pa':
return self.env['ir.attachment']
return super()._get_mail_attachment_from_doc(doc)
l10n_it_edi_enable_send = fields.Boolean(compute='_compute_send_mail_extra_fields')
l10n_it_edi_checkbox_send = fields.Boolean('Tax Agency (Italy)', compute='_compute_l10n_it_edi_checkbox_send',
store=True, readonly=False, help=(
"Send the invoice to the Italian Tax Agency.\n"
"It is set as readonly if a report has already been created, to avoid inconsistencies.\n"
"To re-enable it, delete the PDF attachment."))
l10n_it_edi_readonly = fields.Boolean(compute='_compute_send_mail_extra_fields')
l10n_it_edi_warning_message = fields.Html(compute='_compute_send_mail_extra_fields')
def _get_available_field_values_in_multi(self, move):
# EXTENDS 'account'
values = super()._get_available_field_values_in_multi(move)
values['l10n_it_edi_checkbox_send'] = self.l10n_it_edi_checkbox_send and self._get_default_l10n_it_edi_enable_send(move)
return values
# -------------------------------------------------------------------------
# COMPUTE/CONSTRAINS METHODS
# -------------------------------------------------------------------------
@api.depends('l10n_it_edi_enable_send')
def _compute_send_mail_extra_fields(self):
# EXTENDS account
super()._compute_send_mail_extra_fields()
for wizard in self:
wizard.l10n_it_edi_enable_send = any(
wizard._get_default_l10n_it_edi_enable_send(m)
and not m.l10n_it_edi_attachment_id
for m in wizard.move_ids)
if not wizard.company_id.l10n_it_edi_proxy_user_id:
wizard.l10n_it_edi_warning_message = _("You must accept the terms and conditions in the Settings to use the IT EDI.")
else:
wizard.l10n_it_edi_warning_message = wizard.move_ids._l10n_it_edi_format_export_data_errors()
already_has_pdf = any(wizard.move_ids.mapped("invoice_pdf_report_id"))
already_has_xml = any(x._is_l10n_it_edi_import_file() for x in wizard.move_ids.mapped("attachment_ids"))
wizard.l10n_it_edi_readonly = wizard.l10n_it_edi_warning_message or already_has_pdf or already_has_xml
@api.depends('l10n_it_edi_readonly', 'l10n_it_edi_enable_send')
def _compute_l10n_it_edi_checkbox_send(self):
for wizard in self:
wizard.l10n_it_edi_checkbox_send = wizard.l10n_it_edi_enable_send and not wizard.l10n_it_edi_readonly
@api.depends('move_ids')
def _get_default_l10n_it_edi_enable_send(self, move):
return (
move.company_id.account_fiscal_country_id.code == 'IT'
and move.journal_id.type == 'sale'
and move.l10n_it_edi_state in (False, 'rejected')
)
# -------------------------------------------------------------------------
# BUSINESS ACTIONS
# -------------------------------------------------------------------------
@api.model
def _get_invoice_extra_attachments(self, move):
# EXTENDS 'account'
return super()._get_invoice_extra_attachments(move) + move.l10n_it_edi_attachment_id
def _hook_invoice_document_before_pdf_report_render(self, invoice, invoice_data):
# EXTENDS 'account'
super()._hook_invoice_document_before_pdf_report_render(invoice, invoice_data)
if self.l10n_it_edi_checkbox_send and self._get_default_l10n_it_edi_enable_send(invoice):
if errors := invoice._l10n_it_edi_export_data_check():
message = _("Errors occured while creating the e-invoice file.")
message += "\n- " + "\n- ".join(errors)
invoice_data['error'] = message
def _hook_invoice_document_after_pdf_report_render(self, invoice, invoice_data):
# EXTENDS 'account'
super()._hook_invoice_document_after_pdf_report_render(invoice, invoice_data)
if self.l10n_it_edi_checkbox_send and self._get_default_l10n_it_edi_enable_send(invoice):
invoice_data['l10n_it_edi_values'] = invoice._l10n_it_edi_get_attachment_values(
pdf_values=invoice_data['pdf_attachment_values'])
def _call_web_service_after_invoice_pdf_render(self, invoices_data):
# EXTENDS 'account'
super()._call_web_service_after_invoice_pdf_render(invoices_data)
if self.l10n_it_edi_checkbox_send:
attachments_vals = {}
moves = self.env['account.move']
for move in invoices_data:
if self._get_default_l10n_it_edi_enable_send(move):
moves |= move
attachments_vals[move] = invoices_data[move]['l10n_it_edi_values']
moves._l10n_it_edi_send(attachments_vals)
def _link_invoice_documents(self, invoice, invoice_data):
# EXTENDS 'account'
super()._link_invoice_documents(invoice, invoice_data)
if attachment_vals := invoice_data.get('l10n_it_edi_values'):
self.env['ir.attachment'].sudo().create(attachment_vals)
invoice.invalidate_recordset(fnames=['l10n_it_edi_attachment_id', 'l10n_it_edi_attachment_file'])
@@ -0,0 +1,27 @@
<odoo>
<data>
<record model="ir.ui.view" id="account_move_send_inherit_l10n_it_edi">
<field name="name">account.move.send.form.inherit.l10n_it_edi</field>
<field name="model">account.move.send</field>
<field name="inherit_id" ref="account.account_move_send_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='warnings']" position="inside">
<field name="l10n_it_edi_readonly" invisible="1"/>
<field name="l10n_it_edi_enable_send" invisible="1"/>
<span invisible="not l10n_it_edi_warning_message">
<b>Tax Agency (Italy)</b>
</span>
<div class="alert alert-warning mb-8" role="alert" invisible="not l10n_it_edi_warning_message">
<field name="l10n_it_edi_warning_message"/>
</div>
</xpath>
<xpath expr="//div[@name='option_send_mail']" position='after'>
<div name="option_l10n_it_edi" invisible="not l10n_it_edi_enable_send">
<field name="l10n_it_edi_checkbox_send" readonly="l10n_it_edi_readonly"/>
<b><label for="l10n_it_edi_checkbox_send"/></b>
</div>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -1,11 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from lxml import etree
from odoo.addons.l10n_it_edi.tests.common import TestItEdi
from odoo.exceptions import UserError
from odoo.tests.common import tagged
from odoo import tools
from odoo import Command
@tagged('post_install_l10n', 'post_install', '-at_install')
+5 -3
View File
@@ -16,7 +16,9 @@ def _l10n_it_edi_withholding_post_init(env):
'account.tax',
]
}
env['account.chart.template']._deref_account_tags(template_code, data['account.tax'])
for company in env['res.company'].search([('chart_template', '=', 'it')]):
for company in env['res.company'].search([('chart_template', '=', template_code)]):
_logger.info("Company %s already has the Italian localization installed, updating...", company.name)
env['account.chart.template'].with_company(company)._load_data(data)
company_chart_template = env['account.chart.template'].with_company(company)
company_data = dict(data)
company_chart_template._deref_account_tags(template_code, company_data['account.tax'])
company_chart_template._load_data(company_data)
@@ -22,7 +22,7 @@
<AliquotaIVA t-esc="format_numbers(pension_fund.vat_tax.amount or 0.0)"/>
<Ritenuta t-if="pension_fund.withholding_tax and pension_fund.withholding_tax.sequence > pension_fund.tax.sequence">SI</Ritenuta>
<Natura t-if="pension_fund.tax.l10n_it_has_exoneration" t-esc="format_alphanumeric(pension_fund.tax.l10n_it_kind_exoneration)"/>
<RiferimentoAmministrazione t-if="pension_fund.tax.description" t-esc="format_alphanumeric(pension_fund.tax.description)[:20]"/>
<RiferimentoAmministrazione t-if="pension_fund.tax.description" t-esc="format_alphanumeric(pension_fund.tax.description, 20)"/>
</DatiCassaPrevidenziale>
</t>
</xpath>
@@ -4,4 +4,3 @@
from . import account_tax
from . import account_chart_template
from . import account_move
from . import account_edi_format
@@ -1,130 +0,0 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
import logging
_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.
Needs not to call super, because super checks for one tax only per line.
"""
errors = []
for invoice_line in invoice.invoice_line_ids.filtered(lambda x: x.display_type == 'product'):
all_taxes = invoice_line.tax_ids.flatten_taxes_hierarchy()
vat_taxes, withholding_taxes, pension_fund_taxes = (all_taxes._l10n_it_filter_kind(kind) for kind in ('vat', 'withholding', 'pension_fund'))
if len(vat_taxes.filtered(lambda x: x.amount >= 0)) != 1:
errors.append(_("Bad tax configuration for line %s, there must be one and only one VAT tax per line", invoice_line.name))
if len(pension_fund_taxes) > 1 or len(withholding_taxes) > 1:
errors.append(_("Bad tax configuration for line %s, there must be one Withholding tax and one Pension Fund tax at max.", invoice_line.name))
return errors
def _l10n_it_edi_get_extra_info(self, company, document_type, body_tree):
extra_info, message_to_log = super()._l10n_it_edi_get_extra_info(company, document_type, body_tree)
withholding_elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiRitenuta')
withholding_taxes = []
for withholding in (withholding_elements or []):
tipo_ritenuta = withholding.find("TipoRitenuta")
reason = withholding.find("CausalePagamento")
percentage = withholding.find('AliquotaRitenuta')
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,
[('l10n_it_withholding_type', '=', withholding_type),
('l10n_it_withholding_reason', '=', withholding_reason)],
vat_only=False)
if withholding_tax:
withholding_taxes.append(withholding_tax)
else:
message_to_log.append("%s<br/>%s" % (
_("Withholding tax not found"),
self.env['account.move']._compose_info_message(body_tree, '.'),
))
extra_info["withholding_taxes"] = withholding_taxes
pension_fund_elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiCassaPrevidenziale')
pension_fund_taxes = []
for pension_fund in (pension_fund_elements or []):
pension_fund_type = pension_fund.find("TipoCassa")
tax_factor_percent = pension_fund.find("AlCassa")
vat_tax_factor_percent = pension_fund.find("AliquotaIVA")
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)],
vat_only=False)
if pension_fund_tax:
pension_fund_taxes.append(pension_fund_tax)
else:
message_to_log.append("%s<br/>%s" % (
_("Pension Fund tax not found"),
self.env['account.move']._compose_info_message(body_tree, '.'),
))
extra_info["pension_fund_taxes"] = pension_fund_taxes
return extra_info, message_to_log
def _import_fattura_pa_line(self, element, invoice_line_form, extra_info):
messages_to_log = super()._import_fattura_pa_line(element, invoice_line_form, extra_info)
for withholding_tax in extra_info.get('withholding_taxes', []):
withholding_tags = element.xpath("Ritenuta")
if withholding_tags and withholding_tags[0].text == 'SI':
invoice_line_form.tax_ids |= withholding_tax
for pension_fund_tax in extra_info.get('pension_fund_taxes', []):
invoice_line_form.tax_ids |= pension_fund_tax
if extra_info['simplified']:
return messages_to_log
price_subtotal = invoice_line_form.price_unit
company = invoice_line_form.company_id
# ENASARCO Pension Fund tax (works as a withholding)
for other_data_element in element.xpath('.//AltriDatiGestionali'):
data_kind_element = other_data_element.xpath("./TipoDato")
text_element = other_data_element.xpath("./RiferimentoTesto")
number_element = other_data_element.xpath("./RiferimentoNumero")
if not data_kind_element or not text_element or not number_element:
continue
data_kind, data_text, number_text = data_kind_element[0].text.lower(), text_element[0].text.lower(), number_element[0].text
if data_kind != 'cassa-prev' or ('enasarco' not in data_text and not 'tc07' in data_text):
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')],
vat_only=False)
if enasarco_tax:
invoice_line_form.tax_ids |= enasarco_tax
else:
messages_to_log.append("%s<br/>%s" % (
_("Enasarco tax not found for line with description '%s'", invoice_line_form.name),
self.env['account.move']._compose_info_message(other_data_element, '.'),
))
return messages_to_log
@@ -3,7 +3,7 @@
import logging
from collections import namedtuple
from odoo import api, fields, models
from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
@@ -28,15 +28,15 @@ class AccountMove(models.Model):
move.l10n_it_amount_pension_fund_signed = totals['pension_fund']
move.l10n_it_amount_before_withholding_signed = move.amount_untaxed_signed + totals['vat'] + totals['pension_fund']
def _l10n_it_edi_filter_fatturapa_tax_details(self, line, tax_values):
def _l10n_it_edi_filter_tax_details(self, line, tax_values):
"""Filters tax details to only include the positive amounted lines regarding VAT taxes."""
repartition_line = tax_values['tax_repartition_line']
repartition_line_vat = repartition_line.tax_id._l10n_it_filter_kind('vat')
return repartition_line.factor_percent >= 0 and repartition_line_vat and repartition_line_vat.amount >= 0
def _prepare_fatturapa_export_values(self):
def _l10n_it_edi_get_values(self, pdf_values=None):
"""Add withholding and pension_fund features."""
template_values = super()._prepare_fatturapa_export_values()
template_values = super()._l10n_it_edi_get_values(pdf_values)
# Withholding tax data
WithholdingTaxData = namedtuple('TaxData', ['tax', 'tax_amount'])
@@ -75,7 +75,8 @@ class AccountMove(models.Model):
enasarco_values = False
if enasarco_taxes:
enasarco_values = {}
enasarco_details = self._prepare_edi_tax_details(filter_to_apply=lambda line, tax_values: self.env['account.tax'].browse([tax_values['id']]).l10n_it_pension_fund_type == 'TC07')
enasarco_details = self._prepare_invoice_aggregated_taxes(
filter_tax_values_to_apply=lambda line, tax_values: self.env['account.tax'].browse([tax_values['id']]).l10n_it_pension_fund_type == 'TC07')
for detail in enasarco_details['tax_details_per_record'].values():
for subdetail in detail['tax_details'].values():
# Withholdings are removed from the total, we have to re-add them
@@ -94,3 +95,126 @@ class AccountMove(models.Model):
'document_total': document_total,
})
return template_values
def _l10n_it_edi_export_taxes_data_check(self):
"""
Override to also allow pension_fund, withholding taxes.
Needs not to call super, because super checks for one tax only per line.
"""
errors = []
for invoice_line in self.invoice_line_ids.filtered(lambda x: x.display_type == 'product'):
all_taxes = invoice_line.tax_ids.flatten_taxes_hierarchy()
vat_taxes, withholding_taxes, pension_fund_taxes = (all_taxes._l10n_it_filter_kind(kind) for kind in
('vat', 'withholding', 'pension_fund'))
if len(vat_taxes.filtered(lambda x: x.amount >= 0)) != 1:
errors.append(_("Bad tax configuration for line %s, there must be one and only one VAT tax per line", invoice_line.name))
if len(pension_fund_taxes) > 1 or len(withholding_taxes) > 1:
errors.append(_("Bad tax configuration for line %s, there must be one Withholding tax and one Pension Fund tax at max.", invoice_line.name))
return errors
# -------------------------------------------------------------------------
# Import
# -------------------------------------------------------------------------
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_get_extra_info(self, company, document_type, body_tree):
extra_info, message_to_log = super()._l10n_it_edi_get_extra_info(company, document_type, body_tree)
withholding_elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiRitenuta')
withholding_taxes = []
for withholding in (withholding_elements or []):
tipo_ritenuta = withholding.find("TipoRitenuta")
reason = withholding.find("CausalePagamento")
percentage = withholding.find('AliquotaRitenuta')
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,
[('l10n_it_withholding_type', '=', withholding_type),
('l10n_it_withholding_reason', '=', withholding_reason)],
vat_only=False)
if withholding_tax:
withholding_taxes.append(withholding_tax)
else:
message_to_log.append("%s<br/>%s" % (
_("Withholding tax not found"),
self.env['account.move']._compose_info_message(body_tree, '.'),
))
extra_info["withholding_taxes"] = withholding_taxes
pension_fund_elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiCassaPrevidenziale')
pension_fund_taxes = []
for pension_fund in (pension_fund_elements or []):
pension_fund_type = pension_fund.find("TipoCassa")
tax_factor_percent = pension_fund.find("AlCassa")
vat_tax_factor_percent = pension_fund.find("AliquotaIVA")
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)],
vat_only=False)
if pension_fund_tax:
pension_fund_taxes.append(pension_fund_tax)
else:
message_to_log.append("%s<br/>%s" % (
_("Pension Fund tax not found"),
self.env['account.move']._compose_info_message(body_tree, '.'),
))
extra_info["pension_fund_taxes"] = pension_fund_taxes
return extra_info, message_to_log
def _l10n_it_edi_import_line(self, element, move_line_form, extra_info=None):
messages_to_log = super()._l10n_it_edi_import_line(element, move_line_form, extra_info)
for withholding_tax in extra_info.get('withholding_taxes', []):
withholding_tags = element.xpath("Ritenuta")
if withholding_tags and withholding_tags[0].text == 'SI':
move_line_form.tax_ids |= withholding_tax
for pension_fund_tax in extra_info.get('pension_fund_taxes', []):
move_line_form.tax_ids |= pension_fund_tax
if extra_info['simplified']:
return messages_to_log
price_subtotal = move_line_form.price_unit
company = move_line_form.company_id
# ENASARCO Pension Fund tax (works as a withholding)
for other_data_element in element.xpath('.//AltriDatiGestionali'):
data_kind_element = other_data_element.xpath("./TipoDato")
text_element = other_data_element.xpath("./RiferimentoTesto")
number_element = other_data_element.xpath("./RiferimentoNumero")
if not data_kind_element or not text_element or not number_element:
continue
data_kind, data_text, number_text = data_kind_element[0].text.lower(), text_element[0].text.lower(), number_element[0].text
if data_kind != 'cassa-prev' or ('enasarco' not in data_text and 'tc07' not in data_text):
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')],
vat_only=False)
if enasarco_tax:
move_line_form.tax_ids |= enasarco_tax
else:
messages_to_log.append("%s<br/>%s" % (
_("Enasarco tax not found for line with description '%s'", move_line_form.name),
self.env['account.move']._compose_info_message(other_data_element, '.'),
))
return messages_to_log
@@ -143,10 +143,5 @@
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>___ignore___</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -130,10 +130,5 @@
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>___ignore___</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -123,10 +123,5 @@
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati>
<NomeAttachment>___ignore___</NomeAttachment>
<FormatoAttachment>PDF</FormatoAttachment>
<Attachment>___ignore___</Attachment>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from lxml import etree
from collections import namedtuple
from odoo import fields
@@ -3,4 +3,3 @@
from . import stock_picking
from . import account_invoice
from . import account_edi_format
@@ -1,30 +0,0 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountEdiFormat(models.Model):
_inherit = 'account.edi.format'
def _l10n_it_invoice_is_direct(self, invoice):
""" An invoice is only direct if the Transport Documents are all done the same day as the invoice. """
for ddt in invoice.l10n_it_ddt_ids:
if not ddt.date_done or ddt.date_done != invoice.invoice_date:
return False
return True
def _l10n_it_get_invoice_features_for_document_type_selection(self, invoice):
res = super()._l10n_it_get_invoice_features_for_document_type_selection(invoice)
res['direct_invoice'] = self._l10n_it_invoice_is_direct(invoice)
return res
def _l10n_it_document_type_mapping(self):
""" Deferred invoices (not direct) require TD24 FatturaPA Document Type. """
res = super()._l10n_it_document_type_mapping()
for document_type, infos in res.items():
if document_type == 'TD07':
continue
infos['direct_invoice'] = True
res['TD24'] = dict(move_types=['out_invoice'], import_type='in_invoice', direct_invoice=False)
return res
@@ -11,6 +11,28 @@ class AccountMove(models.Model):
l10n_it_ddt_ids = fields.Many2many('stock.picking', compute="_compute_ddt_ids")
l10n_it_ddt_count = fields.Integer(compute="_compute_ddt_ids")
def _l10n_it_edi_document_type_mapping(self):
""" Deferred invoices (not direct) require TD24 FatturaPA Document Type. """
res = super()._l10n_it_edi_document_type_mapping()
for document_type, infos in res.items():
if document_type == 'TD07':
continue
infos['direct_invoice'] = True
res['TD24'] = {'move_types': ['out_invoice'], 'import_type': 'in_invoice', 'direct_invoice': False}
return res
def _l10n_it_edi_invoice_is_direct(self):
""" An invoice is only direct if the Transport Documents are all done the same day as the invoice. """
for ddt in self.l10n_it_ddt_ids:
if not ddt.date_done or ddt.date_done != self.invoice_date:
return False
return True
def _l10n_it_edi_features_for_document_type_selection(self):
res = super()._l10n_it_edi_features_for_document_type_selection()
res['direct_invoice'] = self._l10n_it_edi_invoice_is_direct()
return res
def _get_ddt_values(self):
"""
We calculate the link between the invoice lines and the deliveries related to the invoice through the
@@ -79,7 +101,7 @@ class AccountMove(models.Model):
'domain': [('id', 'in', self.l10n_it_ddt_ids.ids)],
}
def _prepare_fatturapa_export_values(self):
template_values = super()._prepare_fatturapa_export_values()
def _l10n_it_edi_get_values(self, pdf_values=None):
template_values = super()._l10n_it_edi_get_values(pdf_values)
template_values['ddt_dict'] = self._get_ddt_values()
return template_values
@@ -127,6 +127,5 @@
<CodicePagamento>INV/2020/00001</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati/>
</FatturaElettronicaBody>
</p:FatturaElettronica>
+4 -3
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from lxml import etree
from freezegun import freeze_time
from odoo import tools
from odoo.tests import tagged, Form
@@ -133,10 +134,10 @@ class TestItEdiDDT(TestItEdi):
deferred_invoice.action_post()
# Check the XML output of the invoice
invoice_xml = self.edi_format._l10n_it_edi_export_invoice_as_xml(deferred_invoice)
invoice_xml = deferred_invoice._l10n_it_edi_render_xml()
expected_xml = self._get_stock_ddt_test_file_content("deferred_invoice.xml")
result = self._cleanup_etree(invoice_xml, {"//DatiGeneraliDocumento/Numero": "<Numero/>",})
expected = self._cleanup_etree(expected_xml, {"//DatiGeneraliDocumento/Numero": "<Numero/>",})
result = etree.fromstring(invoice_xml)
expected = etree.fromstring(expected_xml)
self.assertXmlTreeEqual(result, expected)
def _create_delivery(self, sale_order, qty=1):