diff --git a/addons/account/models/ir_attachment.py b/addons/account/models/ir_attachment.py
index f8d30912af9..b0701fce0ff 100644
--- a/addons/account/models/ir_attachment.py
+++ b/addons/account/models/ir_attachment.py
@@ -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.
diff --git a/addons/l10n_it_edi/__manifest__.py b/addons/l10n_it_edi/__manifest__.py
index e592961bf30..efc3de517e7 100644
--- a/addons/l10n_it_edi/__manifest__.py
+++ b/addons/l10n_it_edi/__manifest__.py
@@ -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',
],
diff --git a/addons/l10n_it_edi/data/account_edi_data.xml b/addons/l10n_it_edi/data/account_edi_data.xml
deleted file mode 100644
index fb454fffbd1..00000000000
--- a/addons/l10n_it_edi/data/account_edi_data.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
- Fattura PA (IT)
- fattura_pa
-
-
-
-
\ No newline at end of file
diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml
index e67d3a8e591..9c4def17fae 100644
--- a/addons/l10n_it_edi/data/invoice_it_template.xml
+++ b/addons/l10n_it_edi/data/invoice_it_template.xml
@@ -19,7 +19,7 @@
t-out="format_alphanumeric(line.product_uom_id.name, 10)"/>
-
+
diff --git a/addons/l10n_it_edi/data/ir_cron.xml b/addons/l10n_it_edi/data/ir_cron.xml
index 0515ecef3e1..aa5830164bb 100644
--- a/addons/l10n_it_edi/data/ir_cron.xml
+++ b/addons/l10n_it_edi/data/ir_cron.xml
@@ -1,12 +1,12 @@
-
- FatturaPA: Receive invoices from the exchange system
+
+ IT EDI: Receive invoices from the SdI1days-1
-
- model._cron_receive_fattura_pa()
+
+ model.cron_l10n_it_edi_download_and_update()code
diff --git a/addons/l10n_it_edi/i18n/it.po b/addons/l10n_it_edi/i18n/it.po
index 4447f2f487d..34ca72c9ed2 100644
--- a/addons/l10n_it_edi/i18n/it.po
+++ b/addons/l10n_it_edi/i18n/it.po
@@ -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"
"Language: it\n"
@@ -18,84 +18,109 @@ 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
+#: 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 "%s ha valore di 0.0, devi indicare il tipo di esenzione."
#. 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 "%s deve avere una Partita IVA"
#. 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
+#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a city."
msgstr "%s deve avere una Cittá."
#. 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 "%s deve avere un Codice Fiscale"
#. 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 "%s deve avere una Nazione"
#. 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
+#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a country."
msgstr "%s deve avere una Nazione."
#. 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
+#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a post code of length 5."
msgstr "%s deve avere un CAP di lunghezza 5."
#. 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
+#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a post code."
msgstr "%s deve avere un CAP."
#. 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
+#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "%s must have a street."
msgstr "%s deve avere un Indirizzo."
#. 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 ""
"'Scissione dei pagamenti' non è compatibile con l'esenzione di tipo 'N6'"
+#. module: l10n_it_edi
+#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_form_l10n_it
+msgid ""
+"E-invoicing (Italy)"
+msgstr ""
+"Fatturazione Elettronica (Italia)"
+
+#. module: l10n_it_edi
+#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_move_send_inherit_l10n_it_edi
+msgid ""
+"\n"
+" Tax Agency (Italy)\n"
+" "
+msgstr ""
+"\n"
+" Agenzia delle Entrate (IT)\n"
+" "
+
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_config_settings_view_form
msgid ""
"\n"
" Fattura Elettronica mode\n"
" \n"
-" "
+" "
msgstr ""
"\n"
" Modalità fattura elettronica\n"
@@ -114,25 +139,15 @@ msgstr ""
msgid "A Demo service is in use."
msgstr "Un servizio Demo è in uso"
-#. 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"
-msgstr "Modello di Piano dei Conti"
+msgstr "Modello piano dei conti"
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_account_edi_proxy_client_user
msgid "Account EDI proxy user"
-msgstr ""
+msgstr "Utente Proxy EDI"
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_account_move_send
@@ -156,7 +171,16 @@ msgstr "É giá stato registrato un servizio Ufficiale o di Test."
#. 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 "Errore durante l'aggiornamento dal Server Proxy: (%s) %s"
+
+#. 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 "Il file allegato è vuoto"
@@ -168,18 +192,24 @@ msgstr "Allegato"
#. 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 "Allegato dall'XML"
#. 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
+#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid "Bank account not found, useful informations from XML file:"
msgstr "Conto bancario non trovato, informazioni utili dal file XML:"
+#. 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 "Invio a SdI in corso"
+
#. 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."
@@ -187,11 +217,19 @@ msgstr "Selezionando questa casella, accetto che Odoo invii le mie fatture."
#. 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."
+"Cannot apply Reverse Charge to a bill which contains both services and "
+"goods."
msgstr ""
+"Impossibile applicare il Reverse Charge a una parcella che contiene prodotti"
+" che sono sia servizi che beni"
+
+#. module: l10n_it_edi
+#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_form_l10n_it
+msgid "Check Sending"
+msgstr "Controlla invio"
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_company__l10n_it_codice_fiscale
@@ -223,7 +261,7 @@ msgstr "L'azienda compare nel Registro delle Imprese"
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_res_config_settings
msgid "Config Settings"
-msgstr "Impostazione configurazioni"
+msgstr "Impostazioni di configurazione"
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_res_partner
@@ -235,13 +273,6 @@ msgstr "Contatto"
msgid "Created by"
msgstr "Creato da"
-#. 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"
@@ -258,24 +289,24 @@ msgstr "Valuta"
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_ddt_id
#: model:ir.ui.menu,name:l10n_it_edi.menu_action_ddt_account
msgid "DDT"
-msgstr "DDT"
+msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_l10n_it_ddt__date
msgid "Data DDT"
-msgstr "Data DDT"
+msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_bank_statement_line__l10n_it_stamp_duty
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_stamp_duty
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_stamp_duty
msgid "Dati Bollo"
-msgstr "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 "Demo"
+msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_partner__l10n_it_pa_index
@@ -295,27 +326,15 @@ msgstr "Nome visualizzato"
#. 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 "Data Documento non valida nel file XML:"
-
-#. 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"
-msgstr "La Fattura Elettronica è stata generata il %s da %s"
+msgid "Document date invalid in XML file: %s"
+msgstr "Data documento nel file XML non valida: %s"
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_line_it_FatturaPA
msgid "EAN"
-msgstr "EAN"
-
-#. module: l10n_it_edi
-#: model:ir.model,name:l10n_it_edi.model_account_edi_format
-msgid "EDI format"
-msgstr "Formato EDI"
+msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_company_form_l10n_it
@@ -327,11 +346,6 @@ msgstr "Indice Economico e Amministrativo"
msgid "Electronic Document Invoicing"
msgstr "Interscambio di dati in formato elettronico"
-#. 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
@@ -339,11 +353,24 @@ msgid "Electronic Invoicing"
msgstr "Fatturazione Elettronica"
#. 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"
-msgstr "Fattura Elettronica"
+#. 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 ""
+"Errore di invio del file di Fatturazione Elettronica %s.\n"
+"%s"
+
+#. 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 "Errori nella creazione del file di Fatturazione Elettronica."
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_mail_template
@@ -370,19 +397,14 @@ msgstr "Tipo esenzione"
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_edi_attachment_id
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_edi_attachment_id
msgid "FatturaPA Attachment"
-msgstr "Allegato FatturaPA"
+msgstr "Allegato EDI (Italia)"
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_bank_statement_line__l10n_it_edi_transaction
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_move__l10n_it_edi_transaction
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_payment__l10n_it_edi_transaction
msgid "FatturaPA Transaction"
-msgstr "Transazione FatturaPA"
-
-#. 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 "FatturaPA: Ricezione fatture dal Sistema d'Interscambio"
+msgstr "Transazione EDI (Italia)"
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_res_company__l10n_it_codice_fiscale
@@ -404,6 +426,18 @@ msgstr ""
msgid "INTERNAL"
msgstr "INTERNO"
+#. 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 "EDI Italia: Ricezione Fatturazione Elettronica dal SdI"
+
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/res_company.py:0
@@ -412,10 +446,12 @@ msgid ""
"If one of Share Capital or Sole Shareholder is present, then they must be "
"both filled out."
msgstr ""
+"Se uno fra Capitale Sociale o Socio Unico è presente, allora devono esserlo "
+"entrambi."
#. 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 "
@@ -427,24 +463,20 @@ msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_config_settings_view_form
msgid ""
-"In demo mode Odoo will just simulate the sending of invoices to the "
-"government. \n"
-" In test mode (experimental) Odoo will send "
-"the invoices to a non-production service.\n"
-" Saving this change will direct all companies "
-"on this database to this use this configuration.\n"
-" Once registered for testing or official, the "
-"mode cannot be changed."
+"In demo mode Odoo will just simulate the sending of invoices to the government. \n"
+" In test mode (experimental) Odoo will send the invoices to a non-production service.\n"
+" Saving this change will direct all companies on this database to this use this configuration.\n"
+" Once registered for testing or official, the mode cannot be changed."
msgstr ""
"In modalità Demo, Odoo simula l'invio delle fatture all'Agenzia delle "
-"Entrate. In modalità Test (sperimentale) Odoo invia le fatture a un servizio "
-"non ufficiale. Una volta salvata l'impostazione, tutte le aziende nel "
-"database utilizzano questa configurazione. Una volta registrata, la modalità "
-"non può essere cambiata."
+"Entrate. In modalità Test (sperimentale) Odoo invia le fatture a un servizio"
+" non ufficiale. Una volta salvata l'impostazione, tutte le aziende nel "
+"database utilizzano questa configurazione. Una volta registrata, la modalità"
+" non può essere cambiata."
#. 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 "La linea %s deve avere una e solo una imposta.."
@@ -465,18 +497,6 @@ msgstr ""
msgid "Invoice Reference"
msgstr "Riferimento Fattura"
-#. 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 ""
-"Le Fatture per la Pubblica Amministrazione non sono gestite da Odoo. É "
-"possibile scaricare il documento e inviarlo tramite il sito dell'Agenzia "
-"delle Entrate."
-
#. 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"
@@ -485,20 +505,43 @@ msgstr "É attivo il Proxy EDI"
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__account_edi_proxy_client_user__proxy_type__l10n_it_edi
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"
-msgstr "Fattura italiana: %s"
+msgstr "Fatturazione Elettronica (Italia)"
#. module: l10n_it_edi
#: model:ir.model,name:l10n_it_edi.model_account_move
msgid "Journal Entry"
-msgstr "Movimento Contabile"
+msgstr "Registrazione contabile"
+
+#. module: l10n_it_edi
+#: 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 "Modalitá demo L10n It EDI"
+
+#. 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 "Abilita invio alla Agenzia delle Entrate (Italia)"
+
+#. 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 "Autofattura"
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_config_settings__l10n_it_edi_proxy_current_state
@@ -506,21 +549,31 @@ msgid "L10N It Edi Proxy Current State"
msgstr "Stato corrente del Proxy L10n It EDI"
#. 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"
-msgstr "Modalitá demo L10n It EDI Sdicoop"
+#: 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 "Utente Proxy EdI (Italia)"
#. 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 "Invio SdI (Italia) Readonly"
+
+#. module: l10n_it_edi
+#: model:ir.model.fields,field_description:l10n_it_edi.field_res_config_settings__l10n_it_edi_register
+msgid "L10N It Edi Register"
msgstr "Registra L10n It EDI Sdicoop"
#. 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"
-msgstr "Nome Fattura Elettronica"
+#: 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 "Stato Invio EDI (Italia)"
+
+#. 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 "Avviso EDI (Italia)"
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_company__l10n_it_has_eco_index
@@ -561,8 +614,8 @@ msgstr ""
#: model:ir.model.fields,help:l10n_it_edi.field_res_company__l10n_it_eco_index_share_capital
msgid ""
"Mandatory if the seller/provider is a company with share capital "
-"(SpA, SApA, Srl), this field must contain the amount of share capital "
-"actually paid up as resulting from the last financial statement"
+"(SpA, SApA, Srl), this field must contain the amount of share capital"
+" actually paid up as resulting from the last financial statement"
msgstr ""
"Obbligatorio se il venditore/fornitore è una azienda con capitale sociale "
"(SpA, SApA, Srl), rappresenta l'ammontare del capitale sociale attuale come "
@@ -572,11 +625,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 ""
"Deve contenere il codice da 6-7 caratteri presente nell'indice della PA "
"nelle informazioni relative al servizio di Fatturazione Elettronica, "
@@ -596,10 +648,10 @@ msgstr "Numero nel Registro delle Imprese"
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_l10n_it_ddt__name
msgid "Numero DDT"
-msgstr "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 "Ufficiale"
@@ -607,7 +659,7 @@ msgstr "Ufficiale"
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_it_FatturaPA_export
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_it_simplified_FatturaPA_export
msgid "PDF"
-msgstr "PDF"
+msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_partner__l10n_it_pec_email
@@ -617,10 +669,10 @@ msgstr "Email PEC"
#. 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:"
-msgstr "Data Pagamento non valida nel file XML:"
+msgid "Payment due date invalid in XML file: %s"
+msgstr "Data di pagamento non valida nel file XML: %s"
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_company__l10n_it_eco_index_sole_shareholder__sm
@@ -629,13 +681,13 @@ 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 FatturaPA"
-msgstr ""
-"Per favore, inserire il Codice Fiscale per poter ricevere le fatture da "
+"Please fill your codice fiscale to be able to receive invoices from "
"FatturaPA"
+msgstr ""
+"Per favore, inserire il Codice Fiscale per poter ricevere le fatture dal SdI"
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_res_company__l10n_it_tax_system
@@ -650,16 +702,88 @@ msgstr "Provincia dell'ufficio del Registro delle Imprese"
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_account_edi_proxy_client_user__proxy_type
msgid "Proxy type"
-msgstr ""
+msgstr "Tipo Proxy"
+
+#. 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 "Richiede Firma Utente"
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_it_FatturaPA_export
msgid "SI"
-msgstr "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 "Accettato SdI, Accettato dal Partner PA"
+
+#. 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 "Accettato SdI, Inoltro al Partner Fallito"
+
+#. 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 "Accettato SdI, Inoltrato al Partner"
+
+#. 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 "Accettato SdI, Inoltro in Corso"
+
+#. 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 "Accettato SdI, Decadimento Termini Partner PA"
+
+#. 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 "Accettato SdI, Rifiutato dal Partner PA"
+
+#. 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 "Elaborazione SdI"
+
+#. 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 "Rifiutato SdI"
+
+#. module: l10n_it_edi
+#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_invoice_form_l10n_it
+msgid "Send Tax Integration"
+msgstr "Invio Integrazione Imposte"
+
+#. 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 ""
+"Invia la Fatturazione Elettronica all'Agenzia delle Entrate\n"
+"Viene impostato in sola lettura se è stato già creato un resoconto, per evitare disallineamenti.\n"
+"Per riabilitarlo, cancellare l'allegato PDF."
#. 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 ""
+"L'invio della Fatturazione Elettronica a Partner PA non è supportato.\n"
+" Il file XML per l'EDI (Italia) è stato generato, per favore firmalo digitalmente e caricalo tramite il portale 'Fatture e Corrispettivi' dell'Agenzia delle Entrate."
+
+#. 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 "Servizio momentaneamente non disponibile"
@@ -701,6 +825,11 @@ msgstr ""
msgid "Tax"
msgstr "Imposta"
+#. 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 "Agenzia delle Entrate (IT)"
+
#. module: l10n_it_edi
#: model:ir.model.fields,field_description:l10n_it_edi.field_res_company__l10n_it_tax_system
msgid "Tax System"
@@ -709,11 +838,11 @@ msgstr "Sistema Fiscale"
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_account_tax__l10n_it_has_exoneration
msgid "Tax has a tax exoneration."
-msgstr "La tassa ha un'Esenzione."
+msgstr "L'Imposta ha un'Esenzione."
#. 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 "Imposta non trovata per la linea con descrizione '%s'"
@@ -730,36 +859,38 @@ msgstr "Rappresentante fiscale del partner"
#. 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 ""
-"Il rappresentante fiscale del partner %s di %s deve avere un codice fiscale."
+"Il rappresentante fiscale del partner %s di %s deve avere una Partita IVA."
#. 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 "Test (sperimentale)"
#. 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 "
"(Codice Fiscale)."
msgstr ""
+"Il Cessionario/Committente %s o la sua Azienda devono avere una Partita IVA "
+"e un Codice Fiscale."
#. module: l10n_it_edi
#. odoo-python
#: code:addons/l10n_it_edi/models/res_config_settings.py:0
#, python-format
msgid ""
-"The company has already registered with the service as 'Test' or 'Official', "
-"it cannot change."
+"The company has already registered with the service as 'Test' or 'Official',"
+" it cannot change."
msgstr ""
-"L'Azienda è giá registrata al servizio con la modalitá Test o Officiale, non "
-"può essere cambiata."
+"L'Azienda è giá registrata al servizio con la modalitá Test o Officiale, non"
+" può essere cambiata."
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_company__l10n_it_eco_index_liquidation_state__ls
@@ -773,12 +904,14 @@ msgstr "L'azienda non è in stato di liquidazione"
#. 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 ""
+"Il file di Fatturazione Elettronica %s non può essere inoltrato a %s (Pubblica Amministrazione) al momento.\n"
+"Riproverà per 10 giorni, dopo i quali il documento sarà considerato accettato, ma dovrai comunque inviarlo per Posta o e-mail."
#. module: l10n_it_edi
#: code:addons/l10n_it_edi/models/account_edi_format.py:0
@@ -789,87 +922,152 @@ msgstr "La lunghezza minima per la Partita IVA è 30. %s ha una Partita IVA trop
#. 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."
-msgstr "La Fattura è stata emessa con successo. Il partner della Pubblica Amministrazione "
-"ha avuto 15 giorni per accettare o rifiutare il documento, ma non hanno risposto, "
-"quindi da ora consideriamo la Fattura accettata."
+"The e-invoice file %s couldn't be forwarded to %s.\n"
+"Please remember to send it via post or e-mail."
+msgstr ""
+"Il file di Fatturazione Elettronica %s non può essere inoltrato a %s\n"
+"Per favore, ricordati di inviarlo per Posta o per e-mail."
#. module: l10n_it_edi
#. odoo-python
-#: code:addons/l10n_it_edi/models/account_edi_format.py:0
-#, python-format
-msgid "Service momentarily unavailable"
-msgstr "Servizio momentaneamente non disponibile"
-
-#. 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."
-msgstr "La fattura è stata inviata, ma la consegna al Destinatario è fallita. "
-"É necessario mandare una copia di cortesia della fattura al cliente "
-"tramite un altro canale al di fuori dell'SdI, e notificarlo che l'originale "
-"è depositato nella sua Area Personale all' interno del portale 'Fatture e Corrispettivi' "
-"sul sito dell'Agenzia delle Entrate"
+"The e-invoice file %s has been accepted by %s (Public Administration), a "
+"payment will be issued soon"
+msgstr ""
+"Il file di Fatturazione Elettronica %s è stato accettato da %s (Pubblica "
+"Amministrazione), il pagamento verrà inviato presto."
#. 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."
-msgstr "La fattura è stata inviata, ma la consegna alla Pubblica Amministrazione "
-"è fallita. Il Sistema d'Interscambio contatterá il Destinatario per far rapporto "
-"sul problema, e richiederá che venga approntata una soluzione. Durante i "
-"10 giorni successivi, il Sistema d'Interscambio proverá a inoltrare il file "
-"FatturaPA in questione ancora. Dovesse fallire anche questo tentativo, il "
-"sistema notificherá Odoo della mancata consegna, e sará necessario inviare "
-"la fattura tramite un altro canale, al di fuori dell'SdI"
+"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 ""
+"Il file di Fatturazione Elettronica %s è stato accettato dal SdI.\n"
+"Il SdI sta provando ad inviarlo a %s.\n"
+"Proverà per due giorni, dopo i quali bisognerà eventualmente inviare la fattura al partner per posta o e-mail."
#. 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 "La fattura è stata rifiutata dal Sistema d'Interscambio"
-
-#. 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."
-msgstr "La fattura è stata trasmessa con successo. Il Destinatario ha 15 giorni per accettarla o rifiutarla."
+"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 ""
+"Il file di Fatturazione Elettronica %s è stato rifiutato da %s (Pubblica Amministrazione).\n"
+" Hai 5 giorni per fare una Nota di Credito completa, poi contattare il Partner PA e creare una nuova Fattura Elettronica seguendo i loro requisiti, e reinviarla."
#. 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."
-msgstr "La fattura è stata rifiutata dal Destinatario."
+msgid "The e-invoice file %s has been sent in Demo EDI mode."
+msgstr ""
+"Il file di Fatturazione Elettronica %s è stato inviato in modalità EDI Demo."
#. 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 ""
+"Il file di Fatturazione Elettronica %s non è stato trovato sul Proxy Server "
+"EDI (Italia)"
+
+#. module: l10n_it_edi
+#. odoo-python
+#: code:addons/l10n_it_edi/models/account_move.py:0
+#, python-format
+msgid ""
+"The e-invoice file %s is duplicated.\n"
+"Original message from the SdI: %s"
+msgstr ""
+"Il file di Fatturazione Elettronica %s è duplicato.\n"
+"Messaggio originale dal SdI: %s"
+
+#. module: l10n_it_edi
+#. odoo-python
+#: 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 ""
+"Il file di Fatturazione Elettronica %s è stato mandato al SdI con successo. La fattura è ora considerata fiscalmente rilevante.\n"
+" %s (Pubblica Amministrazione) ha avuto 15 giorni per accettare o rifiutare questo documento, ma dato che non ha risposto, ora la fattura è considerata accettata."
+
+#. 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 ""
+"Il file di Fatturazione Elettronica %s è stato accettato e inoltrato con "
+"successo a %s dal SdI."
+
+#. 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 ""
+"Il file di Fatturazione Elettronica %s è stato inviato al SdI per "
+"l'elaborazione."
+
+#. 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 ""
+"Il file di Fatturazione Elettronica %s è stato inviato al SdI per il controllo.\n"
+"Non è ancora considerato valido, bisogna attendere ulteriori notifiche."
+
+#. 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 ""
+"Il file di Fatturazione Elettronica %s è stato inviato con successo al SdI.\n"
+"%s ha 15 giorni per accettarlo o rifiutarlo."
+
+#. 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 ""
+"Il file di Fatturazione Elettronica %s è duplicato. Per favore, controlla la sequenza relativa ai nomi dei file della FatturaPA.\n"
+"Messaggio originale dal SdI: %s"
+
+#. 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 "La Fattura Elettronica è stata rifiutata dal SdI."
+
+#. module: l10n_it_edi
+#. odoo-python
+#: code:addons/l10n_it_edi/models/account_move.py:0
#, python-format
msgid ""
"The invoice was successfully transmitted to the Public Administration and we"
@@ -879,7 +1077,7 @@ msgstr "La fattura è stata trasmessa con successo alla Pubblica Amministrazione
#. 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 "L'azienda del venditore deve avere specificato un Sistema Fiscale."
@@ -888,8 +1086,7 @@ msgstr "L'azienda del venditore deve avere specificato un Sistema Fiscale."
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_company_form_l10n_it
msgid ""
"The seller/provider is a company listed on the register of companies and as\n"
-" such must also indicate the registration data on "
-"all documents (art. 2250, Italian\n"
+" such must also indicate the registration data on all documents (art. 2250, Italian\n"
" Civil Code)"
msgstr ""
"Il venditore/fornitore è un'azienda presente nel Registro delle Imprese\n"
@@ -899,20 +1096,19 @@ msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_res_company__l10n_it_has_eco_index
msgid ""
-"The seller/provider is a company listed on the register of companies and "
-"as such must also indicate the registration data on all documents "
-"(art. 2250, Italian Civil Code)"
+"The seller/provider is a company listed on the register of companies and as"
+" such must also indicate the registration data on all documents (art."
+" 2250, Italian Civil Code)"
msgstr ""
-"Il venditore/fornitore è un'azienda presente nel Registro delle Impresee "
-"come tale deve indicare i dati di registrazione su tutti i documenti\n"
+"Il venditore/fornitore è un'azienda presente nel Registro delle Impresee come tale deve indicare i dati di registrazione su tutti i documenti\n"
"(art. 2250 del Codice Civile)"
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_res_company__l10n_it_has_tax_representative
msgid ""
"The seller/provider is a non-resident subject which carries out "
-"transactions in Italy with relevance for VAT purposes and which takes "
-"avail of a tax representative in Italy"
+"transactions in Italy with relevance for VAT purposes and which takes"
+" avail of a tax representative in Italy"
msgstr ""
"Il venditore/fornitore è un soggetto non-residente che svolge le sue "
"transazioni in Italia con rilevanza fiscale e che fa riferimento a un "
@@ -921,10 +1117,8 @@ msgstr ""
#. module: l10n_it_edi
#: model_terms:ir.ui.view,arch_db:l10n_it_edi.res_company_form_l10n_it
msgid ""
-"The seller/provider is a non-resident subject which carries out transactions "
-"in Italy\n"
-" with relevance for VAT purposes and which takes "
-"avail of a tax representative in Italy"
+"The seller/provider is a non-resident subject which carries out transactions in Italy\n"
+" with relevance for VAT purposes and which takes avail of a tax representative in Italy"
msgstr ""
"Il venditore/fornitore è un soggetto non-residente che svolge le sue "
"transazioni in Italia con rilevanza fiscale e che fa riferimento a un "
@@ -933,31 +1127,29 @@ msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields,help:l10n_it_edi.field_res_company__l10n_it_eco_index_number
msgid ""
-"This field must contain the number under which the seller/provider is "
-"listed on the register of companies."
+"This field must contain the number under which the seller/provider is"
+" listed on the register of companies."
msgstr ""
-"Questo campo deve contenere il numero sotto il quale è presente nel Registro "
-"delle Imprese"
+"Questo campo deve contenere il numero sotto il quale è presente nel Registro"
+" delle Imprese"
#. 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"
-msgstr ""
-"Una fattura con questo numero è giá stata inviata all'SdI, quindi viene "
-"impostata come inviata. Per favore, verificare che il sistema sia "
-"configurato correttamente, perchè in nessun caso il sistema ha motivo di "
-"inviare la stessa fattura più volte.\n"
-"Messaggio originale dall'SdI: %s"
+msgid "This move is not waiting for updates from the SdI."
+msgstr "Questa Registrazione non sta attendendo aggiornamenti dal SdI."
+
+#. module: l10n_it_edi
+#: model:ir.model.fields,help:l10n_it_edi.field_account_bank_statement_line__l10n_it_edi_state
+#: model:ir.model.fields,help:l10n_it_edi.field_account_move__l10n_it_edi_state
+#: model:ir.model.fields,help:l10n_it_edi.field_account_payment__l10n_it_edi_state
+msgid "This state is updated by default, but you can force the value. "
+msgstr "Questo stato viene aggiornato automaticamente, ma è possibile forzare il valore."
#. 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 "Valore totale del file XML: %s"
@@ -980,18 +1172,35 @@ msgstr "Numero Documento Di Trasporto"
#. 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 "Informazioni di Trasporto dal file XML:"
#. 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 "Utente non autorizzato"
+#. module: l10n_it_edi
+#. odoo-python
+#: code:addons/l10n_it_edi/models/account_edi_proxy_user.py:0
+#, python-format
+msgid "Unknown error"
+msgstr "Errore sconosciuto"
+
+#. 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 ""
+"Descrizione utente dello stato corrente, con suggerimenti sul progresso del "
+"flusso"
+
#. 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"
@@ -999,37 +1208,42 @@ msgstr "Data scadenza IVA"
#. 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 "
"(Codice Destinatario) on the company's contact."
-msgstr "Fatture d'acquisto inviate come autofatture all'SdI richiedono "
-"un Codice Destinatario valido nel Contatto dell'Impresa"
+msgstr ""
+"Le Parcelle inviate come AutoFatture/Integrazioni richiedono l'inserimento "
+"di un Codice Destinatario valido sul Contatto dell'Azienda."
#. 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 "Fornitore non trovato, informazioni dal file XML:"
#. 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."
-msgstr "Non sei autorizzato a controllare lo stato di questa fattura."
+msgid ""
+"We are simulating the sending of the e-invoice file %s, as we are in demo "
+"mode."
+msgstr ""
+"Simulazione dell'invio del file di Fatturazione Elettronica %s in modalità "
+"demo in corso"
#. module: l10n_it_edi
#. odoo-python
-#: 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 ""
-"Devi accettare i Termini e le Condizioni d'uso nelle impostazioni per poter "
-"utilizzare FatturaPA."
+"Devi accettare i Termini e le Condiioni nelle Impostazioni di Contabilità "
+"per usare l'EDI (Italia)"
#. module: l10n_it_edi
#. odoo-python
@@ -1209,7 +1423,8 @@ msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_company__l10n_it_tax_system__rf04
msgid ""
-"[RF04] Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)"
+"[RF04] Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR "
+"633/72)"
msgstr ""
#. module: l10n_it_edi
@@ -1242,8 +1457,8 @@ msgstr ""
#. module: l10n_it_edi
#: model:ir.model.fields.selection,name:l10n_it_edi.selection__res_company__l10n_it_tax_system__rf10
msgid ""
-"[RF10] Intrattenimenti, giochi e altre attività di cui alla tariffa allegata "
-"al DPR 640/72 (art.74, c.6, DPR 633/72)"
+"[RF10] Intrattenimenti, giochi e altre attività di cui alla tariffa allegata"
+" al DPR 640/72 (art.74, c.6, DPR 633/72)"
msgstr ""
#. module: l10n_it_edi
@@ -1302,20 +1517,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 "dal file XML:"
-
-#~ msgid "Last Modified on"
-#~ msgstr "Data Modifica"
-
-#~ msgid ""
-#~ "Tax not found with percentage: %s and exoneration %s for the article: %s"
-#~ msgstr ""
-#~ "L'imposta con percentuale: %s e Esenzione %s per il prodotto: %s non è "
-#~ "stata trovata."
-
-#~ msgid "Tax not found with percentage: %s for the article: %s"
-#~ msgstr ""
-#~ "L'imposta con percentuale: %s per il prodotto: %s non è stata trovata"
diff --git a/addons/l10n_it_edi/i18n/l10n_it_edi.pot b/addons/l10n_it_edi/i18n/l10n_it_edi.pot
index f4743ca61d2..a0d0e2b269a 100644
--- a/addons/l10n_it_edi/i18n/l10n_it_edi.pot
+++ b/addons/l10n_it_edi/i18n/l10n_it_edi.pot
@@ -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 ""
+"E-invoicing (Italy)"
+msgstr ""
+
+#. module: l10n_it_edi
+#: model_terms:ir.ui.view,arch_db:l10n_it_edi.account_move_send_inherit_l10n_it_edi
+msgid ""
+"\n"
+" Tax Agency (Italy)\n"
+" "
+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 ""
\ No newline at end of file
+msgstr ""
diff --git a/addons/l10n_it_edi/models/__init__.py b/addons/l10n_it_edi/models/__init__.py
index 6804a7790d9..b3d014de30a 100644
--- a/addons/l10n_it_edi/models/__init__.py
+++ b/addons/l10n_it_edi/models/__init__.py
@@ -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
diff --git a/addons/l10n_it_edi/models/account_edi_document.py b/addons/l10n_it_edi/models/account_edi_document.py
deleted file mode 100644
index 68484a817b9..00000000000
--- a/addons/l10n_it_edi/models/account_edi_document.py
+++ /dev/null
@@ -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()
diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py
deleted file mode 100644
index 4c36b41b558..00000000000
--- a/addons/l10n_it_edi/models/account_edi_format.py
+++ /dev/null
@@ -1,1086 +0,0 @@
-# -*- coding:utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import Command, api, models, fields, _, _lt
-from odoo.exceptions import UserError
-from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError
-from odoo.addons.l10n_it_edi.tools.remove_signature import remove_signature
-from odoo.osv.expression import OR, AND
-
-from lxml import etree
-from datetime import datetime
-import re
-import logging
-import base64
-
-
-_logger = logging.getLogger(__name__)
-
-DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
-
-
-class AccountEdiFormat(models.Model):
- _inherit = 'account.edi.format'
-
- # -------------------------------------------------------------------------
- # Helpers
- # -------------------------------------------------------------------------
-
- @api.model
- def _l10n_it_edi_generate_electronic_invoice_filename(self, invoice):
- '''Returns a name conform to the Fattura pa Specifications:
- See ES documentation 2.2
- '''
- a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
- # Each company should have its own filename sequence. If it does not exist, create it
- n = self.env['ir.sequence'].with_company(invoice.company_id).next_by_code('l10n_it_edi.fattura_filename')
- if not n:
- # The offset is used to avoid conflicts with existing filenames
- offset = 62 ** 4
- sequence = self.env['ir.sequence'].sudo().create({
- 'name': 'FatturaPA Filename Sequence',
- 'code': 'l10n_it_edi.fattura_filename',
- 'company_id': invoice.company_id.id,
- 'number_next': offset,
- })
- n = sequence._next()
- # The n is returned as a string, but we require an int
- n = int(''.join(filter(lambda c: c.isdecimal(), n)))
-
- progressive_number = ""
- while n:
- (n, m) = divmod(n, len(a))
- progressive_number = a[m] + progressive_number
-
- return '%(country_code)s%(codice)s_%(progressive_number)s.xml' % {
- 'country_code': invoice.company_id.country_id.code,
- 'codice': invoice.company_id.partner_id._l10n_it_edi_normalized_codice_fiscale(),
- 'progressive_number': progressive_number.zfill(5),
- }
-
- def _l10n_it_edi_check_invoice_configuration(self, invoice):
- errors = self._l10n_it_edi_check_ordinary_invoice_configuration(invoice)
-
- if not errors:
- errors = self._l10n_it_edi_check_simplified_invoice_configuration(invoice)
-
- return errors
-
- def _l10n_it_edi_is_self_invoice(self, invoice):
- """
- Italian EDI requires Vendor bills coming from EU countries to be sent as self-invoices.
- We recognize these cases based on the taxes that target the VJ tax grids, which imply
- the use of VAT External Reverse Charge.
- """
- if not invoice.is_purchase_document():
- return False
-
- invoice_lines_tags = invoice.line_ids.tax_tag_ids
- it_tax_report_vj_lines = self.env['account.report.line'].search([
- ('report_id.country_id.code', '=', 'IT'),
- ('code', 'like', 'VJ%'),
- ])
- vj_lines_tags = it_tax_report_vj_lines.expression_ids._get_matching_tags()
- return bool(invoice_lines_tags & vj_lines_tags)
-
- def _l10n_it_edi_check_ordinary_invoice_configuration(self, invoice):
- errors = []
- seller = invoice.company_id
- buyer = invoice.commercial_partner_id
- is_self_invoice = self._l10n_it_edi_is_self_invoice(invoice)
- if is_self_invoice:
- seller, buyer = buyer, seller
-
- # <1.1.1.1>
- if not seller.country_id:
- errors.append(_("%s must have a country", seller.display_name))
-
- # <1.1.1.2>
- if not invoice.company_id.vat:
- errors.append(_("%s must have a VAT number", invoice.company_id.name))
- if seller.vat and len(seller.vat) > 30:
- errors.append(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.", seller.display_name, seller.vat))
-
- # <1.2.1.2>
- if not is_self_invoice and not seller.l10n_it_codice_fiscale:
- errors.append(_("%s must have a codice fiscale number", seller.display_name))
-
- # <1.2.1.8>
- if not is_self_invoice and not seller.l10n_it_tax_system:
- errors.append(_("The seller's company must have a tax system."))
-
- # <1.2.2>
- if not seller.street and not seller.street2:
- errors.append(_("%s must have a street.", seller.display_name))
- if not seller.zip:
- errors.append(_("%s must have a post code.", seller.display_name))
- elif len(seller.zip) != 5 and seller.country_id.code == 'IT':
- errors.append(_("%s must have a post code of length 5.", seller.display_name))
- if not seller.city:
- errors.append(_("%s must have a city.", seller.display_name))
- if not seller.country_id:
- errors.append(_("%s must have a country.", seller.display_name))
-
- if not is_self_invoice and seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat:
- errors.append(_("Tax representative partner %s of %s must have a tax number.", seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name))
-
- # <1.4.1>
- if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
- errors.append(_("The buyer, %s, or his company must have a VAT number and/or a tax code (Codice Fiscale).", buyer.display_name))
-
- if is_self_invoice and self._l10n_it_edi_services_or_goods(invoice) == 'both':
- errors.append(_("Cannot apply Reverse Charge to a bill which contains both services and goods."))
-
- if is_self_invoice and not buyer.partner_id.l10n_it_pa_index:
- errors.append(_("Vendor bills sent as self-invoices to the SdI require a valid PA Index (Codice Destinatario) on the company's contact."))
-
- for tax_line in invoice.line_ids.filtered(lambda line: line.tax_line_id):
- if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
- errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
-
- errors += self._l10n_it_edi_check_taxes_configuration(invoice)
-
- return errors
-
- def _l10n_it_edi_check_taxes_configuration(self, invoice):
- """
- Can be overridden by submodules like l10n_it_edi_withholding, which also allows for withholding and pension_fund taxes.
- """
- 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 = all_taxes.filtered(lambda t: t.amount_type == 'percent' and t.amount >= 0)
- if len(vat_taxes) != 1:
- errors.append(_("In line %s, you must select one and only one VAT tax.", invoice_line.name))
- return errors
-
- def _l10n_it_edi_is_simplified(self, invoice):
- """
- Simplified Invoices are a way for the invoice issuer to create an invoice with limited data.
- Example: a consultant goes to the restaurant and wants the invoice instead of the receipt,
- to be able to deduct the expense from his Taxes. The Italian State allows the restaurant
- to issue a Simplified Invoice with the VAT number only, to speed up times, instead of
- requiring the address and other informations about the buyer.
- Only invoices under the threshold of 400 Euroes are allowed, to avoid this tool
- be abused for bigger transactions, that would enable less transparency to tax institutions.
- """
- buyer = invoice.commercial_partner_id
- return all([
- self.env.ref('l10n_it_edi.account_invoice_it_simplified_FatturaPA_export', raise_if_not_found=False),
- not self._l10n_it_edi_is_self_invoice(invoice),
- self._l10n_it_edi_check_buyer_invoice_configuration(invoice),
- not buyer.country_id or buyer.country_id.code == 'IT',
- buyer.l10n_it_codice_fiscale or (buyer.vat and (buyer.vat[:2].upper() == 'IT' or buyer.vat[:2].isdecimal())),
- invoice.amount_total <= 400,
- ])
-
- def _l10n_it_edi_check_simplified_invoice_configuration(self, invoice):
- return [] if self._l10n_it_edi_is_simplified(invoice) else self._l10n_it_edi_check_buyer_invoice_configuration(invoice)
-
- def _l10n_it_edi_services_or_goods(self, invoice):
- """
- Services and goods have different tax grids when VAT is Reverse Charged, and they can't
- be mixed in the same invoice, because the TipoDocumento depends on which which kind
- of product is bought and it's unambiguous.
- """
- scopes = []
- for line in invoice.invoice_line_ids.filtered(lambda l: l.display_type not in ('line_note', 'line_section')):
- tax_ids_with_tax_scope = line.tax_ids.filtered(lambda x: x.tax_scope)
- if tax_ids_with_tax_scope:
- scopes += tax_ids_with_tax_scope.mapped('tax_scope')
- else:
- scopes.append(line.product_id and line.product_id.type or 'consu')
-
- if set(scopes) == set(['consu', 'service']):
- return "both"
- return scopes and scopes.pop()
-
- def _l10n_it_edi_check_buyer_invoice_configuration(self, invoice):
- errors = []
- buyer = invoice.commercial_partner_id
-
- # <1.4.2>
- if not buyer.street and not buyer.street2:
- errors.append(_("%s must have a street.", buyer.display_name))
- if not buyer.country_id:
- errors.append(_("%s must have a country.", buyer.display_name))
- if not buyer.zip:
- errors.append(_("%s must have a post code.", buyer.display_name))
- elif len(buyer.zip) != 5 and buyer.country_id.code == 'IT':
- errors.append(_("%s must have a post code of length 5.", buyer.display_name))
- if not buyer.city:
- errors.append(_("%s must have a city.", buyer.display_name))
-
- for tax_line in invoice.line_ids.filtered(lambda line: line.tax_line_id):
- if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
- errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
-
- return errors
-
- def _l10n_it_goods_in_italy(self, invoice):
- """
- There is a specific TipoDocumento (Document Type TD19) and tax grid (VJ3) for goods
- that are phisically in Italy but are in a VAT deposit, meaning that the goods
- have not passed customs.
- """
- invoice_lines_tags = invoice.line_ids.tax_tag_ids
- it_tax_report_vj3_lines = self.env['account.report.line'].search([
- ('report_id.country_id.code', '=', 'IT'),
- ('code', '=', 'VJ3'),
- ])
- vj3_lines_tags = it_tax_report_vj3_lines.expression_ids._get_matching_tags()
- return bool(invoice_lines_tags & vj3_lines_tags)
-
- def _l10n_it_document_type_mapping(self):
- """ Returns a dictionary with the required features for every TDxx FatturaPA document type """
- return {
- 'TD01': dict(move_types=['out_invoice'], import_type='in_invoice', self_invoice=False, simplified=False, downpayment=False),
- 'TD02': dict(move_types=['out_invoice'], import_type='in_invoice', self_invoice=False, simplified=False, downpayment=True),
- 'TD04': dict(move_types=['out_refund'], import_type='in_refund', self_invoice=False, simplified=False),
- 'TD07': dict(move_types=['out_invoice'], import_type='in_invoice', self_invoice=False, simplified=True),
- 'TD08': dict(move_types=['out_refund'], import_type='in_refund', self_invoice=False, simplified=True),
- 'TD09': dict(move_types=['out_invoice'], import_type='in_invoice', self_invoice=False, simplified=True),
- 'TD28': dict(move_types=['in_invoice', 'in_refund'], import_type='in_invoice', simplified=False, self_invoice=True, partner_country_code="SM"),
- 'TD17': dict(move_types=['in_invoice', 'in_refund'], import_type='in_invoice', simplified=False, self_invoice=True, services_or_goods="service"),
- 'TD18': dict(move_types=['in_invoice', 'in_refund'], import_type='in_invoice', simplified=False, self_invoice=True, services_or_goods="consu", goods_in_italy=False, partner_in_eu=True),
- 'TD19': dict(move_types=['in_invoice', 'in_refund'], import_type='in_invoice', simplified=False, self_invoice=True, services_or_goods="consu", goods_in_italy=True),
- }
-
- def _l10n_it_get_invoice_features_for_document_type_selection(self, invoice):
- """ Returns a dictionary of features to be compared with the TDxx FatturaPA
- document type requirements. """
- services_or_goods = self._l10n_it_edi_services_or_goods(invoice)
- partner_values = invoice.commercial_partner_id._l10n_it_edi_get_values()
- return {
- 'move_types': invoice.move_type,
- 'partner_in_eu': partner_values.get('in_eu', False),
- 'partner_country_code': partner_values.get('country_code', False),
- 'simplified': self._l10n_it_edi_is_simplified(invoice),
- 'self_invoice': self._l10n_it_edi_is_self_invoice(invoice),
- 'downpayment': invoice._is_downpayment(),
- 'services_or_goods': services_or_goods,
- 'goods_in_italy': services_or_goods == 'consu' and self._l10n_it_goods_in_italy(invoice),
- }
-
- def _l10n_it_get_document_type(self, invoice):
- """ Compare the features of the invoice to the requirements of each TDxx FatturaPA
- document type until you find a valid one. """
- invoice_features = self._l10n_it_get_invoice_features_for_document_type_selection(invoice)
- for code, document_type_features in self._l10n_it_document_type_mapping().items():
- comparisons = []
- for key, invoice_feature in invoice_features.items():
- if key not in document_type_features:
- continue
- document_type_feature = document_type_features.get(key)
- if isinstance(document_type_feature, (tuple, list)):
- comparisons.append(invoice_feature in document_type_feature)
- else:
- comparisons.append(invoice_feature == document_type_feature)
- if all(comparisons):
- return code
- return False
-
- def _l10n_it_is_simplified_document_type(self, document_type):
- return self._l10n_it_document_type_mapping().get(document_type, {}).get('simplified', False)
-
- # -------------------------------------------------------------------------
- # Import
- # -------------------------------------------------------------------------
-
- def _cron_receive_fattura_pa(self):
- ''' Check the proxy for incoming invoices for all companies.
- '''
- for proxy_user in self.env['account_edi_proxy_client.user'].search([
- ('proxy_type', '=', 'l10n_it_edi'),
- ('edi_mode', '!=', 'demo'),
- ]):
- self._receive_fattura_pa(proxy_user)
-
- def _receive_fattura_pa(self, proxy_user):
- ''' Check the proxy for incoming invoices for a specified proxy user.
- '''
- try:
- res = proxy_user._make_request(
- proxy_user._get_server_url() + '/api/l10n_it_edi/1/in/RicezioneInvoice',
- params={'recipient_codice_fiscale': proxy_user.company_id.l10n_it_codice_fiscale})
- except AccountEdiProxyError as e:
- res = {}
- _logger.warning('Error while receiving file from SdiCoop: %s', e)
-
- retrigger = False
- proxy_acks = []
- for id_transaction, fattura in res.items():
-
- # The server has a maximum number of documents it can send at a time
- # If that maximum is reached, then we search for more
- # by re-triggering the download cron, avoiding the timeout.
- current_num, max_num = fattura.get('current_num', 0), fattura.get('max_num', 0)
- retrigger = retrigger or current_num == max_num > 0
-
- if self._save_incoming_attachment_fattura_pa(proxy_user, id_transaction, fattura['filename'], fattura['file'], fattura['key']):
- proxy_acks.append(id_transaction)
-
- if proxy_acks:
- try:
- proxy_user._make_request(
- proxy_user._get_server_url() + '/api/l10n_it_edi/1/ack',
- params={'transaction_ids': proxy_acks})
- except AccountEdiProxyError as e:
- _logger.warning('Error while receiving file from SdiCoop: %s', e)
-
- if retrigger:
- _logger.info('Retriggering "Receive invoices from the exchange system"...')
- self.env.ref('l10n_it_edi.ir_cron_receive_fattura_pa_invoice')._trigger()
-
- def _save_incoming_attachment_fattura_pa(self, proxy_user, id_transaction, filename, content, key):
- ''' Save an incoming file from the SdI as an attachment.
- Commits if successful.
-
- :param proxy_user: the user that saves the attachment.
- :param id_transaction: id of the SdI transaction for communication with the IAP proxy.
- :param filename: name of the file to be saved.
- :param content: encrypted content of the file to be saved.
- :param key: key to decrypt the file.
- :returns: True if everything went well, or the file already exists.
- False if the file cannot be parsed as an XML.
- '''
-
- if self.env['ir.attachment'].search([('name', '=', filename), ('res_model', '=', 'account.move')], limit=1):
- # name should be unique, the invoice already exists
- _logger.info('E-invoice already exists: %s', filename)
- return True
-
- decrypted_content = proxy_user._decrypt_data(content, key)
- attachment = self.env['ir.attachment'].create({
- 'name': filename,
- 'raw': decrypted_content,
- 'type': 'binary'
- })
-
- # Import the move from the attachment.
- # `_create_document_from_attachment` will create an empty move
- # then try and fill it with the content imported from the attachment.
- # Should the import fail, thanks to try..except and savepoint,
- # we will anyway end up with an empty `in_invoice` with the attachment posted on it.
- (self.env['account.journal'].with_company(proxy_user.company_id)
- .with_context(default_move_type='in_invoice')
- ._create_document_from_attachment(attachment.ids))
-
- # Commit the created move, be it correctly imported or empty.
- self.env.cr.commit()
-
- return True
-
- def _check_filename_is_fattura_pa(self, filename):
- return re.search("[A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.((?i:xml.p7m|xml))", filename)
-
- def _is_fattura_pa(self, filename, tree=None):
- return self.code == 'fattura_pa' and self._check_filename_is_fattura_pa(filename)
-
- def _l10n_it_get_partner_invoice(self, tree, company):
- # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2>
- elements = tree.xpath('//CedentePrestatore//IdCodice')
- partner = elements and self.env['res.partner'].search([
- *self.env['res.partner']._check_company_domain(company),
- ('vat', 'ilike', elements[0].text)
- ], limit=1)
- if not partner:
- elements = tree.xpath('//CedentePrestatore//CodiceFiscale')
- if elements:
- codice = elements[0].text
- domains = [[('l10n_it_codice_fiscale', '=', codice)]]
- if re.match(r'^[0-9]{11}$', codice):
- domains.append([('l10n_it_codice_fiscale', '=', 'IT' + codice)])
- elif re.match(r'^IT[0-9]{11}$', codice):
- domains.append([('l10n_it_codice_fiscale', '=',
- self.env['res.partner']._l10n_it_edi_normalized_codice_fiscale(codice))])
- partner = elements and self.env['res.partner'].search(AND([
- *self.env['res.partner']._check_company_domain(company),
- OR(domains)
- ]), limit=1)
- if not partner:
- elements = tree.xpath('//DatiTrasmissione//Email')
- partner = elements and self.env['res.partner'].search([
- *self.env['res.partner']._check_company_domain(company),
- '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text)
- ], limit=1)
-
- return partner
-
- def _convert_date_from_xml(self, xsdate_str):
- """ Dates in FatturaPA are ISO 8601 date format, pattern '[-]CCYY-MM-DD[Z|(+|-)hh:mm]' """
- xsdate_str = xsdate_str.strip()
- xsdate_pattern = r"^-?(?P-?\d{4}-\d{2}-\d{2})(?P[zZ]|[+-]\d{2}:\d{2})?$"
- try:
- match = re.match(xsdate_pattern, xsdate_str)
- converted_date = datetime.strptime(match.group("date"), DEFAULT_FACTUR_ITALIAN_DATE_FORMAT).date()
- except Exception:
- converted_date = False
- return converted_date
-
- def _l10n_it_edi_search_tax_for_import(self, company, percentage, extra_domain=None):
- """ Returns the VAT, Withholding or Pension Fund tax that suits the conditions given
- and matches the percentage found in the XML for the company. """
- domain = [
- *self.env['account.tax']._check_company_domain(company),
- ('amount', '=', percentage),
- ('amount_type', '=', 'percent'),
- ('type_tax_use', '=', 'purchase'),
- ] + (extra_domain or [])
-
- # As we're importing vendor bills, we're excluding Reverse Charge Taxes
- # which have a [100.0, 100.0, -100.0] repartition lines factor_percent distribution.
- # We only allow for taxes that have all positive repartition lines factor_percent distribution.
- taxes = self.env['account.tax'].search(domain).filtered(
- lambda tax: all([rep_line.factor_percent >= 0 for rep_line in tax.invoice_repartition_line_ids]))
-
- return taxes[0] if taxes else taxes
-
- def _l10n_it_edi_get_extra_info(self, company, document_type, body_tree):
- """ This function is meant to collect other information that has to be inserted on the invoice lines by submodules.
- :return extra_info, messages_to_log"""
- return {'simplified': self._l10n_it_is_simplified_document_type(document_type)}, []
-
- def _import_fattura_pa(self, tree, invoice):
- """ Decodes a fattura_pa invoice into an invoice.
-
- :param tree: the fattura_pa tree to decode.
- :param invoice: the invoice to update or an empty recordset.
- :returns: the invoice where the fattura_pa data was imported.
- """
- company = invoice.company_id
-
- # Refund type.
- # TD01 == invoice
- # TD02 == advance/down payment on invoice
- # TD03 == advance/down payment on fee
- # TD04 == credit note
- # TD05 == debit note
- # TD06 == fee
- # TD07 == simplified invoice
- # TD08 == simplified credit note
- # TD09 == simplified debit note
- # For unsupported document types, just assume in_invoice, and log that the type is unsupported
- elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
- document_type = elements[0].text if elements else ''
- move_type = self._l10n_it_document_type_mapping().get(document_type, {}).get('import_type')
- if not move_type:
- move_type = "in_invoice"
- _logger.info('Document type not managed: %s. Invoice type is set by default.', document_type)
-
- invoice.move_type = move_type
-
- # Collect extra info from the XML that may be used by submodules to further put information on the invoice lines
- extra_info, message_to_log = self._l10n_it_edi_get_extra_info(company, document_type, tree)
-
- partner = self._l10n_it_get_partner_invoice(tree, company)
- if partner:
- invoice.partner_id = partner
- else:
- message_to_log.append("%s %s" % (
- _("Vendor not found, useful informations from XML file:"),
- invoice._compose_info_message(tree, './/CedentePrestatore')))
-
- # Numbering attributed by the transmitter. <1.1.2>
- elements = tree.xpath('//ProgressivoInvio')
- if elements:
- invoice.payment_reference = elements[0].text
-
- elements = tree.xpath('.//DatiGeneraliDocumento//Numero')
- if elements:
- invoice.ref = elements[0].text
-
- # Currency. <2.1.1.2>
- elements = tree.xpath('.//DatiGeneraliDocumento/Divisa')
- if elements:
- currency_str = elements[0].text
- currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False)
- if currency != self.env.company.currency_id and currency.active:
- invoice.currency_id = currency
-
- # Date. <2.1.1.3>
- elements = tree.xpath('.//DatiGeneraliDocumento/Data')
- if elements:
- document_date = self._convert_date_from_xml(elements[0].text)
- if document_date:
- invoice.invoice_date = document_date
- else:
- message_to_log.append("%s %s" % (
- _("Document date invalid in XML file:"),
- invoice._compose_info_message(elements[0], '.')
- ))
-
- # Dati Bollo. <2.1.1.6>
- elements = tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo')
- if elements:
- invoice.l10n_it_stamp_duty = float(elements[0].text)
-
-
- # Comment. <2.1.1.11>
- elements = tree.xpath('.//DatiGeneraliDocumento//Causale')
- for element in elements:
- invoice.narration = '%s%s ' % (invoice.narration or '', element.text)
-
- # Informations relative to the purchase order, the contract, the agreement,
- # the reception phase or invoices previously transmitted
- # <2.1.2> - <2.1.6>
- for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']:
- elements = tree.xpath('.//DatiGenerali/' + document_type)
- if elements:
- for element in elements:
- message_to_log.append("%s %s %s" % (document_type, _("from XML file:"),
- invoice._compose_info_message(element, '.')))
-
- # Dati DDT. <2.1.8>
- elements = tree.xpath('.//DatiGenerali/DatiDDT')
- if elements:
- message_to_log.append("%s %s" % (
- _("Transport informations from XML file:"),
- invoice._compose_info_message(tree, './/DatiGenerali/DatiDDT')))
-
- # Due date. <2.4.2.5>
- elements = tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento')
- if elements:
- date_str = elements[0].text.strip()
- if date_str:
- due_date = self._convert_date_from_xml(date_str)
- if due_date:
- invoice.invoice_date_due = fields.Date.to_string(due_date)
- else:
- message_to_log.append("%s %s" % (
- _("Payment due date invalid in XML file:"),
- invoice._compose_info_message(elements[0], '.')
- ))
-
- # Information related to the purchase order <2.1.2>
- po_refs = []
- elements = tree.xpath('//DatiGenerali/DatiOrdineAcquisto/IdDocumento')
- if elements:
- po_refs = [element.text.strip() for element in elements]
- invoice.invoice_origin = ", ".join(po_refs)
-
- # Total amount. <2.4.2.6>
- elements = tree.xpath('.//ImportoPagamento')
- amount_total_import = 0
- for element in elements:
- amount_total_import += float(element.text)
- if amount_total_import:
- message_to_log.append(_("Total amount from the XML File: %s") % (
- amount_total_import))
-
- # Bank account. <2.4.2.13>
- if invoice.move_type not in ('out_invoice', 'in_refund'):
- elements = tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN')
- if elements:
- if invoice.partner_id and invoice.partner_id.commercial_partner_id:
- bank = self.env['res.partner.bank'].search([
- ('acc_number', '=', elements[0].text),
- ('partner_id', '=', invoice.partner_id.commercial_partner_id.id),
- ('company_id', 'in', [invoice.company_id.id, False])
- ], order='company_id', limit=1)
- else:
- bank = self.env['res.partner.bank'].search([
- ('acc_number', '=', elements[0].text), ('company_id', 'in', [invoice.company_id.id, False])
- ], order='company_id', limit=1)
- if bank:
- invoice.partner_bank_id = bank
- else:
- message_to_log.append("%s %s" % (
- _("Bank account not found, useful informations from XML file:"),
- invoice._compose_multi_info_message(
- tree, ['.//DatiPagamento//Beneficiario',
- './/DatiPagamento//IstitutoFinanziario',
- './/DatiPagamento//IBAN',
- './/DatiPagamento//ABI',
- './/DatiPagamento//CAB',
- './/DatiPagamento//BIC',
- './/DatiPagamento//ModalitaPagamento'])))
- else:
- elements = tree.xpath('.//DatiPagamento/DettaglioPagamento')
- if elements:
- message_to_log.append("%s %s" % (
- _("Bank account not found, useful informations from XML file:"),
- invoice._compose_info_message(tree, './/DatiPagamento')))
-
- # Invoice lines. <2.2.1>
- if not extra_info['simplified']:
- elements = tree.xpath('.//DettaglioLinee')
- else:
- elements = tree.xpath('.//DatiBeniServizi')
-
- for element in (elements or []):
- invoice_line_form = invoice.invoice_line_ids.create({
- 'move_id': invoice.id,
- 'tax_ids': [fields.Command.clear()],
- })
- if invoice_line_form:
- message_to_log += self._import_fattura_pa_line(element, invoice_line_form, extra_info)
-
- # Global discount summarized in 1 amount
- discount_elements = tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione')
- if discount_elements:
- taxable_amount = float(invoice.tax_totals['amount_untaxed'])
- discounted_amount = taxable_amount
- for discount_element in discount_elements:
- discount_type = discount_element.xpath('.//Tipo')
- discount_sign = 1
- if discount_type and discount_type[0].text == 'MG':
- discount_sign = -1
- discount_amount = discount_element.xpath('.//Importo')
- if discount_amount:
- discounted_amount -= discount_sign * float(discount_amount[0].text)
- continue
- discount_percentage = discount_element.xpath('.//Percentuale')
- if discount_percentage:
- discounted_amount *= 1 - discount_sign * float(discount_percentage[0].text) / 100
-
- general_discount = discounted_amount - taxable_amount
- sequence = len(elements) + 1
-
- invoice.invoice_line_ids = [Command.create({
- 'sequence': sequence,
- 'name': 'SCONTO' if general_discount < 0 else 'MAGGIORAZIONE',
- 'price_unit': general_discount,
- })]
-
- elements = tree.xpath('.//Allegati')
- if elements:
- for element in elements:
- name_attachment = element.xpath('.//NomeAttachment')[0].text
- attachment_64 = str.encode(element.xpath('.//Attachment')[0].text)
- attachment_64 = self.env['ir.attachment'].create({
- 'name': name_attachment,
- 'datas': attachment_64,
- 'type': 'binary',
- 'res_model': 'account.move',
- 'res_id': invoice.id,
- })
-
- # no_new_invoice to prevent from looping on the message_post that would create a new invoice without it
- invoice.with_context(no_new_invoice=True).message_post(
- body=(_("Attachment from XML")),
- attachment_ids=[attachment_64.id]
- )
- for message in message_to_log:
- invoice.message_post(body=message)
-
- def _import_fattura_pa_line(self, element, invoice_line_form, extra_info=None):
- extra_info = extra_info or {}
- company = invoice_line_form.company_id
- partner = invoice_line_form.partner_id
- message_to_log = []
-
- # Sequence.
- line_elements = element.xpath('.//NumeroLinea')
- if line_elements:
- invoice_line_form.sequence = int(line_elements[0].text)
-
- # Product.
- elements_code = element.xpath('.//CodiceArticolo')
- if elements_code:
- for element_code in elements_code:
- type_code = element_code.xpath('.//CodiceTipo')[0]
- code = element_code.xpath('.//CodiceValore')[0]
- if type_code.text == 'EAN':
- product = self.env['product.product'].search([('barcode', '=', code.text)])
- if product:
- invoice_line_form.product_id = product
- break
- if partner:
- product_supplier = self.env['product.supplierinfo'].search([('partner_id', '=', partner.id), ('product_code', '=', code.text)], limit=2)
- if product_supplier and len(product_supplier) == 1 and product_supplier.product_id:
- invoice_line_form.product_id = product_supplier.product_id
- break
- if not invoice_line_form.product_id:
- for element_code in elements_code:
- code = element_code.xpath('.//CodiceValore')[0]
- product = self.env['product.product'].search([('default_code', '=', code.text)], limit=2)
- if product and len(product) == 1:
- invoice_line_form.product_id = product
- break
-
- # Label.
- line_elements = element.xpath('.//Descrizione')
- if line_elements:
- invoice_line_form.name = " ".join(line_elements[0].text.split())
-
- # Quantity.
- line_elements = element.xpath('.//Quantita')
- if line_elements:
- invoice_line_form.quantity = float(line_elements[0].text)
- else:
- invoice_line_form.quantity = 1
-
- # Taxes
- percentage = None
- price_subtotal = 0
- if not extra_info['simplified']:
- tax_element = element.xpath('.//AliquotaIVA')
- if tax_element and tax_element[0].text:
- percentage = float(tax_element[0].text)
- else:
- amount_element = element.xpath('.//Importo')
- if amount_element and amount_element[0].text:
- amount = float(amount_element[0].text)
- tax_element = element.xpath('.//Aliquota')
- if tax_element and tax_element[0].text:
- percentage = float(tax_element[0].text)
- price_subtotal = amount / (1 + percentage / 100)
- else:
- tax_element = element.xpath('.//Imposta')
- if tax_element and tax_element[0].text:
- tax_amount = float(tax_element[0].text)
- price_subtotal = amount - tax_amount
- percentage = round(tax_amount / price_subtotal * 100)
-
- natura_element = element.xpath('.//Natura')
- invoice_line_form.tax_ids = []
- if percentage is not None:
- l10n_it_kind_exoneration = bool(natura_element) and natura_element[0].text
- conditions = (
- l10n_it_kind_exoneration and [('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration)]
- or [('l10n_it_has_exoneration', '=', False)]
- )
- tax = self._l10n_it_edi_search_tax_for_import(company, percentage, conditions)
- if tax:
- invoice_line_form.tax_ids += tax
- else:
- message_to_log.append("%s %s" % (
- _("Tax not found for line with description '%s'", invoice_line_form.name),
- self.env['account.move']._compose_info_message(element, '.'),
- ))
-
- # Price Unit.
- if not extra_info['simplified']:
- line_elements = element.xpath('.//PrezzoUnitario')
- if line_elements:
- invoice_line_form.price_unit = float(line_elements[0].text)
- else:
- invoice_line_form.price_unit = price_subtotal
-
- # Discounts
- discount_elements = element.xpath('.//ScontoMaggiorazione')
- if discount_elements:
- discount_element = discount_elements[0]
- discount_percentage = discount_element.xpath('.//Percentuale')
- # Special case of only 1 percentage discount
- if discount_percentage and len(discount_elements) == 1:
- discount_type = discount_element.xpath('.//Tipo')
- discount_sign = 1
- if discount_type and discount_type[0].text == 'MG':
- discount_sign = -1
- invoice_line_form.discount = discount_sign * float(discount_percentage[0].text)
- # Discounts in cascade summarized in 1 percentage
- else:
- total = float(element.xpath('.//PrezzoTotale')[0].text)
- discount = 100 - (100 * total) / (invoice_line_form.quantity * invoice_line_form.price_unit)
- invoice_line_form.discount = discount
-
- return message_to_log
-
- # -------------------------------------------------------------------------
- # Export
- # -------------------------------------------------------------------------
-
- def _prepare_invoice_report(self, pdf_writer, edi_document):
- self.ensure_one()
- if self.code != 'fattura_pa':
- return super()._prepare_invoice_report(pdf_writer, edi_document)
- attachment = edi_document.sudo().attachment_id
- if attachment:
- pdf_writer.embed_odoo_attachment(attachment)
-
- def _is_compatible_with_journal(self, journal):
- # OVERRIDE
- self.ensure_one()
- if self.code != 'fattura_pa':
- return super()._is_compatible_with_journal(journal)
- return journal.type in ('sale', 'purchase') and journal.country_code == 'IT'
-
- def _get_move_applicability(self, move):
- # OVERRIDE
- self.ensure_one()
- if self.code != 'fattura_pa':
- return super()._get_move_applicability(move)
-
- is_it_purchase_document = self._l10n_it_edi_is_self_invoice(move) and move.is_purchase_document()
- if move.country_code == 'IT' and (move.is_sale_document() or is_it_purchase_document):
- return {
- 'post': self._post_fattura_pa,
- 'post_batching': lambda move: (move.move_type, bool(move.l10n_it_edi_transaction)),
- }
-
- def _l10n_it_edi_export_invoice_as_xml(self, invoice):
- ''' Create the xml file content.
- :return: The XML content as str.
- '''
- template_values = invoice._prepare_fatturapa_export_values()
- if not self._l10n_it_is_simplified_document_type(template_values['document_type']):
- content = self.env['ir.qweb']._render('l10n_it_edi.account_invoice_it_FatturaPA_export', template_values)
- else:
- content = self.env['ir.qweb']._render('l10n_it_edi.account_invoice_it_simplified_FatturaPA_export', template_values)
- invoice.message_post(body=_(
- "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."
- ))
- return content
-
- def _check_move_configuration(self, move):
- # OVERRIDE
- res = super()._check_move_configuration(move)
- if self.code != 'fattura_pa':
- return res
-
- res.extend(self._l10n_it_edi_check_invoice_configuration(move))
-
- if not self.env['account_edi_proxy_client.user']._get_proxy_users(move.company_id, 'l10n_it_edi'):
- res.append(_("You must accept the terms and conditions in the settings to use FatturaPA."))
-
- return res
-
- def _needs_web_services(self):
- self.ensure_one()
- return self.code == 'fattura_pa' or super()._needs_web_services()
-
- def _l10n_it_post_invoices_step_1(self, invoices):
- ''' Send the invoices to the proxy.
- '''
- to_return = {}
-
- to_send = {}
- for invoice in invoices:
- xml = "" + str(self._l10n_it_edi_export_invoice_as_xml(invoice))
- filename = self._l10n_it_edi_generate_electronic_invoice_filename(invoice)
- attachment = self.env['ir.attachment'].create({
- 'name': filename,
- 'res_id': invoice.id,
- 'res_model': invoice._name,
- 'raw': xml.encode(),
- 'description': _('Italian invoice: %s', invoice.move_type),
- 'type': 'binary',
- })
- invoice.l10n_it_edi_attachment_id = attachment
-
- if invoice._is_commercial_partner_pa():
- invoice.message_post(
- body=(_("Invoices for PA are not managed by Odoo, you can download the document and send it on your own."))
- )
- to_return[invoice] = {'attachment': attachment, 'success': True}
- else:
- to_send[filename] = {
- 'invoice': invoice,
- 'data': {'filename': filename, 'xml': base64.b64encode(xml.encode()).decode()}}
-
- company = invoices.company_id
- proxy_user = self.env['account_edi_proxy_client.user']._get_proxy_users(company, 'l10n_it_edi')
- if not proxy_user: # proxy user should exist, because there is a check in _check_move_configuration
- return {invoice: {
- 'error': _("You must accept the terms and conditions in the settings to use FatturaPA."),
- 'blocking_level': 'error'} for invoice in invoices}
-
- responses = {}
- if proxy_user.edi_mode == 'demo':
- responses = {i['data']['filename']: {'id_transaction': 'demo'} for i in to_send.values()}
- else:
- try:
- responses = self._l10n_it_edi_upload([i['data'] for i in to_send.values()], proxy_user)
- except AccountEdiProxyError as e:
- return {invoice: {'error': e.message, 'blocking_level': 'error'} for invoice in invoices}
-
- for filename, response in responses.items():
- invoice = to_send[filename]['invoice']
- to_return[invoice] = response
- if 'id_transaction' in response:
- invoice.l10n_it_edi_transaction = response['id_transaction']
- to_return[invoice].update({
- 'error': _('The invoice was sent to FatturaPA, but we are still awaiting a response. Click the link above to check for an update.'),
- 'blocking_level': 'info'})
- return to_return
-
- def _l10n_it_post_invoices_step_2(self, invoices):
- ''' Check if the sent invoices have been processed by FatturaPA.
- '''
- to_check = {i.l10n_it_edi_transaction: i for i in invoices}
- to_return = {}
- company = invoices.company_id
-
- proxy_user = self.env['account_edi_proxy_client.user']._get_proxy_users(company, 'l10n_it_edi')
- if not proxy_user: # proxy user should exist, because there is a check in _check_move_configuration
- return {invoice: {
- 'error': _("You must accept the terms and conditions in the settings to use FatturaPA."),
- 'blocking_level': 'error'} for invoice in invoices}
-
- if proxy_user.edi_mode == 'demo':
- # simulate success and bypass ack
- return {invoice: {'attachment': invoice.l10n_it_edi_attachment_id} for invoice in invoices}
- else:
- try:
- responses = proxy_user._make_request(proxy_user._get_server_url() + '/api/l10n_it_edi/1/in/TrasmissioneFatture',
- params={'ids_transaction': list(to_check.keys())})
- except AccountEdiProxyError as e:
- return {invoice: {'error': e.message, 'blocking_level': 'error'} for invoice in invoices}
-
- proxy_acks = []
- for id_transaction, response in responses.items():
- invoice = to_check[id_transaction]
- if 'error' in response:
- to_return[invoice] = response
- continue
-
- state = response['state']
- if state == 'awaiting_outcome':
- to_return[invoice] = {
- 'error': _('The invoice was sent to FatturaPA, but we are still awaiting a response. Click the link above to check for an update.'),
- 'blocking_level': 'info'}
-
- elif state == 'not_found':
- # Invoice does not exist on proxy. Either it does not belong to this proxy_user or it was not created correctly when
- # it was sent to the proxy.
- to_return[invoice] = {'error': _('You are not allowed to check the status of this invoice.'), 'blocking_level': 'error'}
-
- elif state == 'ricevutaConsegna':
- if invoice._is_commercial_partner_pa():
- to_return[invoice] = {'error': _('The invoice has been succesfully transmitted. The addressee has 15 days to accept or reject it.')}
- else:
- to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id, 'success': True}
- proxy_acks.append(id_transaction)
-
- elif state == 'notificaMancataConsegna':
- if invoice._is_commercial_partner_pa():
- to_return[invoice] = {'error': _(
- '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.')}
- else:
- to_return[invoice] = {'success': True, 'attachment': invoice.l10n_it_edi_attachment_id}
- invoice._message_log(body=_(
- '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.'))
- proxy_acks.append(id_transaction)
-
- elif state == 'NotificaDecorrenzaTermini':
- # This condition is part of the Public Administration flow
- invoice._message_log(body=_(
- '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.'))
- to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id, 'success': True}
- proxy_acks.append(id_transaction)
-
- # In the transaction states above, we don't need to read the attachment.
- # In the following cases instead we need to read the information inside
- # about the notification itself, i.e. the error message in case of rejection.
- else:
- attachment_file = response.get('file')
- if not attachment_file: # It means there is no status update, so we can skip it
- document = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'fattura_pa')
- to_return[invoice] = {'error': document.error, 'blocking_level': document.blocking_level}
- continue
-
- xml = proxy_user._decrypt_data(attachment_file, response['key'])
- response_tree = etree.fromstring(xml)
-
- if state == 'notificaScarto':
- elements = response_tree.xpath('//Errore')
- error_codes = [element.find('Codice').text for element in elements]
- errors = [element.find('Descrizione').text for element in elements]
- # Duplicated invoice
- if '00404' in error_codes:
- idx = error_codes.index('00404')
- invoice.message_post(body=_(
- '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', errors[idx]))
- to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id, 'success': True}
- else:
- # Add helpful text if duplicated filename error
- if '00002' in error_codes:
- idx = error_codes.index('00002')
- errors[idx] = _(
- 'The filename is duplicated. Try again (or adjust the FatturaPA Filename sequence).'
- ' Original message from the SDI: %s', [errors[idx]]
- )
- to_return[invoice] = {'error': self._format_error_message(_('The invoice has been refused by the Exchange System'), errors), 'blocking_level': 'error'}
- invoice.l10n_it_edi_transaction = False
- proxy_acks.append(id_transaction)
-
- elif state == 'notificaEsito':
- outcome = response_tree.find('Esito').text
- if outcome == 'EC01':
- to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id, 'success': True}
- else: # ECO2
- to_return[invoice] = {'error': _('The invoice was refused by the addressee.'), 'blocking_level': 'error'}
- proxy_acks.append(id_transaction)
-
- if proxy_acks:
- try:
- proxy_user._make_request(proxy_user._get_server_url() + '/api/l10n_it_edi/1/ack',
- params={'transaction_ids': proxy_acks})
- except AccountEdiProxyError as e:
- # Will be ignored and acked again next time.
- _logger.error('Error while acking file to SdiCoop: %s', e)
-
- return to_return
-
- def _post_fattura_pa(self, invoice):
- if not invoice[0].l10n_it_edi_transaction:
- return self._l10n_it_post_invoices_step_1(invoice)
- else:
- return self._l10n_it_post_invoices_step_2(invoice)
-
- def _post_invoice_edi(self, invoices):
- # OVERRIDE
- self.ensure_one()
- edi_result = super()._post_invoice_edi(invoices)
- if self.code != 'fattura_pa':
- return edi_result
-
- return self._post_fattura_pa(invoices)
-
- # -------------------------------------------------------------------------
- # Proxy methods
- # -------------------------------------------------------------------------
- def _get_proxy_identification(self, company):
- 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)
-
- def _l10n_it_edi_upload(self, files, proxy_user):
- '''Upload files to fatturapa.
-
- :param files: A list of dictionary {filename, base64_xml}.
- :returns: A dictionary.
- * message: Message from fatturapa.
- * transactionId: The fatturapa ID of this request.
- * error: An eventual error.
- * error_level: Info, warning, error.
- '''
- ERRORS = {
- 'EI01': {'error': _lt('Attached file is empty'), 'blocking_level': 'error'},
- 'EI02': {'error': _lt('Service momentarily unavailable'), 'blocking_level': 'warning'},
- 'EI03': {'error': _lt('Unauthorized user'), 'blocking_level': 'error'},
- }
-
- if not files:
- return {}
-
- result = proxy_user._make_request(proxy_user._get_server_url() + '/api/l10n_it_edi/1/out/SdiRiceviFile', params={'files': files})
-
- # Translate the errors.
- for filename in result.keys():
- if 'error' in result[filename]:
- result[filename] = ERRORS.get(result[filename]['error'], {'error': result[filename]['error'], 'blocking_level': 'error'})
-
- return result
diff --git a/addons/l10n_it_edi/models/account_edi_proxy_user.py b/addons/l10n_it_edi/models/account_edi_proxy_user.py
index dbeef9059a7..95cf6016b00 100644
--- a/addons/l10n_it_edi/models/account_edi_proxy_user.py
+++ b/addons/l10n_it_edi/models/account_edi_proxy_user.py
@@ -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)
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py
deleted file mode 100644
index 69b39c8b2e2..00000000000
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ /dev/null
@@ -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 = "" + 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 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''"
- 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 += "
"
- for line in element.iter():
- if line.text:
- text = " ".join(line.text.split())
- if text:
- output_str += "
"
-
- 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 += "
%s: %s
" % (element.tag, text)
- return output_str + "
"
-
- # -------------------------------------------------------------------------
- # 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)
diff --git a/addons/l10n_it_edi/models/account_move.py b/addons/l10n_it_edi/models/account_move.py
new file mode 100644
index 00000000000..ed107069206
--- /dev/null
+++ b/addons/l10n_it_edi/models/account_move.py
@@ -0,0 +1,1479 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from base64 import b64encode
+from datetime import datetime
+import logging
+from lxml import etree
+from markupsafe import escape
+import uuid
+
+from odoo import _, api, Command, fields, models
+from odoo.addons.base.models.ir_qweb_fields import Markup, nl2br, nl2br_enclose
+from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError
+from odoo.exceptions import UserError
+from odoo.tools import float_compare, float_repr, cleanup_xml_node
+
+_logger = logging.getLogger(__name__)
+
+
+WAITING_STATES = ('processing', 'forward_attempt')
+
+
+# -------------------------------------------------------------------------
+# XML tool functions
+# -------------------------------------------------------------------------
+
+def get_text(tree, xpath, many=False):
+ texts = [el.text.strip() for el in tree.xpath(xpath) if el.text]
+ return texts if many else texts[0] if texts else ''
+
+def get_float(tree, xpath):
+ try:
+ return float(get_text(tree, xpath))
+ except ValueError:
+ return 0.0
+
+def get_date(tree, xpath):
+ """ Dates in FatturaPA are ISO 8601 date format, pattern '[-]CCYY-MM-DD[Z|(+|-)hh:mm]' """
+ dt = get_datetime(tree, xpath)
+ return dt.date() if dt else False
+
+def get_datetime(tree, xpath):
+ """ Datetimes in FatturaPA are ISO 8601 date format, pattern '[-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]'
+ Python 3.7 -> 3.11 doesn't support 'Z'.
+ """
+ if (datetime_str := get_text(tree, xpath)):
+ try:
+ return datetime.fromisoformat(datetime_str.replace('Z', '+00:00'))
+ except (ValueError, TypeError):
+ return False
+ return False
+
+
+class AccountMove(models.Model):
+ _inherit = 'account.move'
+
+ l10n_it_edi_state = fields.Selection(
+ string="SDI State",
+ selection=[
+ ('being_sent', 'Being Sent To SdI'),
+ ('requires_user_signature', 'Requires user signature'),
+ ('processing', 'SdI Processing'),
+ ('rejected', 'SdI Rejected'),
+ ('forwarded', 'SdI Accepted, Forwarded to Partner'),
+ ('forward_failed', 'SdI Accepted, Forward to Partner Failed'),
+ ('forward_attempt', 'SdI Accepted, Forwarding to Partner'),
+ ('accepted_by_pa_partner', 'SdI Accepted, Accepted by the PA Partner'),
+ ('rejected_by_pa_partner', 'SdI Accepted, Rejected by the PA Partner'),
+ ('accepted_by_pa_partner_after_expiry', 'SdI Accepted, PA Partner Expired Terms'),
+ ],
+ copy=False, tracking=True,
+ help="This state is updated by default, but you can force the value. ",
+ )
+ l10n_it_edi_header = fields.Html(
+ help='User description of the current state, with hints to make the flow progress',
+ readonly=True,
+ copy=False,
+ )
+ l10n_it_edi_transaction = fields.Char(copy=False, string="FatturaPA Transaction")
+ l10n_it_edi_attachment_file = fields.Binary(copy=False, attachment=True)
+ l10n_it_edi_attachment_id = fields.Many2one(
+ comodel_name='ir.attachment',
+ string="FatturaPA Attachment",
+ compute=lambda self: self._compute_linked_attachment_id('l10n_it_edi_attachment_id', 'l10n_it_edi_attachment_file'),
+ depends=['l10n_it_edi_attachment_file'],
+ )
+ l10n_it_edi_is_self_invoice = fields.Boolean(compute="_compute_l10n_it_edi_is_self_invoice")
+ l10n_it_stamp_duty = fields.Float(default=0, string="Dati Bollo")
+ l10n_it_ddt_id = fields.Many2one('l10n_it.ddt', string='DDT', copy=False)
+
+ # -------------------------------------------------------------------------
+ # Computes
+ # -------------------------------------------------------------------------
+
+ @api.depends('move_type', 'line_ids.tax_tag_ids')
+ def _compute_l10n_it_edi_is_self_invoice(self):
+ """
+ Italian EDI requires Vendor bills coming from EU countries to be sent as self-invoices.
+ We recognize these cases based on the taxes that target the VJ tax grids, which imply
+ the use of VAT External Reverse Charge.
+ """
+ it_tax_report_vj_lines = self.env['account.report.line'].search([
+ ('report_id.country_id.code', '=', 'IT'),
+ ('code', 'like', 'VJ%'),
+ ])
+ vj_lines_tags = it_tax_report_vj_lines.expression_ids._get_matching_tags()
+ for move in self:
+ if not move.is_purchase_document():
+ move.l10n_it_edi_is_self_invoice = False
+ continue
+ invoice_lines_tags = move.line_ids.tax_tag_ids
+ ids_intersection = set(invoice_lines_tags.ids) & set(vj_lines_tags.ids)
+ move.l10n_it_edi_is_self_invoice = bool(ids_intersection)
+
+ @api.depends('l10n_it_edi_transaction')
+ def _compute_need_cancel_request(self):
+ super()._compute_need_cancel_request()
+
+ # -------------------------------------------------------------------------
+ # Overrides
+ # -------------------------------------------------------------------------
+
+ def _need_cancel_request(self):
+ # EXTENDS 'account'
+ return super()._need_cancel_request() or self.l10n_it_edi_transaction
+
+ def _get_edi_decoder(self, file_data, new=False):
+ # EXTENDS 'account'
+ if file_data['type'] == 'l10n_it_edi':
+ return self._l10n_it_edi_import_invoice
+ return super()._get_edi_decoder(file_data, new=new)
+
+ def _post(self, soft=True):
+ # EXTENDS 'account'
+ self.write({'l10n_it_edi_header': False})
+ return super()._post(soft)
+
+ # -------------------------------------------------------------------------
+ # Business actions
+ # -------------------------------------------------------------------------
+
+ def action_l10n_it_edi_send(self):
+ """ Checks that the invoice data is coherent.
+ Attaches the XML file to the invoice.
+ Sends the invoice to the SdI.
+ """
+ self.ensure_one()
+
+ if (errors := self._l10n_it_edi_export_data_check()):
+ message = _("Errors occured while creating the e-invoice file.")
+ message += "\n- " + "\n- ".join(errors)
+ raise UserError(message)
+
+ attachment_vals = self._l10n_it_edi_get_attachment_values(pdf_values=None)
+ self.env['ir.attachment'].create(attachment_vals)
+ self.invalidate_recordset(fnames=['l10n_it_edi_attachment_id', 'l10n_it_edi_attachment_file'])
+ self.message_post(attachment_ids=self.l10n_it_edi_attachment_id.ids)
+ self._l10n_it_edi_send({self: attachment_vals})
+ self.is_move_sent = True
+
+ def action_check_l10n_it_edi(self):
+ self.ensure_one()
+ if not self.l10n_it_edi_transaction and self.l10n_it_edi_state not in WAITING_STATES:
+ raise UserError(_("This move is not waiting for updates from the SdI."))
+ self._l10n_it_edi_update_send_state()
+
+ # -------------------------------------------------------------------------
+ # Helpers
+ # -------------------------------------------------------------------------
+
+ def _l10n_it_edi_get_line_values(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: l.display_type not 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}"
+
+ invoice_lines.append({
+ 'line': line,
+ 'line_number': num + 1,
+ 'description': description or 'NO NAME',
+ 'unit_price': price_unit,
+ 'subtotal_price': price_subtotal,
+ 'vat_tax': line.tax_ids.flatten_taxes_hierarchy().filtered(lambda t: t._l10n_it_filter_kind('vat') and t.amount >= 0),
+ 'downpayment_moves': downpayment_moves,
+ 'discount_type': (
+ 'SC' if line.discount > 0
+ else 'MG' if line.discount < 0
+ else False
+ )
+ })
+ return invoice_lines
+
+ def _l10n_it_edi_get_tax_values(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.
+ base_amount = tax_dict['base_amount']
+ tax_amount = tax_dict['tax_amount']
+ tax = tax_dict['tax']
+ 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
+ tax = tax_dict['tax']
+ # Constraints within the edi make local rounding on price included taxes a problem.
+ # To solve this there is a 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_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 _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
+
+ def _l10n_it_edi_get_values(self, pdf_values=None):
+ self.ensure_one()
+
+ # Flags
+ is_self_invoice = self.l10n_it_edi_is_self_invoice
+ document_type = self._l10n_it_edi_get_document_type()
+
+ # 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'
+
+ tax_details = self._prepare_invoice_aggregated_taxes(filter_tax_values_to_apply=self._l10n_it_edi_filter_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()
+
+ if self._l10n_it_edi_is_simplified_document_type(document_type):
+ formato_trasmissione = "FSM10"
+ elif partner._l10n_it_edi_is_public_administration():
+ formato_trasmissione = "FPA12"
+ else:
+ formato_trasmissione = "FPR12"
+
+ # 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_rate = float_repr(
+ abs(self.amount_total / self.amount_total_signed), precision_digits=5,
+ ) if convert_to_euros and self.invoice_line_ids else None
+
+ invoice_lines = self._l10n_it_edi_get_line_values(reverse_charge_refund, is_downpayment, convert_to_euros)
+ tax_lines = self._l10n_it_edi_get_tax_values(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)
+
+ return {
+ '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,
+ 'formato_trasmissione': formato_trasmissione,
+ 'document_type': document_type,
+ '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,
+ 'pdf_name': pdf_values['name'] if pdf_values else False,
+ 'pdf': b64encode(pdf_values['raw']).decode() if pdf_values else False,
+ }
+
+ def _l10n_it_edi_services_or_goods(self):
+ """
+ Services and goods have different tax grids when VAT is Reverse Charged, and they can't
+ be mixed in the same invoice, because the TipoDocumento depends on which which kind
+ of product is bought and it's unambiguous.
+ """
+ self.ensure_one()
+ scopes = []
+ for line in self.invoice_line_ids.filtered(lambda l: l.display_type not in ('line_note', 'line_section')):
+ tax_ids_with_tax_scope = line.tax_ids.filtered(lambda x: x.tax_scope)
+ if tax_ids_with_tax_scope:
+ scopes += tax_ids_with_tax_scope.mapped('tax_scope')
+ else:
+ scopes.append(line.product_id and line.product_id.type or 'consu')
+
+ if set(scopes) == {'consu', 'service'}:
+ return "both"
+ return scopes and scopes.pop()
+
+ def _l10n_it_edi_goods_in_italy(self):
+ """
+ There is a specific TipoDocumento (Document Type TD19) and tax grid (VJ3) for goods
+ that are phisically in Italy but are in a VAT deposit, meaning that the goods
+ have not passed customs.
+ """
+ self.ensure_one()
+ invoice_lines_tags = self.line_ids.tax_tag_ids
+ it_tax_report_vj3_lines = self.env['account.report.line'].search([
+ ('report_id.country_id.code', '=', 'IT'),
+ ('code', '=', 'VJ3'),
+ ])
+ vj3_lines_tags = it_tax_report_vj3_lines.expression_ids._get_matching_tags()
+ return bool(invoice_lines_tags & vj3_lines_tags)
+
+ def _l10n_it_edi_is_simplified(self):
+ """
+ Simplified Invoices are a way for the invoice issuer to create an invoice with limited data.
+ Example: a consultant goes to the restaurant and wants the invoice instead of the receipt,
+ to be able to deduct the expense from his Taxes. The Italian State allows the restaurant
+ to issue a Simplified Invoice with the VAT number only, to speed up times, instead of
+ requiring the address and other informations about the buyer.
+ Only invoices under the threshold of 400 Euroes are allowed, to avoid this tool
+ be abused for bigger transactions, that would enable less transparency to tax institutions.
+ """
+ self.ensure_one()
+ template_reference = self.env.ref('l10n_it_edi.account_invoice_it_simplified_FatturaPA_export', raise_if_not_found=False)
+ buyer = self.commercial_partner_id
+ return bool(
+ template_reference
+ and not self.l10n_it_edi_is_self_invoice
+ and self._l10n_it_edi_export_buyer_data_check()
+ and (not buyer.country_id or buyer.country_id.code == 'IT')
+ and (buyer.l10n_it_codice_fiscale or (buyer.vat and (buyer.vat[:2].upper() == 'IT' or buyer.vat[:2].isdecimal())))
+ and self.amount_total <= 400
+ )
+
+ def _l10n_it_edi_features_for_document_type_selection(self):
+ """ Returns a dictionary of features to be compared with the TDxx FatturaPA
+ document type requirements. """
+ partner_values = self.commercial_partner_id._l10n_it_edi_get_values()
+ services_or_goods = self._l10n_it_edi_services_or_goods()
+ return {
+ 'move_types': self.move_type,
+ 'partner_in_eu': partner_values.get('in_eu', False),
+ 'partner_country_code': partner_values.get('country_code', False),
+ 'simplified': self._l10n_it_edi_is_simplified(),
+ 'self_invoice': self.l10n_it_edi_is_self_invoice,
+ 'downpayment': self._is_downpayment(),
+ 'services_or_goods': services_or_goods,
+ 'goods_in_italy': services_or_goods == 'consu' and self._l10n_it_edi_goods_in_italy(),
+ }
+
+ def _l10n_it_edi_document_type_mapping(self):
+ """ Returns a dictionary with the required features for every TDxx FatturaPA document type """
+ return {
+ 'TD01': {'move_types': ['out_invoice'],
+ 'import_type': 'in_invoice',
+ 'self_invoice': False,
+ 'simplified': False,
+ 'downpayment': False},
+ 'TD02': {'move_types': ['out_invoice'],
+ 'import_type': 'in_invoice',
+ 'self_invoice': False,
+ 'simplified': False,
+ 'downpayment': True},
+ 'TD04': {'move_types': ['out_refund'],
+ 'import_type': 'in_refund',
+ 'self_invoice': False,
+ 'simplified': False},
+ 'TD07': {'move_types': ['out_invoice'],
+ 'import_type': 'in_invoice',
+ 'self_invoice': False,
+ 'simplified': True},
+ 'TD08': {'move_types': ['out_refund'],
+ 'import_type': 'in_refund',
+ 'self_invoice': False,
+ 'simplified': True},
+ 'TD09': {'move_types': ['out_invoice'],
+ 'import_type': 'in_invoice',
+ 'self_invoice': False,
+ 'simplified': True},
+ 'TD28': {'move_types': ['in_invoice', 'in_refund'],
+ 'import_type': 'in_invoice',
+ 'simplified': False,
+ 'self_invoice': True,
+ 'partner_country_code': "SM"},
+ 'TD17': {'move_types': ['in_invoice', 'in_refund'],
+ 'import_type': 'in_invoice',
+ 'simplified': False,
+ 'self_invoice': True,
+ 'services_or_goods': "service"},
+ 'TD18': {'move_types': ['in_invoice', 'in_refund'],
+ 'import_type': 'in_invoice',
+ 'simplified': False,
+ 'self_invoice': True,
+ 'services_or_goods': "consu",
+ 'goods_in_italy': False,
+ 'partner_in_eu': True},
+ 'TD19': {'move_types': ['in_invoice', 'in_refund'],
+ 'import_type': 'in_invoice',
+ 'simplified': False,
+ 'self_invoice': True,
+ 'services_or_goods': "consu",
+ 'goods_in_italy': True},
+ }
+
+ def _l10n_it_edi_get_document_type(self):
+ """ Compare the features of the invoice to the requirements of each TDxx FatturaPA
+ document type until you find a valid one. """
+ invoice_features = self._l10n_it_edi_features_for_document_type_selection()
+ for code, document_type_features in self._l10n_it_edi_document_type_mapping().items():
+ comparisons = []
+ for key, invoice_feature in invoice_features.items():
+ if key not in document_type_features:
+ continue
+ document_type_feature = document_type_features.get(key)
+ if isinstance(document_type_feature, list):
+ comparisons.append(invoice_feature in document_type_feature)
+ else:
+ comparisons.append(invoice_feature == document_type_feature)
+ if all(comparisons):
+ return code
+ return False
+
+ def _l10n_it_edi_is_simplified_document_type(self, document_type):
+ mapping = self._l10n_it_edi_document_type_mapping()
+ return mapping.get(document_type, {}).get('simplified', False)
+
+ # -------------------------------------------------------------------------
+ # EDI: Import
+ # -------------------------------------------------------------------------
+
+ def cron_l10n_it_edi_download_and_update(self):
+ """ Crons run with sudo(), with empty recordset. Remember that. """
+ retrigger = False
+ for proxy_user in self.env['account_edi_proxy_client.user'].search([('proxy_type', '=', 'l10n_it_edi')]):
+ proxy_user = proxy_user.with_company(proxy_user.company_id)
+ if proxy_user.edi_mode != 'demo':
+ moves_to_check = self.search([
+ ('company_id', '=', proxy_user.company_id.id),
+ ('l10n_it_edi_transaction', '!=', False),
+ ('l10n_it_edi_state', 'in', WAITING_STATES)
+ ])
+ if moves_to_check:
+ moves_to_check._l10n_it_edi_update_send_state()
+ retrigger = retrigger or self._l10n_it_edi_download_invoices(proxy_user)
+
+ # Retrigger download if there are still some on the server
+ if retrigger:
+ _logger.info('Retriggering "Receive invoices from the SdI"...')
+ self.env.ref('l10n_it_edi.ir_cron_l10n_it_edi_download_and_update')._trigger()
+
+ def _l10n_it_edi_download_invoices(self, proxy_user):
+ """ Check the proxy for incoming invoices for a specified proxy user.
+ :return: True if there remain some invoices on the server to be downloaded, False otherwise.
+ """
+ server_url = proxy_user._get_server_url()
+
+ # Download invoices
+ invoices_data = {}
+ try:
+ invoices_data = proxy_user._make_request(f'{server_url}/api/l10n_it_edi/1/in/RicezioneInvoice',
+ params={'recipient_codice_fiscale': proxy_user.company_id.l10n_it_codice_fiscale})
+ except AccountEdiProxyError as e:
+ _logger.error('Error while receiving invoices from the SdI: %s', e)
+ return False
+
+ # Process the downloaded invoices
+ processed = self._l10n_it_edi_process_downloads(invoices_data, proxy_user)
+ if processed['proxy_acks']:
+ try:
+ proxy_user._make_request(
+ f'{server_url}/api/l10n_it_edi/1/ack',
+ params={'transaction_ids': processed['proxy_acks']})
+ except AccountEdiProxyError as e:
+ _logger.error('Error while receiving file from the SdI: %s', e)
+
+ return processed['retrigger']
+
+ def _l10n_it_edi_process_downloads(self, invoices_data, proxy_user):
+ """ Every attachment will be committed if stored succesfully.
+ Also moves will be committed one by one, even if imported incorrectly.
+ """
+ proxy_acks = []
+ retrigger = False
+ moves = self.env['account.move']
+
+ for id_transaction, invoice_data in invoices_data.items():
+
+ # The IAP server has a maximum number of documents it can send.
+ # If that maximum is reached, then we search for more
+ # by re-triggering the download cron, avoiding the timeout.
+ current_num = invoice_data.get('current_num', 0)
+ max_num = invoice_data.get('max_num', 0)
+ retrigger = retrigger or current_num == max_num > 0
+
+ # `_l10n_it_edi_create_move_from_attachment` will create an empty move
+ # then try and fill it with the content imported from the attachment.
+ # Should the import fail, thanks to try..except and savepoint,
+ # we will anyway end up with an empty `in_invoice` with the attachment posted on it.
+ if move := self.with_company(self.company_id)._l10n_it_edi_create_move_with_attachment(
+ invoice_data['filename'],
+ invoice_data['file'],
+ invoice_data['key'],
+ proxy_user,
+ ):
+ self.env.cr.commit()
+ moves |= move
+ proxy_acks.append(id_transaction)
+
+ # Extend created moves with the related attachments and commit
+ for move in moves:
+ move._extend_with_attachments(move.l10n_it_edi_attachment_id, new=True)
+ self.env.cr.commit()
+
+ return {"retrigger": retrigger, "proxy_acks": proxy_acks}
+
+ def _l10n_it_edi_create_move_with_attachment(self, filename, content, key, proxy_user):
+ """ Creates a move and save an incoming file from the SdI as its attachment.
+
+ :param filename: name of the file to be saved.
+ :param content: encrypted content of the file to be saved.
+ :param key: key to decrypt the file.
+ :param proxy_user: the AccountEdiProxyClientUser to use for decrypting the file
+ """
+
+ # Name should be unique, the invoice already exists
+ Attachment = self.env['ir.attachment']
+ if Attachment.search_count([
+ ('name', '=', filename),
+ ('res_model', '=', 'account.move'),
+ ('res_field', '=', 'l10n_it_edi_attachment_file'),
+ ], limit=1):
+ _logger.warning('E-invoice already exists: %s', filename)
+ return False
+
+ # Decrypt with the server key
+ try:
+ decrypted_content = proxy_user._decrypt_data(content, key)
+ except Exception as e: # noqa: BLE001
+ _logger.warning("Cannot decrypt e-invoice: %s, %s", filename, e)
+ return False
+
+ # Create the attachment, an empty move, then attach the two and commit
+ move = self.create({})
+ attachment = Attachment.create({
+ 'name': filename,
+ 'raw': decrypted_content,
+ 'type': 'binary',
+ 'res_model': 'account.move',
+ 'res_id': move.id,
+ 'res_field': 'l10n_it_edi_attachment_file'
+ })
+ move.with_context(
+ account_predictive_bills_disable_prediction=True,
+ no_new_invoice=True,
+ ).message_post(attachment_ids=attachment.ids)
+
+ return move
+
+ def _l10n_it_edi_search_partner(self, company, vat, codice_fiscale, email):
+ for domain in [vat and [('vat', 'ilike', vat)],
+ codice_fiscale and [('l10n_it_codice_fiscale', 'in', ('IT' + codice_fiscale, codice_fiscale))],
+ email and ['|', ('email', '=', email), ('l10n_it_pec_email', '=', email)]]:
+ if domain and (partner := self.env['res.partner'].search(
+ domain + self.env['res.partner']._check_company_domain(company), limit=1)):
+ return partner
+ return self.env['res.partner']
+
+ def _l10n_it_edi_search_tax_for_import(self, company, percentage, extra_domain=None):
+ """ Returns the VAT, Withholding or Pension Fund tax that suits the conditions given
+ and matches the percentage found in the XML for the company. """
+ domain = [
+ *self.env['account.tax']._check_company_domain(company),
+ ('amount', '=', percentage),
+ ('amount_type', '=', 'percent'),
+ ('type_tax_use', '=', 'purchase'),
+ ] + (extra_domain or [])
+
+ # As we're importing vendor bills, we're excluding Reverse Charge Taxes
+ # which have a [100.0, 100.0, -100.0] repartition lines factor_percent distribution.
+ # We only allow for taxes that have all positive repartition lines factor_percent distribution.
+ taxes = self.env['account.tax'].search(domain).filtered(
+ lambda tax: all(rep_line.factor_percent >= 0 for rep_line in tax.invoice_repartition_line_ids))
+
+ return taxes[0] if taxes else taxes
+
+ def _l10n_it_edi_get_extra_info(self, company, document_type, body_tree):
+ """ This function is meant to collect other information that has to be inserted on the invoice lines by submodules.
+ :return extra_info, messages_to_log"""
+ return {'simplified': self.env['account.move']._l10n_it_edi_is_simplified_document_type(document_type)}, []
+
+ def _l10n_it_edi_import_invoice(self, invoice, data, is_new):
+ """ Decodes a l10n_it_edi move into an Odoo move.
+
+ :param data: the dictionary with the content to be imported
+ keys: 'filename', 'content', 'xml_tree', 'type', 'sort_weight'
+ :param is_new: whether the move is newly created or to be updated
+ :returns: the imported move
+ """
+ tree = data['xml_tree']
+ company = self.company_id
+
+ # For unsupported document types, just assume in_invoice, and log that the type is unsupported
+ document_type = get_text(tree, '//DatiGeneraliDocumento/TipoDocumento')
+ move_type = self._l10n_it_edi_document_type_mapping().get(document_type, {}).get('import_type')
+ if not move_type:
+ move_type = "in_invoice"
+ _logger.info('Document type not managed: %s. Invoice type is set by default.', document_type)
+
+ self.move_type = move_type
+
+ # Collect extra info from the XML that may be used by submodules to further put information on the invoice lines
+ extra_info, message_to_log = self._l10n_it_edi_get_extra_info(company, document_type, tree)
+
+ # Partner
+ vat = get_text(tree, '//CedentePrestatore//IdCodice')
+ codice_fiscale = get_text(tree, '//CedentePrestatore//CodiceFiscale')
+ email = get_text(tree, '//DatiTrasmissione//Email')
+ if partner := self._l10n_it_edi_search_partner(company, vat, codice_fiscale, email):
+ self.partner_id = partner
+ else:
+ message = Markup(" ").join((
+ _("Vendor not found, useful informations from XML file:"),
+ self._compose_info_message(tree, './/CedentePrestatore')
+ ))
+ message_to_log.append(message)
+
+ # Numbering attributed by the transmitter
+ if progressive_id := get_text(tree, '//ProgressivoInvio'):
+ self.payment_reference = progressive_id
+
+ # Document Number
+ if number := get_text(tree, './/DatiGeneraliDocumento//Numero'):
+ self.ref = number
+
+ # Currency
+ if currency_str := get_text(tree, './/DatiGeneraliDocumento/Divisa'):
+ currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False)
+ if currency != self.env.company.currency_id and currency.active:
+ self.currency_id = currency
+
+ # Date
+ if document_date := get_date(tree, './/DatiGeneraliDocumento/Data'):
+ self.invoice_date = document_date
+ else:
+ message_to_log.append(_("Document date invalid in XML file: %s", document_date))
+
+ # Stamp Duty
+ if stamp_duty := get_text(tree, './/DatiGeneraliDocumento/DatiBollo/ImportoBollo'):
+ self.l10n_it_stamp_duty = float(stamp_duty)
+
+ # Comment
+ for narration in get_text(tree, './/DatiGeneraliDocumento//Causale', many=True):
+ self.narration = '%s%s ' % (self.narration or '', narration)
+
+ # Informations relative to the purchase order, the contract, the agreement,
+ # the reception phase or invoices previously transmitted
+ # <2.1.2> - <2.1.6>
+ for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']:
+ for element in tree.xpath('.//DatiGenerali/' + document_type):
+ message = Markup("{} {} {}").format(document_type, _("from XML file:"), self._compose_info_message(element, '.'))
+ message_to_log.append(message)
+
+ # Dati DDT. <2.1.8>
+ if elements := tree.xpath('.//DatiGenerali/DatiDDT'):
+ message = Markup(" ").join((
+ _("Transport informations from XML file:"),
+ self._compose_info_message(tree, './/DatiGenerali/DatiDDT')
+ ))
+ message_to_log.append(message)
+
+ # Due date. <2.4.2.5>
+ if due_date := get_date(tree, './/DatiPagamento/DettaglioPagamento/DataScadenzaPagamento'):
+ self.invoice_date_due = fields.Date.to_string(due_date)
+ else:
+ message_to_log.append(_("Payment due date invalid in XML file: %s", str(due_date)))
+
+ # Information related to the purchase order <2.1.2>
+ if (po_refs := get_text(tree, '//DatiGenerali/DatiOrdineAcquisto/IdDocumento', many=True)):
+ self.invoice_origin = ", ".join(po_refs)
+
+ # Total amount. <2.4.2.6>
+ if amount_total := sum([float(x) for x in get_text(tree, './/ImportoPagamento', many=True) if x]):
+ message_to_log.append(_("Total amount from the XML File: %s", amount_total))
+
+ # Bank account. <2.4.2.13>
+ if self.move_type not in ('out_invoice', 'in_refund'):
+ if acc_number := get_text(tree, './/DatiPagamento/DettaglioPagamento/IBAN'):
+ if self.partner_id and self.partner_id.commercial_partner_id:
+ bank = self.env['res.partner.bank'].search([
+ ('acc_number', '=', acc_number),
+ ('partner_id', '=', self.partner_id.commercial_partner_id.id),
+ ('company_id', 'in', [self.company_id.id, False])
+ ], order='company_id', limit=1)
+ else:
+ bank = self.env['res.partner.bank'].search([
+ ('acc_number', '=', acc_number),
+ ('company_id', 'in', [self.company_id.id, False])
+ ], order='company_id', limit=1)
+ if bank:
+ self.partner_bank_id = bank
+ else:
+ message = Markup(" ").join((
+ _("Bank account not found, useful informations from XML file:"),
+ self._compose_info_message(tree, [
+ './/DatiPagamento//Beneficiario',
+ './/DatiPagamento//IstitutoFinanziario',
+ './/DatiPagamento//IBAN',
+ './/DatiPagamento//ABI',
+ './/DatiPagamento//CAB',
+ './/DatiPagamento//BIC',
+ './/DatiPagamento//ModalitaPagamento'
+ ])
+ ))
+ message_to_log.append(message)
+ elif elements := tree.xpath('.//DatiPagamento/DettaglioPagamento'):
+ message = Markup(" ").join((
+ _("Bank account not found, useful informations from XML file:"),
+ self._compose_info_message(tree, './/DatiPagamento')
+ ))
+ message_to_log.append(message)
+
+ # Invoice lines. <2.2.1>
+ tag_name = './/DettaglioLinee' if not extra_info['simplified'] else './/DatiBeniServizi'
+ for element in tree.xpath(tag_name):
+ move_line = self.invoice_line_ids.create({
+ 'move_id': self.id,
+ 'tax_ids': [fields.Command.clear()]})
+ if move_line:
+ message_to_log += self._l10n_it_edi_import_line(element, move_line, extra_info)
+
+ # Global discount summarized in 1 amount
+ if discount_elements := tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione'):
+ taxable_amount = float(self.tax_totals['amount_untaxed'])
+ discounted_amount = taxable_amount
+ for discount_element in discount_elements:
+ discount_sign = 1
+ if (discount_type := discount_element.xpath('.//Tipo')) and discount_type[0].text == 'MG':
+ discount_sign = -1
+ if discount_amount := get_text(discount_element, './/Importo'):
+ discounted_amount -= discount_sign * float(discount_amount)
+ continue
+ if discount_percentage := get_text(discount_element, './/Percentuale'):
+ discounted_amount *= 1 - discount_sign * float(discount_percentage) / 100
+
+ general_discount = discounted_amount - taxable_amount
+ sequence = len(elements) + 1
+
+ self.invoice_line_ids = [Command.create({
+ 'sequence': sequence,
+ 'name': 'SCONTO' if general_discount < 0 else 'MAGGIORAZIONE',
+ 'price_unit': general_discount,
+ })]
+
+ for element in tree.xpath('.//Allegati'):
+ attachment_64 = self.env['ir.attachment'].create({
+ 'name': get_text(element, './/NomeAttachment'),
+ 'datas': str.encode(get_text(element, './/Attachment')),
+ 'type': 'binary',
+ 'res_model': 'account.move',
+ 'res_id': self.id,
+ })
+
+ # no_new_invoice to prevent from looping on the.message_post that would create a new invoice without it
+ self.with_context(no_new_invoice=True).sudo().message_post(
+ body=(_("Attachment from XML")),
+ attachment_ids=[attachment_64.id],
+ )
+
+ for message in message_to_log:
+ self.sudo().message_post(body=message)
+ return self
+
+ def _l10n_it_edi_import_line(self, element, move_line, extra_info=None):
+ extra_info = extra_info or {}
+ company = move_line.company_id
+ partner = move_line.partner_id
+ message_to_log = []
+
+ # Sequence.
+ line_elements = element.xpath('.//NumeroLinea')
+ if line_elements:
+ move_line.sequence = int(line_elements[0].text)
+
+ # Product.
+ if elements_code := element.xpath('.//CodiceArticolo'):
+ for element_code in elements_code:
+ type_code = element_code.xpath('.//CodiceTipo')[0]
+ code = element_code.xpath('.//CodiceValore')[0]
+ product = self.env['product.product'].search([('barcode', '=', code.text)])
+ if (product and type_code.text == 'EAN'):
+ move_line.product_id = product
+ break
+ if partner:
+ product_supplier = self.env['product.supplierinfo'].search([('partner_id', '=', partner.id), ('product_code', '=', code.text)], limit=2)
+ if product_supplier and len(product_supplier) == 1 and product_supplier.product_id:
+ move_line.product_id = product_supplier.product_id
+ break
+ if not move_line.product_id:
+ for element_code in elements_code:
+ code = element_code.xpath('.//CodiceValore')[0]
+ product = self.env['product.product'].search([('default_code', '=', code.text)], limit=2)
+ if product and len(product) == 1:
+ move_line.product_id = product
+ break
+
+ # Name and Quantity.
+ move_line.name = " ".join(get_text(element, './/Descrizione').split())
+ move_line.quantity = float(get_text(element, './/Quantita') or '1')
+
+ # Taxes
+ percentage = None
+ if not extra_info['simplified']:
+ percentage = get_float(element, './/AliquotaIVA')
+ if price_unit := get_float(element, './/PrezzoUnitario'):
+ move_line.price_unit = price_unit
+ elif amount := get_float(element, './/Importo'):
+ percentage = get_float(element, './/Aliquota')
+ if not percentage and (tax_amount := get_float(element, './/Imposta')):
+ percentage = round(tax_amount / (amount - tax_amount) * 100)
+ move_line.price_unit = amount / (1 + percentage / 100)
+
+ move_line.tax_ids = []
+ if percentage is not None:
+ conditions = [('l10n_it_has_exoneration', '=', False)]
+ if l10n_it_kind_exoneration := get_text(element, './/Natura'):
+ conditions = [('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration)]
+ if tax := self._l10n_it_edi_search_tax_for_import(company, percentage, conditions):
+ move_line.tax_ids += tax
+ else:
+ message = Markup(" ").join((
+ _("Tax not found for line with description '%s'", move_line.name),
+ self._compose_info_message(element, '.')
+ ))
+ message_to_log.append(message)
+
+ # Discounts
+ if elements := element.xpath('.//ScontoMaggiorazione'):
+ element = elements[0]
+ # Special case of only 1 percentage discount
+ if len(elements) == 1:
+ if discount_percentage := get_float(element, './/Percentuale'):
+ discount_type = get_text(element, './/Tipo')
+ discount_sign = -1 if discount_type == 'MG' else 1
+ move_line.discount = discount_sign * discount_percentage
+ # Discounts in cascade summarized in 1 percentage
+ else:
+ total = get_float(element, './/PrezzoTotale')
+ discount = 100 - (100 * total) / (move_line.quantity * move_line.price_unit)
+ move_line.discount = discount
+
+ return message_to_log
+
+ def _l10n_it_edi_format_errors(self, header, errors):
+ return Markup('{}
{}
').format(
+ nl2br_enclose(header, 'span') if header else '',
+ Markup().join(nl2br_enclose(' '.join(error.split()), 'li') for error in errors)
+ )
+
+ def _compose_info_message(self, tree, tags):
+ result = ""
+ for tag in tags if isinstance(tags, list) else [tags]:
+ for el in tree.xpath(tag):
+ result += self._l10n_it_edi_format_errors("", [f'{subel.tag}: {subel.text}' for subel in el.iter()])
+ return result
+
+ # -------------------------------------------------------------------------
+ # EDI: Export
+ # -------------------------------------------------------------------------
+
+ def _l10n_it_edi_export_data_check(self):
+ errors = self._l10n_it_edi_base_export_data_check()
+ if not self._l10n_it_edi_is_simplified():
+ errors += self._l10n_it_edi_export_buyer_data_check()
+ return errors
+
+ def _l10n_it_edi_format_export_data_errors(self):
+ messages = (
+ self._l10n_it_edi_format_errors(move.name + ":" if len(self) > 1 else False, move_warnings)
+ for move in self if (move_warnings := move._l10n_it_edi_export_data_check())
+ )
+ return Markup(" ").join(messages) or False
+
+ def _l10n_it_edi_base_export_data_check(self):
+ errors = []
+ seller = self.company_id
+ buyer = self.commercial_partner_id
+ is_self_invoice = self.l10n_it_edi_is_self_invoice
+ if is_self_invoice:
+ seller, buyer = buyer, seller
+
+ # <1.1.1.1>
+ if not seller.country_id:
+ errors.append(_("%s must have a country", seller.display_name))
+
+ # <1.1.1.2>
+ if not self.company_id.vat:
+ errors.append(_("%s must have a VAT number", seller.display_name))
+ if seller.vat and len(seller.vat) > 30:
+ errors.append(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.", seller.display_name, seller.vat))
+
+ # <1.2.1.2>
+ if not is_self_invoice and not seller.l10n_it_codice_fiscale:
+ errors.append(_("%s must have a codice fiscale number", seller.display_name))
+
+ # <1.2.1.8>
+ if not is_self_invoice and not seller.l10n_it_tax_system:
+ errors.append(_("The seller's company must have a tax system."))
+
+ # <1.2.2>
+ if not seller.street and not seller.street2:
+ errors.append(_("%s must have a street.", seller.display_name))
+ if not seller.zip:
+ errors.append(_("%s must have a post code.", seller.display_name))
+ elif len(seller.zip) != 5 and seller.country_id.code == 'IT':
+ errors.append(_("%s must have a post code of length 5.", seller.display_name))
+ if not seller.city:
+ errors.append(_("%s must have a city.", seller.display_name))
+ if not seller.country_id:
+ errors.append(_("%s must have a country.", seller.display_name))
+
+ if not is_self_invoice and seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat:
+ errors.append(_("Tax representative partner %s of %s must have a tax number.", seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name))
+
+ # <1.4.1>
+ if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
+ errors.append(_("The buyer, %s, or his company must have a VAT number and/or a tax code (Codice Fiscale).", buyer.display_name))
+
+ if is_self_invoice and self._l10n_it_edi_services_or_goods() == 'both':
+ errors.append(_("Cannot apply Reverse Charge to a bill which contains both services and goods."))
+
+ if is_self_invoice and not buyer.partner_id.l10n_it_pa_index:
+ errors.append(_("Vendor bills sent as self-invoices to the SdI require a valid PA Index (Codice Destinatario) on the company's contact."))
+
+ for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id):
+ if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
+ errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
+
+ errors += self._l10n_it_edi_export_taxes_data_check()
+
+ return errors
+
+ def _l10n_it_edi_export_taxes_data_check(self):
+ """
+ Can be overridden by submodules like l10n_it_edi_withholding, which also allows for withholding and pension_fund taxes.
+ """
+ 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 = all_taxes.filtered(lambda t: t.amount_type == 'percent' and t.amount >= 0)
+ if len(vat_taxes) != 1:
+ errors.append(_("In line %s, you must select one and only one VAT tax.", invoice_line.name))
+ return errors
+
+ def _l10n_it_edi_export_buyer_data_check(self):
+ errors = []
+ buyer = self.commercial_partner_id
+
+ # <1.4.2>
+ if not buyer.street and not buyer.street2:
+ errors.append(_("%s must have a street.", buyer.display_name))
+ if not buyer.country_id:
+ errors.append(_("%s must have a country.", buyer.display_name))
+ if not buyer.zip:
+ errors.append(_("%s must have a post code.", buyer.display_name))
+ elif len(buyer.zip) != 5 and buyer.country_id.code == 'IT':
+ errors.append(_("%s must have a post code of length 5.", buyer.display_name))
+ if not buyer.city:
+ errors.append(_("%s must have a city.", buyer.display_name))
+
+ for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id):
+ if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
+ errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
+
+ return errors
+
+ def _l10n_it_edi_get_formatters(self):
+ 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_date(dt):
+ # Format the date in the italian standard.
+ dt = dt or datetime.now()
+ return dt.strftime('%Y-%m-%d')
+
+ 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 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)
+
+ return {
+ '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,
+ }
+
+ def _l10n_it_edi_render_xml(self, pdf_values=None):
+ ''' Create the xml file content.
+ :return: The XML content as bytestring.
+ '''
+ qweb_template_name = (
+ 'l10n_it_edi.account_invoice_it_FatturaPA_export' if not self._l10n_it_edi_is_simplified()
+ else 'l10n_it_edi.account_invoice_it_simplified_FatturaPA_export')
+ xml_content = self.env['ir.qweb']._render(qweb_template_name, {
+ **self._l10n_it_edi_get_values(pdf_values),
+ **self._l10n_it_edi_get_formatters()})
+ xml_node = cleanup_xml_node(xml_content, remove_blank_nodes=False)
+ return etree.tostring(xml_node, xml_declaration=True, encoding='UTF-8')
+
+ def _l10n_it_edi_get_attachment_values(self, pdf_values=None):
+ self.ensure_one()
+ return {
+ 'name': self._l10n_it_edi_generate_filename(),
+ 'type': 'binary',
+ 'mimetype': 'application/xml',
+ 'description': _('IT EDI e-move: %s', self.move_type),
+ 'company_id': self.company_id.id,
+ 'res_id': self.id,
+ 'res_model': self._name,
+ 'res_field': 'l10n_it_edi_attachment_file',
+ 'raw': self._l10n_it_edi_render_xml(pdf_values=pdf_values),
+ }
+
+ def _l10n_it_edi_generate_filename(self):
+ '''Returns a name conform to the Fattura pa Specifications:
+ See ES documentation 2.2
+ '''
+ a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ # Each company should have its own filename sequence. If it does not exist, create it
+ n = self.env['ir.sequence'].with_company(self.company_id).next_by_code('l10n_it_edi.fattura_filename')
+ if not n:
+ # The offset is used to avoid conflicts with existing filenames
+ offset = 62 ** 4
+ sequence = self.env['ir.sequence'].sudo().create({
+ 'name': 'FatturaPA Filename Sequence',
+ 'code': 'l10n_it_edi.fattura_filename',
+ 'company_id': self.company_id.id,
+ 'number_next': offset,
+ })
+ n = sequence._next()
+ # The n is returned as a string, but we require an int
+ n = int(''.join(filter(lambda c: c.isdecimal(), n)))
+
+ progressive_number = ""
+ while n:
+ (n, m) = divmod(n, len(a))
+ progressive_number = a[m] + progressive_number
+
+ return '%(country_code)s%(codice)s_%(progressive_number)s.xml' % {
+ 'country_code': self.company_id.country_id.code,
+ 'codice': self.company_id.partner_id._l10n_it_edi_normalized_codice_fiscale(),
+ 'progressive_number': progressive_number.zfill(5),
+ }
+
+ def _l10n_it_edi_send(self, attachments_vals):
+ files_to_upload = []
+ filename_move = {}
+
+ # Setup moves for sending
+ for move in self:
+ attachment_vals = attachments_vals[move]
+ filename = attachment_vals['name']
+ content = b64encode(attachment_vals['raw']).decode()
+ move.l10n_it_edi_header = False
+ if move.commercial_partner_id._l10n_it_edi_is_public_administration():
+ move.l10n_it_edi_state = 'requires_user_signature'
+ move.l10n_it_edi_transaction = False
+ move.sudo().message_post(body=nl2br(escape(_(
+ "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."
+ ))))
+ else:
+ move.l10n_it_edi_state = 'being_sent'
+ files_to_upload.append({'filename': filename, 'xml': content})
+ filename_move[filename] = move
+
+ # Upload files
+ try:
+ results = self._l10n_it_edi_upload(files_to_upload)
+ except AccountEdiProxyError as e:
+ messages_to_log = []
+ for filename in filename_move:
+ unsent_move = filename_move[filename]
+ unsent_move.l10n_it_edi_state = False
+ text_message = _("Error uploading the e-invoice file %s.\n%s", filename, e.message)
+ html_message = nl2br(escape(text_message))
+ unsent_move.l10n_it_edi_header = text_message
+ unsent_move.sudo().message_post(body=html_message)
+ messages_to_log.append(text_message)
+ raise UserError("\n".join(messages_to_log)) from e
+
+ # Handle results
+ for filename, vals in results.items():
+ sent_move = filename_move[filename]
+ if 'error' in vals:
+ sent_move.l10n_it_edi_state = False
+ sent_move.l10n_it_edi_transaction = False
+ message = nl2br(escape(_("Error uploading the e-invoice file %s.\n%s", filename, vals['error'])))
+ else:
+ is_demo = vals['id_transaction'] == 'demo'
+ sent_move.l10n_it_edi_state = 'processing'
+ sent_move.l10n_it_edi_transaction = vals['id_transaction']
+ message = (
+ _("We are simulating the sending of the e-invoice file %s, as we are in demo mode.", filename)
+ if is_demo else _("The e-invoice file %s was sent to the SdI for processing.", filename))
+ sent_move.l10n_it_edi_header = message
+ sent_move.sudo().message_post(body=message)
+
+ def _l10n_it_edi_upload(self, files):
+ '''Upload files to the SdI.
+
+ :param files: A list of dictionary {filename, base64_xml}.
+ :returns: A dictionary.
+ * message: Message from fatturapa.
+ * transactionId: The fatturapa ID of this request.
+ * error: An eventual error.
+ '''
+ if not files:
+ return {}
+ proxy_user = self.company_id.l10n_it_edi_proxy_user_id
+ proxy_user.ensure_one()
+ if proxy_user.edi_mode == 'demo':
+ return {file_data['filename']: {'id_transaction': 'demo'} for file_data in files}
+
+ ERRORS = {'EI01': _('Attached file is empty'),
+ 'EI02': _('Service momentarily unavailable'),
+ 'EI03': _('Unauthorized user')}
+
+ server_url = proxy_user._get_server_url()
+ results = proxy_user._make_request(
+ f'{server_url}/api/l10n_it_edi/1/out/SdiRiceviFile',
+ params={'files': files})
+
+ for filename, vals in results.items():
+ if 'error' in vals:
+ results[filename]['error'] = ERRORS.get(vals.get('error'), _("Unknown error"))
+
+ return results
+
+ # -------------------------------------------------------------------------
+ # EDI: Update notifications
+ # -------------------------------------------------------------------------
+
+ def _l10n_it_edi_update_send_state(self):
+ ''' Check if the current invoices have been processed by the SdI. '''
+ proxy_user = self.company_id.l10n_it_edi_proxy_user_id
+ if proxy_user.edi_mode == 'demo':
+ for move in self:
+ filename = move.l10n_it_edi_attachment_id and move.l10n_it_edi_attachment_id.name or '???'
+ self._l10n_it_edi_write_send_state(
+ transformed_notification={
+ 'l10n_it_edi_state': 'forwarded',
+ 'l10n_it_edi_transaction': f'demo_{uuid.uuid4()}',
+ 'send_ack_to_edi_proxy': False,
+ 'date': fields.Date.today(),
+ 'filename': filename},
+ message=_("The e-invoice file %s has been sent in Demo EDI mode.", filename))
+ return
+
+ server_url = proxy_user._get_server_url()
+ try:
+ notifications = proxy_user._make_request(
+ f'{server_url}/api/l10n_it_edi/1/in/TrasmissioneFatture',
+ params={'ids_transaction': self.mapped("l10n_it_edi_transaction")})
+ except AccountEdiProxyError as pe:
+ raise UserError(_("An error occurred while downloading updates from the Proxy Server: (%s) %s", pe.code, pe.message)) from pe
+
+ for _id_transaction, notification in notifications.items():
+ encrypted_update_content = notification.get('file')
+ encryption_key = notification.get('key')
+ if (encrypted_update_content and encryption_key):
+ notification['xml_content'] = proxy_user._decrypt_data(encrypted_update_content, encryption_key)
+
+ acks = {'transaction_ids': [], 'states': []}
+ for move in self:
+ notification = notifications[move.l10n_it_edi_transaction]
+ parsed_notification = move._l10n_it_edi_parse_notification(notification)
+ transformed_notification = move._l10n_it_edi_transform_notification(parsed_notification)
+ message = move._l10n_it_edi_get_message(transformed_notification)
+ move._l10n_it_edi_write_send_state(transformed_notification, message)
+ if (
+ transformed_notification.get('send_ack_to_edi_proxy')
+ and (id_transaction_to_ack := transformed_notification.get('l10n_it_edi_transaction'))
+ and (ack_state := transformed_notification.get('l10n_it_edi_state'))
+ ):
+ acks['transaction_ids'].append(id_transaction_to_ack)
+ acks['states'].append(ack_state)
+
+ if acks:
+ transaction_ids = acks['transaction_ids']
+ states = acks['states']
+ try:
+ proxy_user._make_request(
+ f'{server_url}/api/l10n_it_edi/1/ack',
+ params={'transaction_ids': transaction_ids, 'states': states})
+ except AccountEdiProxyError as pe:
+ raise UserError(_("An error occurred while downloading updates from the Proxy Server: (%s) %s", pe.code, pe.message)) from pe
+
+ def _l10n_it_edi_parse_notification(self, notification):
+ sdi_state = notification.get('state', '')
+ if not (xml_content := notification.get('xml_content')):
+ return {'sdi_state': sdi_state}
+
+ decrypted_update_content = etree.fromstring(xml_content)
+ outcome = get_text(decrypted_update_content, './/Esito')
+ date_arrival = get_datetime(decrypted_update_content, './/DataOraRicezione') or fields.Date.today()
+ errors = [(
+ get_text(error_element, '//Codice'),
+ get_text(error_element, '//Descrizione'),
+ ) for error_element in decrypted_update_content.xpath('//Errore')]
+ filename = get_text(decrypted_update_content, './/NomeFile')
+
+ return {
+ 'sdi_state': sdi_state,
+ 'errors': errors,
+ 'outcome': outcome,
+ 'date': date_arrival,
+ 'filename': filename,
+ }
+
+ def _l10n_it_edi_transform_notification(self, parsed_notification):
+ """ Reads the notification XML coming from the EDI Proxy Server
+ Recovers information about the new state.
+ Computes whether the EDI Proxy Server is to be acked,
+ and whether the id_transaction has to be reset.
+ """
+ self.ensure_one()
+ state_map = {
+ 'not_found': False,
+ 'awaiting_outcome': 'processing',
+ 'notificaScarto': 'rejected',
+ 'ricevutaConsegna': 'forwarded',
+ 'forward_attempt': 'forward_attempt',
+ 'notificaMancataConsegna': 'forward_failed',
+ ('notificaEsito', 'EC01'): 'accepted_by_pa_partner',
+ ('notificaEsito', 'EC02'): 'rejected_by_pa_partner',
+ 'notificaDecorrenzaTermini': 'accepted_by_pa_partner_after_expiry',
+ }
+ sdi_state = parsed_notification['sdi_state']
+ filename = parsed_notification.get('filename')
+ errors = parsed_notification.get('errors', [])
+ date = parsed_notification.get('date', fields.Date.today())
+ if not filename and self.l10n_it_edi_attachment_id:
+ filename = self.l10n_it_edi_attachment_id.name
+ outcome = parsed_notification.get('outcome', False)
+ if not outcome:
+ new_state = state_map.get(sdi_state, False)
+ else:
+ new_state = state_map.get((sdi_state, outcome), False)
+
+ parsed_notification.update({
+ 'l10n_it_edi_state': new_state,
+ 'l10n_it_edi_transaction': False if new_state in (False, 'rejected') else self.l10n_it_edi_transaction,
+ 'send_ack_to_edi_proxy': bool(new_state),
+ 'date': date,
+ 'errors': errors,
+ 'filename': filename,
+ })
+ return parsed_notification
+
+ def _l10n_it_edi_write_send_state(self, transformed_notification, message):
+ """ Update the record with the data coming from the IAP server.
+ Eventually post the message.
+ Commit the transaction.
+ """
+ self.ensure_one()
+ old_state = self.l10n_it_edi_state
+ new_state = transformed_notification['l10n_it_edi_state']
+ self.write({
+ 'l10n_it_edi_state': new_state,
+ 'l10n_it_edi_transaction': transformed_notification['l10n_it_edi_transaction'],
+ 'l10n_it_edi_header': message or False,
+ })
+
+ if message and old_state != new_state:
+ self.with_context(no_new_invoice=True).sudo().message_post(body=message)
+
+ if new_state == 'rejected':
+ self.l10n_it_edi_attachment_file = False
+
+ self.env.cr.commit()
+
+ def _l10n_it_edi_get_message(self, transformed_notification):
+ """ The status change will be notified in the chatter of the move.
+ Compute the message from the notification information coming from the EDI Proxy Server
+ """
+ self.ensure_one()
+ partner = self.commercial_partner_id
+ partner_name = partner.display_name
+ filename = transformed_notification['filename']
+ new_state = transformed_notification['l10n_it_edi_state']
+ if new_state == 'rejected':
+ DUPLICATE_MOVE = '00404'
+ DUPLICATE_FILENAME = '00002'
+ error_descriptions = []
+ for error_code, error_description in transformed_notification['errors']:
+ error_description_copy = error_description
+ if error_code == DUPLICATE_MOVE:
+ error_description_copy = _(
+ "The e-invoice file %s is duplicated.\n"
+ "Original message from the SdI: %s",
+ filename, error_description_copy)
+ elif error_code == DUPLICATE_FILENAME:
+ error_description_copy = _(
+ "The e-invoice filename %s is duplicated. Please check the FatturaPA Filename sequence.\n"
+ "Original message from the SdI: %s",
+ filename, error_description_copy)
+ error_descriptions.append(error_description_copy)
+
+ return self._l10n_it_edi_format_errors(_('The e-invoice has been refused by the SdI.'), error_descriptions)
+
+ elif partner._l10n_it_edi_is_public_administration():
+ pa_specific_map = {
+ 'forwarded': nl2br(escape(_(
+ "The e-invoice file %s was succesfully sent to the SdI.\n"
+ "%s has 15 days to accept or reject it.",
+ filename, partner_name))),
+ 'forward_attempt': nl2br(escape(_(
+ "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.",
+ filename, partner_name))),
+ 'accepted_by_pa_partner_after_expiry': nl2br(escape(_(
+ "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.",
+ filename, partner_name))),
+ 'rejected_by_pa_partner': nl2br(escape(_(
+ "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.",
+ filename, partner_name))),
+ 'accepted_by_pa_partner': _(
+ "The e-invoice file %s has been accepted by %s (Public Administration), a payment will be issued soon",
+ filename, partner_name),
+ }
+ if pa_specific_message := pa_specific_map.get(new_state):
+ return pa_specific_message
+
+ new_state_messages_map = {
+ False: _(
+ "The e-invoice file %s has not been found on the EDI Proxy server.", filename),
+ 'processing': nl2br(escape(_(
+ "The e-invoice file %s was sent to the SdI for validation.\n"
+ "It is not yet considered accepted, please wait further notifications.",
+ filename))),
+ 'forwarded': _(
+ "The e-invoice file %s was accepted and succesfully forwarded it to %s by the SdI.",
+ filename, partner_name),
+ 'forward_attempt': nl2br(escape(_(
+ "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.",
+ filename, partner_name))),
+ 'forward_failed': nl2br(escape(_(
+ "The e-invoice file %s couldn't be forwarded to %s.\n"
+ "Please remember to send it via post or e-mail.",
+ filename, partner_name)))
+ }
+ return new_state_messages_map.get(new_state)
diff --git a/addons/l10n_it_edi/models/account_tax.py b/addons/l10n_it_edi/models/account_tax.py
new file mode 100644
index 00000000000..9f6ae34d1f3
--- /dev/null
+++ b/addons/l10n_it_edi/models/account_tax.py
@@ -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)
diff --git a/addons/l10n_it_edi/models/ir_attachment.py b/addons/l10n_it_edi/models/ir_attachment.py
index 331d23fc3b0..212ac57a75f 100644
--- a/addons/l10n_it_edi/models/ir_attachment.py
+++ b/addons/l10n_it_edi/models/ir_attachment.py
@@ -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'{y}" 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 = []
diff --git a/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge.xml b/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge.xml
index bce9af29a7b..3b0a1b986dc 100644
--- a/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge.xml
@@ -106,10 +106,5 @@
1600.80
-
- BILL2022030001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge_2.xml b/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge_2.xml
index cf77da88da7..aee789a8ff9 100644
--- a/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge_2.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge_2.xml
@@ -102,10 +102,5 @@
1600.80
-
- BILL2022030001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge_san_marino.xml b/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge_san_marino.xml
index 71f0c6a076c..ed025cdb69b 100644
--- a/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge_san_marino.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/bill_reverse_charge_san_marino.xml
@@ -102,10 +102,5 @@
1600.80
-
- BILL2022030001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/credit_note_negative_price.xml b/addons/l10n_it_edi/tests/export_xmls/credit_note_negative_price.xml
index 1cf0c81c38a..e371e10fffb 100644
--- a/addons/l10n_it_edi/tests/export_xmls/credit_note_negative_price.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/credit_note_negative_price.xml
@@ -107,10 +107,5 @@
I
-
- RINV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/credit_note_reverse_charge.xml b/addons/l10n_it_edi/tests/export_xmls/credit_note_reverse_charge.xml
index fe00f458ac9..62f78a7cfbd 100644
--- a/addons/l10n_it_edi/tests/export_xmls/credit_note_reverse_charge.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/credit_note_reverse_charge.xml
@@ -106,10 +106,5 @@
1600.80
-
- RBILL2022030001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_below_400_codice_simplified.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_below_400_codice_simplified.xml
index 9a21a46bf74..d402f6128df 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_below_400_codice_simplified.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_below_400_codice_simplified.xml
@@ -63,10 +63,5 @@
4.40
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_fully_discounted.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_fully_discounted.xml
index 39b3a6a650a..97457016780 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_fully_discounted.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_fully_discounted.xml
@@ -94,10 +94,5 @@
INV/2022/00001
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_negative_price.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_negative_price.xml
index 31076ca3622..d016a39dbb2 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_negative_price.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_negative_price.xml
@@ -112,10 +112,5 @@
INV/2022/00001
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_non_latin_and_latin.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_non_latin_and_latin.xml
index da582e01c5c..36f8d53536b 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_non_latin_and_latin.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_non_latin_and_latin.xml
@@ -76,7 +76,7 @@
2
- ?-
+ --1.00800.400000800.40
@@ -106,10 +106,5 @@
INV/2022/00001
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_partially_discounted.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_partially_discounted.xml
index 8f753cd43ea..41681a7beb9 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_partially_discounted.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_partially_discounted.xml
@@ -114,10 +114,5 @@
INV/2022/00001
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_price_included_taxes.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_price_included_taxes.xml
index c4584e232a9..2e3c1ec5ca7 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_price_included_taxes.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_price_included_taxes.xml
@@ -113,10 +113,5 @@
INV/2022/00001
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_reverse_charge.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_reverse_charge.xml
index ee2376076ec..3fe79adcff4 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_reverse_charge.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_reverse_charge.xml
@@ -101,10 +101,5 @@
___ignore___
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_total_400_VAT_simplified.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_total_400_VAT_simplified.xml
index 917156dc742..297d0337491 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_total_400_VAT_simplified.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_total_400_VAT_simplified.xml
@@ -53,10 +53,5 @@
72.13
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/export_xmls/invoice_zero_percent_taxes.xml b/addons/l10n_it_edi/tests/export_xmls/invoice_zero_percent_taxes.xml
index cf98abe1a56..565d99b01ca 100644
--- a/addons/l10n_it_edi/tests/export_xmls/invoice_zero_percent_taxes.xml
+++ b/addons/l10n_it_edi/tests/export_xmls/invoice_zero_percent_taxes.xml
@@ -102,10 +102,5 @@
INV/2022/00001
-
- INV202200001.pdf
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi/tests/test_edi_export.py b/addons/l10n_it_edi/tests/test_edi_export.py
index d4e19bfe845..dbe2a48af93 100644
--- a/addons/l10n_it_edi/tests/test_edi_export.py
+++ b/addons/l10n_it_edi/tests/test_edi_export.py
@@ -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({
diff --git a/addons/l10n_it_edi/tests/test_edi_import.py b/addons/l10n_it_edi/tests/test_edi_import.py
index 8b64cd00a73..e53fe8d58ac 100644
--- a/addons/l10n_it_edi/tests/test_edi_import.py
+++ b/addons/l10n_it_edi/tests/test_edi_import.py
@@ -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)
diff --git a/addons/l10n_it_edi/tools/remove_signature.py b/addons/l10n_it_edi/tools/remove_signature.py
index 69ce31993a6..33d9f87d940 100644
--- a/addons/l10n_it_edi/tools/remove_signature.py
+++ b/addons/l10n_it_edi/tools/remove_signature.py
@@ -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
diff --git a/addons/l10n_it_edi/views/l10n_it_view.xml b/addons/l10n_it_edi/views/l10n_it_view.xml
index 8e8db6a7bc3..f3a16f4d3e8 100644
--- a/addons/l10n_it_edi/views/l10n_it_view.xml
+++ b/addons/l10n_it_edi/views/l10n_it_view.xml
@@ -91,6 +91,7 @@
+
@@ -98,48 +99,12 @@
account.invoice.select.inheritaccount.move
-
+
-
-
-
-
-
- account.move.tree.inherit
- account.move
-
-
-
-
-
-
-
-
-
-
-
- account.move.tree.inherit
- account.move
-
-
-
-
-
-
-
-
-
-
- account.invoice.select.inherit
- account.move
-
-
-
-
-
+
@@ -150,23 +115,56 @@
20
-
-
-
-
+
+
+
+
+
+
+
+
+
E-invoicing (Italy)
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_it_edi/views/res_config_settings_views.xml b/addons/l10n_it_edi/views/res_config_settings_views.xml
index c8aff2aa226..17d38c879b2 100644
--- a/addons/l10n_it_edi/views/res_config_settings_views.xml
+++ b/addons/l10n_it_edi/views/res_config_settings_views.xml
@@ -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.
-
-
+
Allow Odoo to process invoices
By checking this box, I accept that Odoo may process my invoices.
-
+
An Official or Test service has been registered.
-
+
A Demo service is in use.
diff --git a/addons/l10n_it_edi/wizard/__init__.py b/addons/l10n_it_edi/wizard/__init__.py
index 30c60877ea7..586321184b3 100644
--- a/addons/l10n_it_edi/wizard/__init__.py
+++ b/addons/l10n_it_edi/wizard/__init__.py
@@ -1,2 +1,4 @@
# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from . import account_move_send
diff --git a/addons/l10n_it_edi/wizard/account_move_send.py b/addons/l10n_it_edi/wizard/account_move_send.py
index d77ea9cef8a..c2bfb707900 100644
--- a/addons/l10n_it_edi/wizard/account_move_send.py
+++ b/addons/l10n_it_edi/wizard/account_move_send.py
@@ -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'])
diff --git a/addons/l10n_it_edi/wizard/account_move_send_views.xml b/addons/l10n_it_edi/wizard/account_move_send_views.xml
new file mode 100644
index 00000000000..809dd8bac33
--- /dev/null
+++ b/addons/l10n_it_edi/wizard/account_move_send_views.xml
@@ -0,0 +1,27 @@
+
+
+
+ account.move.send.form.inherit.l10n_it_edi
+ account.move.send
+
+
+
+
+
+
+ Tax Agency (Italy)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_it_edi_pa/tests/test_edi_pa.py b/addons/l10n_it_edi_pa/tests/test_edi_pa.py
index 822991b18e1..86b1c71c824 100644
--- a/addons/l10n_it_edi_pa/tests/test_edi_pa.py
+++ b/addons/l10n_it_edi_pa/tests/test_edi_pa.py
@@ -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')
diff --git a/addons/l10n_it_edi_withholding/__init__.py b/addons/l10n_it_edi_withholding/__init__.py
index cf1cbac649b..a8ca4231bc7 100644
--- a/addons/l10n_it_edi_withholding/__init__.py
+++ b/addons/l10n_it_edi_withholding/__init__.py
@@ -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)
diff --git a/addons/l10n_it_edi_withholding/data/invoice_it_template.xml b/addons/l10n_it_edi_withholding/data/invoice_it_template.xml
index cc658c99ef5..1abc3dfe231 100644
--- a/addons/l10n_it_edi_withholding/data/invoice_it_template.xml
+++ b/addons/l10n_it_edi_withholding/data/invoice_it_template.xml
@@ -22,7 +22,7 @@
SI
-
+
diff --git a/addons/l10n_it_edi_withholding/models/__init__.py b/addons/l10n_it_edi_withholding/models/__init__.py
index 46ffb4e3873..9d5466c1400 100644
--- a/addons/l10n_it_edi_withholding/models/__init__.py
+++ b/addons/l10n_it_edi_withholding/models/__init__.py
@@ -4,4 +4,3 @@
from . import account_tax
from . import account_chart_template
from . import account_move
-from . import account_edi_format
diff --git a/addons/l10n_it_edi_withholding/models/account_edi_format.py b/addons/l10n_it_edi_withholding/models/account_edi_format.py
deleted file mode 100644
index 0f0f51c9551..00000000000
--- a/addons/l10n_it_edi_withholding/models/account_edi_format.py
+++ /dev/null
@@ -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 %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 %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 %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
diff --git a/addons/l10n_it_edi_withholding/models/account_move.py b/addons/l10n_it_edi_withholding/models/account_move.py
index 210d9fbc2da..5beadd517c2 100644
--- a/addons/l10n_it_edi_withholding/models/account_move.py
+++ b/addons/l10n_it_edi_withholding/models/account_move.py
@@ -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 %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 %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 %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
diff --git a/addons/l10n_it_edi_withholding/tests/export_xmls/enasarco_tax_invoice.xml b/addons/l10n_it_edi_withholding/tests/export_xmls/enasarco_tax_invoice.xml
index 03d389b79dc..8b9bb3091bb 100644
--- a/addons/l10n_it_edi_withholding/tests/export_xmls/enasarco_tax_invoice.xml
+++ b/addons/l10n_it_edi_withholding/tests/export_xmls/enasarco_tax_invoice.xml
@@ -143,10 +143,5 @@
___ignore___
-
- ___ignore___
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi_withholding/tests/export_xmls/pension_fund_tax_invoice.xml b/addons/l10n_it_edi_withholding/tests/export_xmls/pension_fund_tax_invoice.xml
index 764b0fd2401..6828c9fadcb 100644
--- a/addons/l10n_it_edi_withholding/tests/export_xmls/pension_fund_tax_invoice.xml
+++ b/addons/l10n_it_edi_withholding/tests/export_xmls/pension_fund_tax_invoice.xml
@@ -130,10 +130,5 @@
___ignore___
-
- ___ignore___
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi_withholding/tests/export_xmls/withholding_tax_invoice.xml b/addons/l10n_it_edi_withholding/tests/export_xmls/withholding_tax_invoice.xml
index 1243150db3a..d91d8bd27a1 100644
--- a/addons/l10n_it_edi_withholding/tests/export_xmls/withholding_tax_invoice.xml
+++ b/addons/l10n_it_edi_withholding/tests/export_xmls/withholding_tax_invoice.xml
@@ -123,10 +123,5 @@
___ignore___
-
- ___ignore___
- PDF
- ___ignore___
-
diff --git a/addons/l10n_it_edi_withholding/tests/test_withholding.py b/addons/l10n_it_edi_withholding/tests/test_withholding.py
index f7594c872f9..2da158812c1 100644
--- a/addons/l10n_it_edi_withholding/tests/test_withholding.py
+++ b/addons/l10n_it_edi_withholding/tests/test_withholding.py
@@ -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
diff --git a/addons/l10n_it_stock_ddt/models/__init__.py b/addons/l10n_it_stock_ddt/models/__init__.py
index 19f889ff02c..f346087e6fb 100644
--- a/addons/l10n_it_stock_ddt/models/__init__.py
+++ b/addons/l10n_it_stock_ddt/models/__init__.py
@@ -3,4 +3,3 @@
from . import stock_picking
from . import account_invoice
-from . import account_edi_format
diff --git a/addons/l10n_it_stock_ddt/models/account_edi_format.py b/addons/l10n_it_stock_ddt/models/account_edi_format.py
deleted file mode 100644
index 28559635c2b..00000000000
--- a/addons/l10n_it_stock_ddt/models/account_edi_format.py
+++ /dev/null
@@ -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
diff --git a/addons/l10n_it_stock_ddt/models/account_invoice.py b/addons/l10n_it_stock_ddt/models/account_invoice.py
index 66ea2340d92..a92ce368e30 100644
--- a/addons/l10n_it_stock_ddt/models/account_invoice.py
+++ b/addons/l10n_it_stock_ddt/models/account_invoice.py
@@ -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
diff --git a/addons/l10n_it_stock_ddt/tests/expected_xmls/deferred_invoice.xml b/addons/l10n_it_stock_ddt/tests/expected_xmls/deferred_invoice.xml
index 63d6af48110..d3d70ace4f7 100644
--- a/addons/l10n_it_stock_ddt/tests/expected_xmls/deferred_invoice.xml
+++ b/addons/l10n_it_stock_ddt/tests/expected_xmls/deferred_invoice.xml
@@ -127,6 +127,5 @@
INV/2020/00001
-
diff --git a/addons/l10n_it_stock_ddt/tests/test_edi.py b/addons/l10n_it_stock_ddt/tests/test_edi.py
index 810b70c21ac..49b378c5ffa 100644
--- a/addons/l10n_it_stock_ddt/tests/test_edi.py
+++ b/addons/l10n_it_stock_ddt/tests/test_edi.py
@@ -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": "",})
- expected = self._cleanup_etree(expected_xml, {"//DatiGeneraliDocumento/Numero": "",})
+ result = etree.fromstring(invoice_xml)
+ expected = etree.fromstring(expected_xml)
self.assertXmlTreeEqual(result, expected)
def _create_delivery(self, sale_order, qty=1):