diff --git a/l10n_it_edi_fatturhello/README.rst b/l10n_it_edi_fatturhello/README.rst new file mode 100644 index 000000000000..cdfddc2ed165 --- /dev/null +++ b/l10n_it_edi_fatturhello/README.rst @@ -0,0 +1,189 @@ +================================================ +ITA - Fattura elettronica - Supporto Fatturhello +================================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7fadf76cbaf92d73b8702e81dc6ae477b235457178106669e643b427c182743f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--italy-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-italy/tree/18.0/l10n_it_edi_fatturhello + :alt: OCA/l10n-italy +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-italy-18-0/l10n-italy-18-0-l10n_it_edi_fatturhello + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-italy&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +**Italiano** + +Questo modulo consente di inviare e ricevere i file XML della fattura +elettronica tramite Fatturhello (https://fatturhello.it). + +La ricezione avviene periodicamente, tramite il CRON "IT EDI: Ricezione +fatture dall'SdI". Ad ogni esecuzione tutte le fatture elettroniche +successive a quella configurata (a meno di duplicati) verranno +scaricate. + +La documentazione delle API è +https://documenter.getpostman.com/view/3340600/RWTiyLCz. + +**English** + +This module allows you to send and receive electronic invoice/bill XML +files via Fatturhello (https://fatturhello.it). + +The receipt happens periodically, with the native CRON "IT EDI: Receive +invoices from the SdI". During each execution all the E-Bills subsequent +the configured one (except for duplicates) will be downloaded. + +The API documentation is +https://documenter.getpostman.com/view/3340600/RWTiyLCz. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +**Italiano** + +In Fatturazione > Configurazione > Impostazioni, abilitare "Usa +Fatturhello" ed effettuare il login. + +Se alcune fatture di Fatturhello sono già presenti in Odoo, abilitare la +modalità debug e valorizzare "Identificativo dell'ultima fattura +scaricata" in Fatturazione > Configurazione > Impostazioni. Se +"Identificativo dell'ultima fattura scaricata" è vuoto, verranno +scaricate tutte le fatture elettroniche presenti in Fatturhello. + +*Fatturhello* + +Per inviare in automatico a SdI le fatture caricate da Odoo, abilitare +"Spedisci XML caricati". + +**English** + +In Invoicing > Configuration > Settings, enable "Use Fatturhello" and +login. + +If some Fatturhello electronic bills are already present in Odoo, enable +Debug mode and fill "Last downloaded E-Bill identifier" in Invoicing > +Configuration > Settings. If "Last downloaded E-Bill identifier" is +empty, all the E-Bills in Fatturhello will be downloaded. + +*Fatturhello* + +In order to automatically send invoices uploaded from Odoo to the ES, +enable "Spedisci XML caricati". + +Usage +===== + +**Italiano** + +Per ricevere le fatture elettroniche e aggiornare lo stato delle fatture +elettroniche inviate, attivare il CRON "IT EDI: Ricezione fatture +dall'SdI". + +Se una fattura rimane nello stato "Inviata a Fatturhello", vuol dire che +Fatturhello non è configurato per inviarla automaticamente allo SdI. In +questo caso: + +1. In Odoo: eliminare la fattura elettronica creata, +2. In Fatturhello: abilitare l'invio automatico a SdI, +3. In Odoo: ricreare la fattura elettronica e inviarla. + +**English** + +In order to receive the electronic bills and update the status of sent +electronic invoices, activate the "Import E-Bills from Fatturhello" +CRON. + +If an invoice remains in the "Sent to Fatturhello" status, it means that +Fatturhello is not configured to automatically send it to the ES. In +this case: + +1. In Odoo: Delete the created electronic invoice, +2. In Fatturhello: Enable automatic sending to ES, +3. In Odoo: Recreate the electronic invoice and send it. + +Known issues / Roadmap +====================== + +- Parse more types of status notification + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* PyTech + +Contributors +------------ + +- `PyTech `__: + + - Simone Rubino + +Other credits +------------- + +The development of this module has been financially supported by: + +- Northern Power Systems SRL (https://www.nps100.com) + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-HekkiMelody| image:: https://github.com/HekkiMelody.png?size=40px + :target: https://github.com/HekkiMelody + :alt: HekkiMelody +.. |maintainer-SirPyTech| image:: https://github.com/SirPyTech.png?size=40px + :target: https://github.com/SirPyTech + :alt: SirPyTech + +Current `maintainers `__: + +|maintainer-HekkiMelody| |maintainer-SirPyTech| + +This module is part of the `OCA/l10n-italy `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/l10n_it_edi_fatturhello/__init__.py b/l10n_it_edi_fatturhello/__init__.py new file mode 100644 index 000000000000..d6ade57604db --- /dev/null +++ b/l10n_it_edi_fatturhello/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models +from . import wizards diff --git a/l10n_it_edi_fatturhello/__manifest__.py b/l10n_it_edi_fatturhello/__manifest__.py new file mode 100644 index 000000000000..a3e286edbd73 --- /dev/null +++ b/l10n_it_edi_fatturhello/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "ITA - Fattura elettronica - Supporto Fatturhello", + "version": "18.0.1.0.0", + "category": "Localization/Italy", + "summary": "Invio e ricezione fatture elettroniche tramite Fatturhello", + "author": "PyTech, Odoo Community Association (OCA)", + "maintainers": [ + "HekkiMelody", + "SirPyTech", + ], + "website": "https://github.com/OCA/l10n-italy", + "license": "AGPL-3", + "depends": [ + "l10n_it_edi_sdi", + ], + "data": [ + "security/ir.model.access.csv", + "views/account_move_views.xml", + "views/res_config_settings_views.xml", + "wizards/login_views.xml", + ], +} diff --git a/l10n_it_edi_fatturhello/data/neutralize.sql b/l10n_it_edi_fatturhello/data/neutralize.sql new file mode 100644 index 000000000000..d9ca6ef2129a --- /dev/null +++ b/l10n_it_edi_fatturhello/data/neutralize.sql @@ -0,0 +1,3 @@ +-- Remove token for Fatturhello authentication +UPDATE res_company +SET fatturhello_login_authtoken = null; diff --git a/l10n_it_edi_fatturhello/i18n/it.po b/l10n_it_edi_fatturhello/i18n/it.po new file mode 100644 index 000000000000..58b645e722c0 --- /dev/null +++ b/l10n_it_edi_fatturhello/i18n/it.po @@ -0,0 +1,538 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_it_fatturapa_fatturhello +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-05-11 16:45+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_ids +msgid "Activities" +msgstr "Attività" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decorazione eccezione attività" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_state +msgid "Activity State" +msgstr "Stato attività" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icona tipo attività" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__fatturhello_last_processed_status_datetime +msgid "" +"All the status updates more recent than this date will be processed.\n" +"If empty, all the status updates will be processed." +msgstr "" +"Tutti gli aggiornamenti di stato successivi a questa data verranno " +"elaborati.\n" +"Se il campo è vuoto, verranno elaborati tutti gli aggiornamenti di stato." + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Base URL" +msgstr "URL base" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Base URL is mandatory." +msgstr "L'URL base è obbligatorio." + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.fatturhello_login_view_form +msgid "Cancel" +msgstr "Annulla" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__channel_id +msgid "Channel" +msgstr "Canale" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__channel_type +msgid "Channels (Pec, Web, Sftp) could be provided by external modules." +msgstr "I canali (PEC, Web, SFTP) possono essere forniti da moduli esterni." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_res_company +msgid "Companies" +msgstr "Aziende" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/wizards/login.py:0 +#, python-format +msgid "Configure the base URL in order to login" +msgstr "Configurare l'URL base per accedere" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.fatturhello_login_view_form +msgid "Confirm" +msgstr "Conferma" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Download failed.\n" +"Response is:\n" +"%s" +msgstr "" +"Scaricamento fallito.\n" +"La risposta è:\n" +"%s" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_account_bank_statement_line__fatturapa_state +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_account_move__fatturapa_state +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_account_payment__fatturapa_state +msgid "E-invoice State" +msgstr "Stato e-fattura" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_sdi_channel +msgid "ES channel" +msgstr "Canale SdI" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__channel_type +msgid "ES channel type" +msgstr "Tipo canale SdI" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_fatturapa_attachment_out +msgid "Electronic Invoice" +msgstr "Fattura elettronica" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields.selection,name:l10n_it_fatturapa_fatturhello.selection__sdi_channel__channel_type__fatturhello +#: model:sdi.channel,name:l10n_it_fatturapa_fatturhello.sdi_channel_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_fatturapa_out_attachment_form +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Fatturhello" +msgstr "Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_base_url +msgid "Fatturhello Base Url" +msgstr "URL base Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_res_company__fatturhello_identifier +msgid "Fatturhello Identifier" +msgstr "Identificatore Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_res_company__fatturhello_last_downloaded_e_bill_identifer +msgid "Fatturhello Last Downloaded E Bill Identifer" +msgstr "Identificatore ultima e-fattura Fatturhello scaricata" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_login_authtoken +msgid "Fatturhello Login Authtoken" +msgstr "Authtoken accesso Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_login_authtoken_create_date +msgid "Fatturhello Login Authtoken Create Date" +msgstr "Data creazione authtoken accesso Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__fatturhello_protocol +msgid "Fatturhello Protocol" +msgstr "Protocollo Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_username +msgid "Fatturhello Username" +msgstr "Nome utente Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_res_company__fatturhello_last_downloaded_e_bill_identifer +msgid "" +"Fatturhello's protocol number of the last E-Bill downloaded.\n" +"All the E-Bills subsequent this one will be downloaded during the CRON execution.\n" +"If empty, all the E-Bills in Fatturhello will be downloaded." +msgstr "" +"Numero di protocollo di Fatturhello dell'ultima fattura elettronica " +"scaricata.\n" +"Tutte le fatture elettroniche successive a questa verranno scaricate durante " +"l'esecuzione del CRON.\n" +"Se si vuole, verranno scaricate tutte le fatture elettroniche presenti in " +"Fatturhello." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icona Font Awesome es. fa-tasks" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__id +msgid "ID" +msgstr "ID" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_exception_icon +msgid "Icon" +msgstr "Icona" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icona per indicare un'attività eccezione." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__fatturhello_protocol +msgid "Identifier assigned during upload to Fatturhello." +msgstr "Identificatore assegnato durante il caricamento in Fatturhello." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_res_company__fatturhello_identifier +msgid "" +"Identifier of this company in Fatturhello, set automatically during login." +msgstr "" +"Identificativo di questa azienda in Fatturhello, impostato automaticamente " +"al momento dell'accesso." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.actions.server,name:l10n_it_fatturapa_fatturhello.ir_cron_fatturhello_import_e_bills_ir_actions_server +#: model:ir.cron,cron_name:l10n_it_fatturapa_fatturhello.ir_cron_fatturhello_import_e_bills +msgid "Import E-Bills from Fatturhello" +msgstr "Importa fattura elettronica da Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_account_move +msgid "Journal Entry" +msgstr "Registrazione contabile" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_company_form_e_invoice_sdi +msgid "Last downloaded E-Bill identifier" +msgstr "Ultimo identificatore fattura elettronica scaricato" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__fatturhello_last_processed_status_datetime +msgid "Last processed Fatturhello status" +msgstr "Stato ultimo Fatturhello elaborato" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_fatturapa_out_attachment_form +msgid "Last processed status" +msgstr "Ultimo stato elaborato" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"List Download failed.\n" +"Response is:\n" +"%s" +msgstr "" +"Scaricamento della lista non riuscito.\n" +"La risposta è:\n" +"%s" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Login" +msgstr "Login" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Login Auth Token" +msgstr "Token autorizzazione accesso" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Login Auth Token creation date" +msgstr "Data creazione token autorizzazione accesso" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_l10n_it_fatturapa_fatturhello_login +msgid "Login to Fatturhello" +msgstr "Accesso a Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Login with token failed.\n" +"Response is:\n" +"%s" +msgstr "" +"Accesso con token fallito.\n" +"La risposta è:\n" +"%s" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Login with user and password failed.\n" +"Response is:\n" +"%s" +msgstr "" +"Accesso con utente e password fallito.\n" +"La risposta è:\n" +"%s" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_l10n_it_fatturapa_fatturhello_connector +msgid "Manage communication with Fatturhello API." +msgstr "Gestione comunicazione con API Fatturhello." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Scadenza mia attività" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Scadenza prossima attività" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_summary +msgid "Next Activity Summary" +msgstr "Riepilogo prossima attività" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo prossima attività" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Notification type %(notification_type)s not managed" +msgstr "Il tipo notifica %(notification_type)s non è gestito" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__password +msgid "Password" +msgstr "Password" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Password is mandatory." +msgstr "La password è obbligatoria." + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Path %s not found." +msgstr "Percorso %s non trovato." + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/wizards/login.py:0 +#, python-format +msgid "Please select a channel to login to" +msgstr "Selezionare un canale cui accedere" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_fatturapa_out_attachment_form +msgid "Protocol" +msgstr "Protcollo" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/sdi_channel.py:0 +#, python-format +msgid "Renew login" +msgstr "Aggiorna accesso" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Request failed, see the logs for more details." +msgstr "Richiesta fallita, visualizza i log per ulteriori dettagli." + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Response creation failed, see the logs for more details." +msgstr "Creazione risposta fallita, vedere i log per ulteriori dettagli." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_user_id +msgid "Responsible User" +msgstr "Utente responsabile" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields.selection,name:l10n_it_fatturapa_fatturhello.selection__account_move__fatturapa_state__sent_to_fatturhello +#: model:ir.model.fields.selection,name:l10n_it_fatturapa_fatturhello.selection__fatturapa_attachment_out__state__sent_to_fatturhello +msgid "Sent to Fatturhello" +msgstr "Inviato a Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__state +msgid "State" +msgstr "Stato" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Stato in base alle attività\n" +"Scaduto: la data richiesta è trascorsa\n" +"Oggi: la data attività è oggi\n" +"Pianificato: attività future." + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Status check failed.\n" +"Response is:\n" +"%s" +msgstr "" +"Fallito controllo stato.\n" +"La risposta è:\n" +"%s" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"The company '%(company)s' does not match any company in this Fatturhello account.\n" +"Please check that VAT '%(vat)s' and FC '%(fc)s' correspond." +msgstr "" +"L'azienda '%(company)s' non corrisponde a nessuna azienda presente in questo " +"account Fatturhello.\n" +"Si prega di verificare che il codice IVA '%(vat)s' e il codice FC '%(fc)s' " +"corrispondano." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_login_authtoken +msgid "Token can only be set during login. Login again to update the token." +msgstr "" +"Il token può essere impostato solo al momento dell'accesso. Effettuare " +"nuovamente l'accesso per aggiornare il token." + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Token is mandatory." +msgstr "Il token è obbligatorio." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo di attività eccezione sul record." + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.actions.server,name:l10n_it_fatturapa_fatturhello.ir_cron_fatturhello_update_e_invoices_status_ir_actions_server +#: model:ir.cron,cron_name:l10n_it_fatturapa_fatturhello.ir_cron_fatturhello_update_e_invoices_status +msgid "Update the status of E-Invoices uploaded to Fatturhello" +msgstr "Aggiorna lo stato delle fatture elettroniche caricate su Fatturhello" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Upload failed.\n" +"Response is:\n" +"%s" +msgstr "" +"Caricamento fallito.\n" +"La risposta è:\n" +"%s" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__username +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Username" +msgstr "Nome utente" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_username +msgid "Username can only be set during login." +msgstr "Il nome utente può essere impostato durante l'accesso." + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Username is mandatory." +msgstr "Il nome utente è obbligatorio." + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_company_form_e_invoice_sdi +msgid "YYYYXXXXXXXX" +msgstr "YYYYXXXXXXXX" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/wizards/login.py:0 +#, python-format +msgid "You can only login to Fatturhello channels" +msgstr "Si può effettuare l'accesso solo al canale Fatturhello" diff --git a/l10n_it_edi_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot b/l10n_it_edi_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot new file mode 100644 index 000000000000..c9e548742f18 --- /dev/null +++ b/l10n_it_edi_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot @@ -0,0 +1,496 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_it_fatturapa_fatturhello +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_ids +msgid "Activities" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_state +msgid "Activity State" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__fatturhello_last_processed_status_datetime +msgid "" +"All the status updates more recent than this date will be processed.\n" +"If empty, all the status updates will be processed." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Base URL" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Base URL is mandatory." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.fatturhello_login_view_form +msgid "Cancel" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__channel_id +msgid "Channel" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__channel_type +msgid "Channels (Pec, Web, Sftp) could be provided by external modules." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_res_company +msgid "Companies" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/wizards/login.py:0 +#, python-format +msgid "Configure the base URL in order to login" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.fatturhello_login_view_form +msgid "Confirm" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__create_uid +msgid "Created by" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__create_date +msgid "Created on" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__display_name +msgid "Display Name" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Download failed.\n" +"Response is:\n" +"%s" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_account_bank_statement_line__fatturapa_state +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_account_move__fatturapa_state +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_account_payment__fatturapa_state +msgid "E-invoice State" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_sdi_channel +msgid "ES channel" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__channel_type +msgid "ES channel type" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_fatturapa_attachment_out +msgid "Electronic Invoice" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields.selection,name:l10n_it_fatturapa_fatturhello.selection__sdi_channel__channel_type__fatturhello +#: model:sdi.channel,name:l10n_it_fatturapa_fatturhello.sdi_channel_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_fatturapa_out_attachment_form +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Fatturhello" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_base_url +msgid "Fatturhello Base Url" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_res_company__fatturhello_identifier +msgid "Fatturhello Identifier" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_res_company__fatturhello_last_downloaded_e_bill_identifer +msgid "Fatturhello Last Downloaded E Bill Identifer" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_login_authtoken +msgid "Fatturhello Login Authtoken" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_login_authtoken_create_date +msgid "Fatturhello Login Authtoken Create Date" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__fatturhello_protocol +msgid "Fatturhello Protocol" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_username +msgid "Fatturhello Username" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_res_company__fatturhello_last_downloaded_e_bill_identifer +msgid "" +"Fatturhello's protocol number of the last E-Bill downloaded.\n" +"All the E-Bills subsequent this one will be downloaded during the CRON execution.\n" +"If empty, all the E-Bills in Fatturhello will be downloaded." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__id +msgid "ID" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__fatturhello_protocol +msgid "Identifier assigned during upload to Fatturhello." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_res_company__fatturhello_identifier +msgid "" +"Identifier of this company in Fatturhello, set automatically during login." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.actions.server,name:l10n_it_fatturapa_fatturhello.ir_cron_fatturhello_import_e_bills_ir_actions_server +#: model:ir.cron,cron_name:l10n_it_fatturapa_fatturhello.ir_cron_fatturhello_import_e_bills +msgid "Import E-Bills from Fatturhello" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login____last_update +msgid "Last Modified on" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__write_date +msgid "Last Updated on" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_company_form_e_invoice_sdi +msgid "Last downloaded E-Bill identifier" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__fatturhello_last_processed_status_datetime +msgid "Last processed Fatturhello status" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_fatturapa_out_attachment_form +msgid "Last processed status" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"List Download failed.\n" +"Response is:\n" +"%s" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Login" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Login Auth Token" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Login Auth Token creation date" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_l10n_it_fatturapa_fatturhello_login +msgid "Login to Fatturhello" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Login with token failed.\n" +"Response is:\n" +"%s" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Login with user and password failed.\n" +"Response is:\n" +"%s" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model,name:l10n_it_fatturapa_fatturhello.model_l10n_it_fatturapa_fatturhello_connector +msgid "Manage communication with Fatturhello API." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Notification type %(notification_type)s not managed" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__password +msgid "Password" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Password is mandatory." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Path %s not found." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/wizards/login.py:0 +#, python-format +msgid "Please select a channel to login to" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_fatturapa_out_attachment_form +msgid "Protocol" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/sdi_channel.py:0 +#, python-format +msgid "Renew login" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Request failed, see the logs for more details." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Response creation failed, see the logs for more details." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields.selection,name:l10n_it_fatturapa_fatturhello.selection__account_move__fatturapa_state__sent_to_fatturhello +#: model:ir.model.fields.selection,name:l10n_it_fatturapa_fatturhello.selection__fatturapa_attachment_out__state__sent_to_fatturhello +msgid "Sent to Fatturhello" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_fatturapa_attachment_out__state +msgid "State" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Status check failed.\n" +"Response is:\n" +"%s" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"The company '%(company)s' does not match any company in this Fatturhello account.\n" +"Please check that VAT '%(vat)s' and FC '%(fc)s' correspond." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_login_authtoken +msgid "Token can only be set during login. Login again to update the token." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Token is mandatory." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.actions.server,name:l10n_it_fatturapa_fatturhello.ir_cron_fatturhello_update_e_invoices_status_ir_actions_server +#: model:ir.cron,cron_name:l10n_it_fatturapa_fatturhello.ir_cron_fatturhello_update_e_invoices_status +msgid "Update the status of E-Invoices uploaded to Fatturhello" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "" +"Upload failed.\n" +"Response is:\n" +"%s" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,field_description:l10n_it_fatturapa_fatturhello.field_l10n_it_fatturapa_fatturhello_login__username +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_sdi_channel_form +msgid "Username" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model:ir.model.fields,help:l10n_it_fatturapa_fatturhello.field_sdi_channel__fatturhello_username +msgid "Username can only be set during login." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/models/connector.py:0 +#, python-format +msgid "Username is mandatory." +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#: model_terms:ir.ui.view,arch_db:l10n_it_fatturapa_fatturhello.view_company_form_e_invoice_sdi +msgid "YYYYXXXXXXXX" +msgstr "" + +#. module: l10n_it_fatturapa_fatturhello +#. odoo-python +#: code:addons/l10n_it_fatturapa_fatturhello/wizards/login.py:0 +#, python-format +msgid "You can only login to Fatturhello channels" +msgstr "" diff --git a/l10n_it_edi_fatturhello/migrations/18.0.1.0.0/post-migration.py b/l10n_it_edi_fatturhello/migrations/18.0.1.0.0/post-migration.py new file mode 100644 index 000000000000..c0a8e6ccc61f --- /dev/null +++ b/l10n_it_edi_fatturhello/migrations/18.0.1.0.0/post-migration.py @@ -0,0 +1,117 @@ +# Copyright 2026 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +def get_fatturhello_channel_ids(env): + env.cr.execute( + """ +SELECT id +FROM sdi_channel +WHERE channel_type = 'fatturhello' + """, + ) + return env.cr.fetchall() + + +def removed_channel(env): + """The ES Channel does not exist anymore. + + Move/remove the data of Fatturhello channels. + """ + if channel_ids := get_fatturhello_channel_ids(env): + # Move fields to the Company. + set_clause = ",\n".join( + f"{channel_field} = sdi_channel.{channel_field}" + for channel_field in [ + "fatturhello_base_url", + "fatturhello_username", + "fatturhello_login_authtoken", + "fatturhello_login_authtoken_create_date", + ] + ) + openupgrade.logged_query( + env.cr, + f""" +UPDATE res_company +SET + fatturhello_is_used = true, + {set_clause} +FROM sdi_channel +WHERE + res_company.sdi_channel_id = sdi_channel.id + AND sdi_channel.id = ANY(%(channel_ids)s) + """, + dict( + channel_ids=channel_ids, + ), + ) + + # Remove the mail activities, like "Renew Login". + openupgrade.logged_query( + env.cr, + """ +DELETE FROM mail_activity +WHERE + res_model = 'sdi.channel' + AND res_id = ANY(%(channel_ids)s) + """, + dict( + channel_ids=channel_ids, + ), + ) + + +def removed_attachment(env): + """The FatturaPA attachment does not exist anymore. + + Move/remove the data of Fatturhello attachments. + """ + # Fatturhello fields + set_clause = ",\n".join( + f"{attachment_field} = fatturapa_attachment_out.{attachment_field}" + for attachment_field in [ + "fatturhello_protocol", + "fatturhello_last_processed_status_datetime", + ] + ) + openupgrade.logged_query( + env.cr, + f""" +UPDATE account_move +SET + {set_clause} +FROM fatturapa_attachment_out + JOIN sdi_channel + ON fatturapa_attachment_out.channel_id = sdi_channel.id + JOIN ir_attachment + ON ir_attachment.res_model = 'account.move' + AND ir_attachment.id = fatturapa_attachment_out.ir_attachment_id +WHERE + sdi_channel.id = ANY(%(channel_ids)s) + AND account_move.id = ir_attachment.res_id + """, + dict( + channel_ids=get_fatturhello_channel_ids(env), + ), + ) + + # State field: this is not managed by other modules + # because they don't know how to translate it + openupgrade.logged_query( + env.cr, + """ +UPDATE account_move +SET + l10n_it_edi_state = 'sent_to_fatturhello' +WHERE + fatturapa_state = 'sent_to_fatturhello' + """, + ) + + +@openupgrade.migrate() +def migrate(env, version): + removed_channel(env) + removed_attachment(env) diff --git a/l10n_it_edi_fatturhello/models/__init__.py b/l10n_it_edi_fatturhello/models/__init__.py new file mode 100644 index 000000000000..50d15fe21ffd --- /dev/null +++ b/l10n_it_edi_fatturhello/models/__init__.py @@ -0,0 +1,6 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_move +from . import connector +from . import res_company +from . import res_config_settings diff --git a/l10n_it_edi_fatturhello/models/account_move.py b/l10n_it_edi_fatturhello/models/account_move.py new file mode 100644 index 000000000000..60c4a88a58be --- /dev/null +++ b/l10n_it_edi_fatturhello/models/account_move.py @@ -0,0 +1,336 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 + +from odoo import api, fields, models + +from odoo.addons.base.models.ir_qweb_fields import nl2br +from odoo.addons.l10n_it_edi.models.account_move import WAITING_STATES +from odoo.addons.l10n_it_edi_sdi.models.mail_thread import NOTIFICATION_TYPE_MAP + +NOTIFICATION_TYPE_MAP["FHSDI"] = "awaiting_outcome" + + +class AccountMove(models.Model): + _inherit = "account.move" + + fatturhello_protocol = fields.Char( + readonly=True, + help="Identifier assigned during upload to Fatturhello.", + ) + l10n_it_edi_state = fields.Selection( + selection_add=[ + # Before the `being_sent` status + ("sent_to_fatturhello", "Sent to Fatturhello"), + ("being_sent",), + ], + ondelete={ + "sent_to_fatturhello": "set being_sent", + }, + ) + fatturhello_last_processed_status_datetime = fields.Datetime( + string="Last processed Fatturhello status", + help="All the status updates more recent than this date will be processed.\n" + "If empty, all the status updates will be processed.", + ) + fatturhello_is_used = fields.Boolean( + related="company_id.fatturhello_is_used", + ) + + def _fatturhello_adapt_file_name(self, file_name): + """Fatturhello requires the file name to start with the country code (IT).""" + company = self.env.company + if file_name: + country_code = company.country_id.code + if country_code and not file_name.startswith(country_code): + file_name = country_code + file_name + return file_name + + def _l10n_it_edi_generate_filename(self): + file_vat = super()._l10n_it_edi_generate_filename() + company = self.company_id._l10n_it_get_edi_company() + if company.fatturhello_is_used: + file_vat = self._fatturhello_adapt_file_name(file_vat) + return file_vat + + def _fatturhello_get_e_invoice_identifier(self): + """How the uploaded invoice can be identified for API calls.""" + self.ensure_one() + return self.l10n_it_edi_attachment_id.name + + def _fatturhello_check_l10n_it_edi_state(self): + self.ensure_one() + if self.l10n_it_edi_state == "sent_to_fatturhello": + self._l10n_it_edi_update_send_state() + + def action_check_l10n_it_edi(self): + if self.company_id.fatturhello_is_used: + self._fatturhello_check_l10n_it_edi_state() + return super().action_check_l10n_it_edi() + + @api.model + def _fatturhello_import_e_bills_cron(self): + """Method executed by CRON to import E-Bills.""" + connector = self.env["l10n_it_edi_fatturhello.connector"] + # Not using sudo so that we can do only + # whatever the CRON's user can do + companies = self.env["res.company"].search( + [ + ("fatturhello_is_used", "=", True), + ] + ) + for company in companies: + # Login + url = company.fatturhello_base_url + session_token = connector.get_session_token( + url, + company.fatturhello_login_authtoken, + ) + + # Retrieve the identifiers of the E-Bills to be downloaded + company_identifier = company.fatturhello_identifier + e_bills_identifiers = connector.get_e_bills_list( + url, + session_token, + company_identifier, + last_downloaded_e_bill_identifer=company.fatturhello_last_downloaded_e_bill_identifer, + ) + file_name_content_list = [] + if e_bills_identifiers: + company.fatturhello_last_downloaded_e_bill_identifer = ( + e_bills_identifiers[-1] + ) + + # Download each E-Bill's data + file_name_content_list = list() + for e_bill_identifier in e_bills_identifiers: + file_name, file_content = connector.download_file( + url, + session_token, + company_identifier, + e_bill_identifier, + ) + file_name_content_list.append( + self.env["mail.thread"]._Attachment(file_name, file_content, {}) + ) + + # Create E-Bills + self.env["mail.thread"]._l10n_it_edi_sdi_process_incoming_invoices( + dict(), + file_name_content_list, + company, + ) + + def _l10n_it_edi_download_invoices(self, proxy_user): + # Fatturhello companies do not need a proxy user, + # so this method shouldn't be called. + # But if they had one, `super` would fail. + if proxy_user.company_id.fatturhello_is_used: + return True + return super()._l10n_it_edi_download_invoices(proxy_user) + + def _fatturhello_get_moves_for_status_update(self): + """Invoices in to be updated with Fatturhello data. + + Grouped by Company. + If called on a recordset, the invoices returned are a subset of the recordset. + """ + if self: + companies = self.company_id.filtered("fatturhello_is_used") + else: + companies = self.env["res.company"].search( + [ + ("fatturhello_is_used", "=", True), + ] + ) + + domain = [ + ("l10n_it_edi_state", "in", ("sent_to_fatturhello",) + WAITING_STATES), + ("company_id", "in", companies.ids), + ] + if self: + domain.append([("id", "in", self.ids)]) + + invoices_data = self.env["account.move"].search_read( + domain=domain, + fields=[ + "id", + "company_id", + ], + load=None, + ) + invoices_by_company = dict() + for invoice_data in invoices_data: + company = self.env["res.company"].browse(invoice_data["company_id"]) + move = self.env["account.move"].browse(invoice_data["id"]) + if company not in invoices_by_company: + invoices_by_company[company] = move + else: + invoices_by_company[company] |= move + return invoices_by_company + + def _fatturhello_l10n_it_edi_update_send_state(self): + """Update the status of E-Invoices in `self`, if needed. + + If `self` is an empty recordset (CRON), + update all the invoices in the current companies. + """ + connector = self.env["l10n_it_edi_fatturhello.connector"] + moves_by_company_id = self._fatturhello_get_moves_for_status_update() + for company, moves in moves_by_company_id.items(): + sdi_response_name_content_list = list() + # Login + url = company.fatturhello_base_url + session_token = connector.get_session_token( + url, + company.fatturhello_login_authtoken, + ) + + # Download each E-Invoice's status + move_to_statuses_dict = { + move: connector.get_e_invoice_status_list( + url, + session_token, + company.fatturhello_identifier, + move._fatturhello_get_e_invoice_identifier(), + ) + for move in moves + } + + # Parse the Fatturhello status into a SdI message + for move, statuses in move_to_statuses_dict.items(): + last_processed_status_datetime = ( + move.fatturhello_last_processed_status_datetime + ) + for status_index, status in enumerate(statuses): + status_datetime = status["Data"] + if ( + not last_processed_status_datetime + or status_datetime > last_processed_status_datetime + ): + # Process only if more recent than the last one processed + name, content = connector._prepare_sdi_notification( + move._fatturhello_get_e_invoice_identifier(), + status_index, + status, + ) + sdi_response_name_content_list.append( + self.env["mail.thread"]._Attachment(name, content, {}) + ) + + # Update moves + self.env["mail.thread"]._l10n_it_edi_sdi_process_notifications( + dict(), sdi_response_name_content_list, company=company + ) + + def _l10n_it_edi_update_send_state(self): + fatturhello_moves = self.filtered( + lambda move: move.company_id.fatturhello_is_used + ) + fatturhello_moves._fatturhello_l10n_it_edi_update_send_state() + + other_moves = self - fatturhello_moves + result = None + if other_moves: + result = super(AccountMove, other_moves)._l10n_it_edi_update_send_state() + return result + + def cron_l10n_it_edi_download_and_update(self): + self._fatturhello_import_e_bills_cron() + self._fatturhello_l10n_it_edi_update_send_state() + return super().cron_l10n_it_edi_download_and_update() + + def _fatturhello_l10n_it_edi_upload_single(self, file): + """Send the e-invoices in `self` using Fatturhello.""" + self.ensure_one() + connector = self.env["l10n_it_edi_fatturhello.connector"] + url = self.company_id.fatturhello_base_url + session_token = connector.get_session_token( + url, + self.company_id.fatturhello_login_authtoken, + ) + company_identifier = self.company_id.fatturhello_identifier + upload_result = connector.upload_e_invoice( + url, + session_token, + company_identifier, + self, + file["filename"], + file["xml"].encode(), + ) + return { + "message": "Sent to Fatturhello", + "id_transaction": upload_result["protocollo"], + "error": upload_result["error"], + } + + def _l10n_it_edi_upload_single(self, file): + if self.company_id.fatturhello_is_used: + return self._fatturhello_l10n_it_edi_upload_single(file) + return super()._l10n_it_edi_upload_single(file) + + def _fatturhello_l10n_it_edi_send(self, attachments_vals): + # Extracted from super's `_l10n_it_edi_send` + # because it does many proxy-related things + # that we can't easily avoid/undo + results = {} + for move in self: + attachment = attachments_vals[move] + filename = attachment["name"] + content = base64.b64encode(attachment["raw"]).decode() + + results[filename] = move._l10n_it_edi_upload( + [ + { + "filename": filename, + "xml": content, + "destination_code": move.commercial_partner_id.l10n_it_pa_index, + } + ] + )[filename] + + message = move.env._( + "The e-invoice file %s was sent to Fatturhello.", filename + ) + move.update( + { + "l10n_it_edi_header": message, + "l10n_it_edi_state": "sent_to_fatturhello", + } + ) + + header = nl2br(message) + move.sudo().message_post(body=header) + return results + + def _l10n_it_edi_send(self, attachments_vals): + results = {} + fatturhello_moves = self.filtered( + lambda move: move.company_id.fatturhello_is_used + ) + if fatturhello_moves: + results.update(self._fatturhello_l10n_it_edi_send(attachments_vals)) + + results.update( + super(AccountMove, self - fatturhello_moves)._l10n_it_edi_send( + attachments_vals + ) + ) + return results + + def _fatturhello_l10n_it_edi_write_send_state( + self, transformed_notification, message + ): + if receipt_datetime := transformed_notification.get("date"): + self.fatturhello_last_processed_status_datetime = receipt_datetime + + def _l10n_it_edi_write_send_state(self, transformed_notification, message): + result = super()._l10n_it_edi_write_send_state( + transformed_notification, message + ) + if self.company_id.fatturhello_is_used: + self._fatturhello_l10n_it_edi_write_send_state( + transformed_notification, message + ) + return result diff --git a/l10n_it_edi_fatturhello/models/connector.py b/l10n_it_edi_fatturhello/models/connector.py new file mode 100644 index 000000000000..a19e63a11b5b --- /dev/null +++ b/l10n_it_edi_fatturhello/models/connector.py @@ -0,0 +1,578 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +import logging +import re +import urllib +from datetime import datetime +from io import BytesIO + +import requests +from lxml import etree + +from odoo import _, exceptions, fields, models + +_logger = logging.getLogger(__name__) + +DOWNLOAD_BATCH_LIMIT = 10 +SENT_FROM_FATTURHELLO_NOTIFICATION_TYPE = "FHSDI" +TIMEOUT = 20 + + +class FatturhelloConnector(models.AbstractModel): + _name = "l10n_it_edi_fatturhello.connector" + _description = "Manage communication with Fatturhello API." + + def _get_relative_paths(self): + """Dictionary mapping all relative paths of managed API endpoints.""" + return { + ("login", "POST"): "b2beasy/login", + ("upload", "POST"): "b2beasy/Upload", + ("sdi_files", "GET"): "b2beasy/Interscambio/%(idazienda)s", + ("sdi_years", "GET"): "b2beasy/Interscambioanni/%(idazienda)s", + ("download", "GET"): "b2beasy/Allegati/%(idfile)s", + ("sdi_status", "GET"): "fatture/StatoSdi", + } + + def _get_url(self, base_url, path_key, path_values=None): + """Get the full URL of an API endpoint. + + :param path_key: Identifier of the endpoint + to be searched in the result of `_get_relative_paths` + :param path_values: Dictionary of values to be substituted in the path + """ + if not base_url: + raise exceptions.UserError(_("Base URL is mandatory.")) + + relative_paths = self._get_relative_paths() + relative_path = relative_paths.get(path_key) + if relative_path is None: + raise exceptions.UserError(_("Path %s not found.", path_key)) + elif path_values: + relative_path = relative_path % path_values + + # Otherwise the last path part + # is substituted by the relative path. + # See urllib.parse.urljoin docs. + if relative_path.startswith("/"): + relative_path = relative_path[1:] + if not base_url.endswith("/"): + base_url += "/" + + return urllib.parse.urljoin(base_url, relative_path) + + def _request(self, *args, **kwargs): + """Wrapper around `requests.request` to raise proper Odoo exceptions.""" + kwargs = kwargs or dict() + kwargs.setdefault("timeout", TIMEOUT) + try: + # Disable because timeout is actually in kwargs + # pylint: disable=external-request-timeout + response = requests.request(*args, **kwargs) + except requests.exceptions.RequestException as exc: + _logger.exception("Response creation failed.") + raise exceptions.UserError( + _("Response creation failed, see the logs for more details.") + ) from exc + else: + try: + response.raise_for_status() + except requests.exceptions.HTTPError as exc: + _logger.exception("Request failed.") + raise exceptions.UserError( + _("Request failed, see the logs for more details.") + ) from exc + + response.encoding = "utf-8-sig" + + return response + + def _get_company_data(self, company, companies_data): + """Extract the data of `company` from `companies_data`. + + :param company: Odoo's `res.company` record + :param companies_data: Fatturhello's list of dictionaries, + each representing the data of one company. + .""" + company.ensure_one() + company_vat, company_fc = company.vat, company.l10n_it_codice_fiscale + for company_data in companies_data: + if company_data["pi"] == company_vat and company_data["cf"] == company_fc: + break + else: + raise exceptions.UserError( + _( + "The company '%(company)s' does not match " + "any company in this Fatturhello account.\n" + "Please check that VAT '%(vat)s' and FC '%(fc)s' correspond.", + company=company.name, + vat=company_vat or "", + fc=company_fc or "", + ) + ) + return company_data + + def _login_user_pass(self, base_url, username, password): + """Call the API to login with user and password.""" + url = self._get_url(base_url, ("login", "POST")) + + if not username: + raise exceptions.UserError(_("Username is mandatory.")) + if not password: + raise exceptions.UserError(_("Password is mandatory.")) + + response = self._request( + "POST", + url, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps( + { + "username": username, + "password": password, + "createtoken": True, + } + ), + ) + + result = response.json() + if not result.get("success"): + raise exceptions.UserError( + _( + "Login with user and password failed.\nResponse is:\n%s", + json.dumps(result, indent=4), + ) + ) + + return result + + def get_secrets(self, url, company, username, password): + """Get secrets used to authenticate API calls.""" + login_result = self._login_user_pass(url, username, password) + company_data = self._get_company_data(company, login_result["aziende"]) + return { + "authtoken": login_result["authtoken"], + "company_identifier": company_data["codice"], + } + + def _login_token(self, base_url, authtoken): + """Call the API to login with authtoken.""" + url = self._get_url(base_url, ("login", "POST")) + + if not authtoken: + raise exceptions.UserError(_("Token is mandatory.")) + + response = self._request( + "POST", + url, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps( + { + "token": authtoken, + } + ), + ) + + result = response.json() + if not result.get("success"): + raise exceptions.UserError( + _( + "Login with token failed.\nResponse is:\n%s", + json.dumps(result, indent=4), + ) + ) + + return result + + def get_session_token(self, url, authtoken): + """Get session token for single API call authentication.""" + login_result = self._login_token(url, authtoken) + return login_result["token"] + + def _upload_post( + self, + base_url, + session_token, + company_identifier, + file_name, + file_content, + ): + """Upload a file for a company. + + :param company_identifier: The identifier of the Company + where the file will be uploaded. + """ + url = self._get_url(base_url, ("upload", "POST")) + + response = self._request( + "POST", + url, + headers={ + "Authorization": f"Bearer {session_token}", + }, + data={ + "idupload": company_identifier, + }, + files={ + "file": (file_name, BytesIO(file_content)), + }, + ) + + result = response.json() + if not result.get("success"): + raise exceptions.UserError( + _( + "Upload failed.\nResponse is:\n%s", + json.dumps(result, indent=4), + ) + ) + + return result + + def _prepare_move_upload_values(self, upload_result): + """Values to be updated on an Odoo `ir.attachment` after a successful upload.""" + return { + "fatturhello_protocol": upload_result["files"][0]["protocollo"], + "l10n_it_edi_state": "sent_to_fatturhello", + } + + def upload_e_invoice( + self, base_url, session_token, company_identifier, move, file_name, file_content + ): + """Upload an e-invoice for a company. + + :param company_identifier: The identifier of the Company + where the file will be uploaded. + """ + move.ensure_one() + upload_result = self._upload_post( + base_url, + session_token, + company_identifier, + file_name, + file_content, + ) + move.update(self._prepare_move_upload_values(upload_result)) + return upload_result + + def _get_sdi_files_list( + self, + base_url, + session_token, + company_identifier, + offset=None, + limit=None, + order=None, + year=None, + ): + """Get the list of SdI files for a company. + + :param company_identifier: The identifier of the Company in Fatturhello + owning the SdI files. + """ + url = self._get_url( + base_url, + ("sdi_files", "GET"), + path_values=dict(idazienda=company_identifier), + ) + + response = self._request( + "GET", + url, + headers={ + "Authorization": f"Bearer {session_token}", + }, + params={ + "offset": offset, + "limit": limit, + "order": order, + "anno": year, + }, + ) + + result = response.json() + if not result.get("success"): + raise exceptions.UserError( + _( + "List Download failed.\nResponse is:\n%s", + json.dumps(result, indent=4), + ) + ) + + return result + + def _filter_e_bills(self, files_data): + """Extract from `files_data` only the E-Bills to be imported.""" + return [ + row + for row in files_data + if row["statotesto"] == "ricevuta" and row["statopa"] == "3" + ] + + def _get_sdi_years( + self, + base_url, + session_token, + company_identifier, + ): + """Get the list of years that SdI has been used for a company. + + :param company_identifier: The identifier of the Company in Fatturhello. + """ + url = self._get_url( + base_url, + ("sdi_years", "GET"), + path_values=dict(idazienda=company_identifier), + ) + + response = self._request( + "GET", + url, + headers={ + "Authorization": f"Bearer {session_token}", + }, + ) + + return response.json() + + def get_e_bills_list( + self, + base_url, + session_token, + company_identifier, + last_downloaded_e_bill_identifer=None, + ): + """Get the list of identifiers of E-Bills to be downloaded. + + E-Bills are ordered from the oldest to the latest one. + + :param company_identifier: The identifier of the Company in Fatturhello + owning the E-Bills. + :param last_downloaded_e_bill_identifer: Identifier of the downloaded E-Bill. + If provided, only the identifiers of the E-Bills + more recent than the specified one will be dowloaded. + """ + e_bills_years = self._get_sdi_years( + base_url, + session_token, + company_identifier, + ) + # Order years from most recent + e_bills_years = sorted(e_bills_years, reverse=True) + + e_bills_identifiers = [] + found_last_downloaded_e_bill = False + for e_bills_year in e_bills_years: + # Download all the E-Bills of the year + # in batches, from the latest one, + # until we find the last we already downloaded (if any) + if found_last_downloaded_e_bill: + break + + offset = 0 + while files_data := self._get_sdi_files_list( + base_url, + session_token, + company_identifier, + offset=offset, + limit=DOWNLOAD_BATCH_LIMIT, + order="desc", + year=e_bills_year, + ).get("rows"): + if found_last_downloaded_e_bill: + break + offset += len(files_data) + 1 + + year_e_bills_data = self._filter_e_bills(files_data) + year_e_bills_identifiers = [ + e_bill_data["idallegatoxml"] for e_bill_data in year_e_bills_data + ] + + for e_bill_identifier in year_e_bills_identifiers: + if e_bill_identifier == last_downloaded_e_bill_identifer: + found_last_downloaded_e_bill = True + break + e_bills_identifiers.append(e_bill_identifier) + + # We have downloaded the E-Bills from the latest to the oldest + # so that we can stop when the `last_downloaded` is found, + # but we should process them from the oldest to the latest. + e_bills_identifiers.reverse() + return e_bills_identifiers + + def download_file( + self, + base_url, + session_token, + company_identifier, + file_identifier, + ): + """Download a file from a company. + + :param company_identifier: The identifier of the Company in Fatturhello + owning the file. + :param file_identifier: The identifier of the file to be downloaded. + """ + url = self._get_url( + base_url, + ("download", "GET"), + path_values=dict(idfile=file_identifier), + ) + + response = self._request( + "GET", + url, + headers={ + "Authorization": f"Bearer {session_token}", + }, + ) + + content = response.content + if not content: + raise exceptions.UserError( + _( + "Download failed.\nResponse is:\n%s", + content.decode(), + ) + ) + name = re.findall("filename=(.+)", response.headers["content-disposition"])[0] + return name, content + + def _get_sdi_file_status( + self, + base_url, + session_token, + company_identifier, + file_identifier, + ): + """Get the SdI status of a SdI file. + + :param company_identifier: The identifier of the Company in Fatturhello + owning the SdI file. + """ + url = self._get_url(base_url, ("sdi_status", "GET")) + + response = self._request( + "GET", + url, + headers={ + "Authorization": f"Bearer {session_token}", + }, + params={ + "identificativo": file_identifier, + }, + ) + + result = response.json() + if not result.get("success"): + raise exceptions.UserError( + _( + "Status check failed.\nResponse is:\n%s", + json.dumps(result, indent=4), + ) + ) + + return result + + def _prepare_sdi_notification_type(self, fatturhello_status): + # Determine the notification type + status_str = fatturhello_status["Esito"] + if status_str == "Inoltrata al Sdi": + # This is Fatturhello confirming + # that the invoice has been sent to SdI: + # it is not a notification from SdI + notification_type = SENT_FROM_FATTURHELLO_NOTIFICATION_TYPE + elif status_str.startswith("Fattura ricevuta"): + notification_type = "RC" + else: + # Consider anything else as a "Notifica di Scarto". + # This will be refined + # when we will know all possible Fatturhello statuses. + notification_type = "NS" + _logger.info( + f"Fatturhello status considered as a 'Notifica di Scarto': {status_str}" + ) + return notification_type + + def _prepare_sdi_notification( + self, + file_name, + fatturhello_status_index, + fatturhello_status, + ): + """Transform a status from Fatturhello in SdI Messaggi format.""" + notification_type = self._prepare_sdi_notification_type(fatturhello_status) + + # Build the notification + sdi_notification_name = ( + "_".join( + ( + file_name.split(".")[0], + notification_type, + str(fatturhello_status_index), + ) + ) + + ".xml" + ) + + status_date = fields.Datetime.to_string(fatturhello_status["Data"]) + status_str = fatturhello_status["Esito"] + if notification_type == SENT_FROM_FATTURHELLO_NOTIFICATION_TYPE: + # Build a dummy XML to follow the usual flow + response_content_root = etree.Element("ReceivedFromFatturhello") + etree.SubElement(response_content_root, "Descrizione").text = status_str + elif notification_type == "RC": + response_content_root = etree.Element("RicevutaConsegna") + etree.SubElement( + response_content_root, "DataOraConsegna" + ).text = status_date + elif notification_type == "NS": + response_content_root = etree.Element("NotificaScarto") + errors_list = etree.SubElement(response_content_root, "ListaErrori") + etree.SubElement(errors_list, "Descrizione").text = status_str + else: + raise exceptions.UserError( + _( + "Notification type %(notification_type)s not managed", + notification_type=notification_type, + ) + ) + + etree.SubElement(response_content_root, "NomeFile").text = file_name + etree.SubElement(response_content_root, "IdentificativoSdI") + etree.SubElement(response_content_root, "DataOraRicezione").text = status_date + etree.SubElement(response_content_root, "MessageId") + return sdi_notification_name, etree.tostring(response_content_root) + + def get_e_invoice_status_list( + self, + base_url, + session_token, + company_identifier, + file_identifier, + ): + """Get all the status updates of `file_identifier`. + + The statuses are ordered from oldest to latest. + """ + e_bill_status_list = self._get_sdi_file_status( + base_url, + session_token, + company_identifier, + file_identifier, + ).get("rows") + + for e_bill_status in e_bill_status_list: + e_bill_status["Data"] = datetime.strptime( + e_bill_status["Data"], + "%d/%m/%Y %H:%M:%S", + ) + + e_bill_status_list.sort( + key=lambda status: status["Data"], + ) + return e_bill_status_list diff --git a/l10n_it_edi_fatturhello/models/res_company.py b/l10n_it_edi_fatturhello/models/res_company.py new file mode 100644 index 000000000000..7399b3615ad0 --- /dev/null +++ b/l10n_it_edi_fatturhello/models/res_company.py @@ -0,0 +1,66 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + fatturhello_identifier = fields.Char( + readonly=True, + help="Identifier of this company in Fatturhello, " + "set automatically during login.", + ) + fatturhello_last_downloaded_e_bill_identifer = fields.Char( + help="Fatturhello's protocol number of the last E-Bill downloaded.\n" + "All the E-Bills subsequent this one will be downloaded " + "during the CRON execution.\n" + "If empty, all the E-Bills in Fatturhello will be downloaded.", + ) + fatturhello_is_used = fields.Boolean( + string="Use Fatturhello", + ) + fatturhello_base_url = fields.Char( + name="Fatturhello base URL", + default="https://app.b2beasy.it", + ) + fatturhello_username = fields.Char( + readonly=True, + help="Username can only be set during login.", + ) + fatturhello_login_authtoken = fields.Char( + readonly=True, + help="Token can only be set during login. Login again to update the token.", + ) + fatturhello_login_authtoken_create_date = fields.Date( + readonly=True, + ) + + def fatturhello_action_login(self): + """Action to open the login wizard.""" + login_wizard = self.env["l10n_it_edi_fatturhello.login"].with_context( + active_id=self.id, + ) + login_action = login_wizard.get_formview_action() + login_action.update( + { + "name": "Login", + "target": "new", + } + ) + return login_action + + def _fatturhello_update_credentials(self, credentials): + """Store the credentials for authenticating API calls.""" + self.ensure_one() + self.update( + { + "fatturhello_username": credentials["username"], + "fatturhello_login_authtoken": credentials["authtoken"], + "fatturhello_login_authtoken_create_date": fields.Date.today(), + } + ) + self.fatturhello_identifier = credentials["company_identifier"] + return True diff --git a/l10n_it_edi_fatturhello/models/res_config_settings.py b/l10n_it_edi_fatturhello/models/res_config_settings.py new file mode 100644 index 000000000000..a23ff5700ea7 --- /dev/null +++ b/l10n_it_edi_fatturhello/models/res_config_settings.py @@ -0,0 +1,38 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + fatturhello_identifier = fields.Char( + related="company_id.fatturhello_identifier", + ) + fatturhello_last_downloaded_e_bill_identifer = fields.Char( + related="company_id.fatturhello_last_downloaded_e_bill_identifer", + readonly=False, + ) + fatturhello_is_used = fields.Boolean( + related="company_id.fatturhello_is_used", + readonly=False, + ) + fatturhello_base_url = fields.Char( + related="company_id.fatturhello_base_url", + readonly=False, + ) + fatturhello_username = fields.Char( + related="company_id.fatturhello_username", + ) + fatturhello_login_authtoken = fields.Char( + related="company_id.fatturhello_login_authtoken", + ) + fatturhello_login_authtoken_create_date = fields.Date( + related="company_id.fatturhello_login_authtoken_create_date", + ) + + def fatturhello_action_login(self): + """Open the login wizard for the active company.""" + return self.env.company.fatturhello_action_login() diff --git a/l10n_it_edi_fatturhello/pyproject.toml b/l10n_it_edi_fatturhello/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/l10n_it_edi_fatturhello/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/l10n_it_edi_fatturhello/readme/CONFIGURE.md b/l10n_it_edi_fatturhello/readme/CONFIGURE.md new file mode 100644 index 000000000000..10297f7f84ff --- /dev/null +++ b/l10n_it_edi_fatturhello/readme/CONFIGURE.md @@ -0,0 +1,21 @@ +**Italiano** + +In Fatturazione > Configurazione > Impostazioni, abilitare "Usa Fatturhello" ed effettuare il login. + +Se alcune fatture di Fatturhello sono già presenti in Odoo, abilitare la modalità debug e valorizzare "Identificativo dell'ultima fattura scaricata" in Fatturazione > Configurazione > Impostazioni. +Se "Identificativo dell'ultima fattura scaricata" è vuoto, verranno scaricate tutte le fatture elettroniche presenti in Fatturhello. + +*Fatturhello* + +Per inviare in automatico a SdI le fatture caricate da Odoo, abilitare "Spedisci XML caricati". + +**English** + +In Invoicing > Configuration > Settings, enable "Use Fatturhello" and login. + +If some Fatturhello electronic bills are already present in Odoo, enable Debug mode and fill "Last downloaded E-Bill identifier" in Invoicing > Configuration > Settings. +If "Last downloaded E-Bill identifier" is empty, all the E-Bills in Fatturhello will be downloaded. + +*Fatturhello* + +In order to automatically send invoices uploaded from Odoo to the ES, enable "Spedisci XML caricati". diff --git a/l10n_it_edi_fatturhello/readme/CONTRIBUTORS.md b/l10n_it_edi_fatturhello/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..6e720b67df64 --- /dev/null +++ b/l10n_it_edi_fatturhello/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/l10n_it_edi_fatturhello/readme/CREDITS.md b/l10n_it_edi_fatturhello/readme/CREDITS.md new file mode 100644 index 000000000000..c1cba3ffaa31 --- /dev/null +++ b/l10n_it_edi_fatturhello/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +- Northern Power Systems SRL (https://www.nps100.com) diff --git a/l10n_it_edi_fatturhello/readme/DESCRIPTION.md b/l10n_it_edi_fatturhello/readme/DESCRIPTION.md new file mode 100644 index 000000000000..6b4e69f9aa75 --- /dev/null +++ b/l10n_it_edi_fatturhello/readme/DESCRIPTION.md @@ -0,0 +1,19 @@ +**Italiano** + +Questo modulo consente di inviare e ricevere i file XML della +fattura elettronica tramite Fatturhello (https://fatturhello.it). + +La ricezione avviene periodicamente, tramite il CRON "IT EDI: Ricezione fatture dall'SdI". +Ad ogni esecuzione tutte le fatture elettroniche successive a quella configurata (a meno di duplicati) verranno scaricate. + +La documentazione delle API è https://documenter.getpostman.com/view/3340600/RWTiyLCz. + +**English** + +This module allows you to send and receive electronic +invoice/bill XML files via Fatturhello (https://fatturhello.it). + +The receipt happens periodically, with the native CRON "IT EDI: Receive invoices from the SdI". +During each execution all the E-Bills subsequent the configured one (except for duplicates) will be downloaded. + +The API documentation is https://documenter.getpostman.com/view/3340600/RWTiyLCz. diff --git a/l10n_it_edi_fatturhello/readme/ROADMAP.md b/l10n_it_edi_fatturhello/readme/ROADMAP.md new file mode 100644 index 000000000000..b369c6a71093 --- /dev/null +++ b/l10n_it_edi_fatturhello/readme/ROADMAP.md @@ -0,0 +1 @@ +- Parse more types of status notification diff --git a/l10n_it_edi_fatturhello/readme/USAGE.md b/l10n_it_edi_fatturhello/readme/USAGE.md new file mode 100644 index 000000000000..8718380b9d40 --- /dev/null +++ b/l10n_it_edi_fatturhello/readme/USAGE.md @@ -0,0 +1,19 @@ +**Italiano** + +Per ricevere le fatture elettroniche e aggiornare lo stato delle fatture elettroniche inviate, attivare il CRON "IT EDI: Ricezione fatture dall'SdI". + +Se una fattura rimane nello stato "Inviata a Fatturhello", vuol dire che Fatturhello non è configurato per inviarla automaticamente allo SdI. +In questo caso: +1. In Odoo: eliminare la fattura elettronica creata, +2. In Fatturhello: abilitare l'invio automatico a SdI, +3. In Odoo: ricreare la fattura elettronica e inviarla. + +**English** + +In order to receive the electronic bills and update the status of sent electronic invoices, activate the "Import E-Bills from Fatturhello" CRON. + +If an invoice remains in the "Sent to Fatturhello" status, it means that Fatturhello is not configured to automatically send it to the ES. +In this case: +1. In Odoo: Delete the created electronic invoice, +2. In Fatturhello: Enable automatic sending to ES, +3. In Odoo: Recreate the electronic invoice and send it. diff --git a/l10n_it_edi_fatturhello/security/ir.model.access.csv b/l10n_it_edi_fatturhello/security/ir.model.access.csv new file mode 100644 index 000000000000..753ad2046474 --- /dev/null +++ b/l10n_it_edi_fatturhello/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +model_access_login_manager,Users can manage Login to Fatturhello,l10n_it_edi_fatturhello.model_l10n_it_edi_fatturhello_login,account.group_account_manager,1,1,1,1 diff --git a/l10n_it_edi_fatturhello/static/description/icon.png b/l10n_it_edi_fatturhello/static/description/icon.png new file mode 100644 index 000000000000..1dcc49c24f36 Binary files /dev/null and b/l10n_it_edi_fatturhello/static/description/icon.png differ diff --git a/l10n_it_edi_fatturhello/static/description/index.html b/l10n_it_edi_fatturhello/static/description/index.html new file mode 100644 index 000000000000..8a114c81f0ce --- /dev/null +++ b/l10n_it_edi_fatturhello/static/description/index.html @@ -0,0 +1,512 @@ + + + + + +ITA - Fattura elettronica - Supporto Fatturhello + + + +
+

ITA - Fattura elettronica - Supporto Fatturhello

+ + +

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

+

Italiano

+

Questo modulo consente di inviare e ricevere i file XML della fattura +elettronica tramite Fatturhello (https://fatturhello.it).

+

La ricezione avviene periodicamente, tramite il CRON “IT EDI: Ricezione +fatture dall’SdI”. Ad ogni esecuzione tutte le fatture elettroniche +successive a quella configurata (a meno di duplicati) verranno +scaricate.

+

La documentazione delle API è +https://documenter.getpostman.com/view/3340600/RWTiyLCz.

+

English

+

This module allows you to send and receive electronic invoice/bill XML +files via Fatturhello (https://fatturhello.it).

+

The receipt happens periodically, with the native CRON “IT EDI: Receive +invoices from the SdI”. During each execution all the E-Bills subsequent +the configured one (except for duplicates) will be downloaded.

+

The API documentation is +https://documenter.getpostman.com/view/3340600/RWTiyLCz.

+

Table of contents

+ +
+

Configuration

+

Italiano

+

In Fatturazione > Configurazione > Impostazioni, abilitare “Usa +Fatturhello” ed effettuare il login.

+

Se alcune fatture di Fatturhello sono già presenti in Odoo, abilitare la +modalità debug e valorizzare “Identificativo dell’ultima fattura +scaricata” in Fatturazione > Configurazione > Impostazioni. Se +“Identificativo dell’ultima fattura scaricata” è vuoto, verranno +scaricate tutte le fatture elettroniche presenti in Fatturhello.

+

Fatturhello

+

Per inviare in automatico a SdI le fatture caricate da Odoo, abilitare +“Spedisci XML caricati”.

+

English

+

In Invoicing > Configuration > Settings, enable “Use Fatturhello” and +login.

+

If some Fatturhello electronic bills are already present in Odoo, enable +Debug mode and fill “Last downloaded E-Bill identifier” in Invoicing > +Configuration > Settings. If “Last downloaded E-Bill identifier” is +empty, all the E-Bills in Fatturhello will be downloaded.

+

Fatturhello

+

In order to automatically send invoices uploaded from Odoo to the ES, +enable “Spedisci XML caricati”.

+
+
+

Usage

+

Italiano

+

Per ricevere le fatture elettroniche e aggiornare lo stato delle fatture +elettroniche inviate, attivare il CRON “IT EDI: Ricezione fatture +dall’SdI”.

+

Se una fattura rimane nello stato “Inviata a Fatturhello”, vuol dire che +Fatturhello non è configurato per inviarla automaticamente allo SdI. In +questo caso:

+
    +
  1. In Odoo: eliminare la fattura elettronica creata,
  2. +
  3. In Fatturhello: abilitare l’invio automatico a SdI,
  4. +
  5. In Odoo: ricreare la fattura elettronica e inviarla.
  6. +
+

English

+

In order to receive the electronic bills and update the status of sent +electronic invoices, activate the “Import E-Bills from Fatturhello” +CRON.

+

If an invoice remains in the “Sent to Fatturhello” status, it means that +Fatturhello is not configured to automatically send it to the ES. In +this case:

+
    +
  1. In Odoo: Delete the created electronic invoice,
  2. +
  3. In Fatturhello: Enable automatic sending to ES,
  4. +
  5. In Odoo: Recreate the electronic invoice and send it.
  6. +
+
+
+

Known issues / Roadmap

+
    +
  • Parse more types of status notification
  • +
+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • PyTech
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainers:

+

HekkiMelody SirPyTech

+

This module is part of the OCA/l10n-italy project on GitHub.

+

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

+
+
+
+ + diff --git a/l10n_it_edi_fatturhello/tests/__init__.py b/l10n_it_edi_fatturhello/tests/__init__.py new file mode 100644 index 000000000000..5007d588dca5 --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_download +from . import test_login +from . import test_upload diff --git a/l10n_it_edi_fatturhello/tests/common.py b/l10n_it_edi_fatturhello/tests/common.py new file mode 100644 index 000000000000..4b90c691176c --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/common.py @@ -0,0 +1,83 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +from os.path import join +from unittest import mock + +from requests import Response, codes + +from odoo.tools import file_open, file_path + +from odoo.addons.l10n_it_edi.tests.common import TestItEdi + +MODULE = "l10n_it_edi_fatturhello" +REQUEST_PATH = f"odoo.addons.{MODULE}.models.connector.requests.request" + + +class Common(TestItEdi): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company.update( + { + "fatturhello_is_used": True, + "l10n_it_codice_fiscale": "06363391001", + "vat": "06363391001", + } + ) + cls.cron = cls.env.ref("l10n_it_edi.ir_cron_l10n_it_edi_download_and_update") + cls.cron.user_id.company_ids |= cls.company + cls.module = MODULE + + def _get_records_from_action(self, action, values=None): + """Get the records opened by `action`. + + If `action` does not return any record, one is created using `values`. + """ + context = action.get("context", dict()) + record_model = self.env[action["res_model"]].with_context(**context) + if record_id := action.get("res_id"): + record = record_model.browse(record_id) + else: + record = record_model.create(values or dict()) + return record + + def _get_file_content(self, name): + """Get the binary content of the file `name` in test responses.""" + path = file_path(join(MODULE, "tests", "responses", name)) + with file_open(path, mode="rb") as f: + return f.read() + + def _get_response(self, name, headers_name=None): + """Get a response for the file `name` in test responses.""" + response = Response() + response.status_code = codes.ok + response._content = self._get_file_content(name) + if headers_name: + response.headers = json.loads(self._get_file_content(headers_name).decode()) + return response + + def _get_login_wizard(self, company, values=None): + """Return the login wizard for `company`, populated with `values`. + + If `values` is omitted, default values will be used. + """ + if values is None: + values = { + "username": "username", + "password": "password", + } + action = ( + self.env["res.config.settings"] + .with_company(company) + .fatturhello_action_login() + ) + login_wizard = self._get_records_from_action(action, values=values) + return login_wizard + + def _login(self, company, values=None): + wizard = self._get_login_wizard(company) + with mock.patch(REQUEST_PATH) as mock_request: + mock_request.return_value = self._get_response("login_success") + return wizard.confirm() diff --git a/l10n_it_edi_fatturhello/tests/responses/download_file_content_success b/l10n_it_edi_fatturhello/tests/responses/download_file_content_success new file mode 100644 index 000000000000..54cc69718d85 --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/download_file_content_success @@ -0,0 +1,114 @@ + + + + + + IT + 05979361218 + + 00001 + FPR12 + 0000000 + + + + + + IT + 02780790107 + + + SOCIETA' ALPHA SRL + + RF01 + + + VIALE ROMA 543 + 07100 + SASSARI + SS + IT + + + + + 06363391001 + + BETA GAMMA + + + + VIA TORINO 38-B + 00145 + ROMA + RM + IT + + + + + + + TD01 + EUR + 2014-12-18 + 123 + LA FATTURA FA RIFERIMENTO AD UNA OPERAZIONE AAAA BBBBBBBBBBBBBBBBBB CCC DDDDDDDDDDDDDDD E FFFFFFFFFFFFFFFFFFFF GGGGGGGGGG HHHHHHH II LLLLLLLLLLLLLLLLL MMM NNNNN OO PPPPPPPPPPP QQQQ RRRR SSSSSSSSSSSSSS + SEGUE DESCRIZIONE CAUSALE NEL CASO IN CUI NON SIANO STATI SUFFICIENTI 200 CARATTERI AAAAAAAAAAA BBBBBBBBBBBBBBBBB + + + 1 + 66685 + 1 + + + + + IT + 04507990150 + + + Trasporto spa + + + 2012-10-22T16:46:12.000+02:00 + + + + + 1 + + INT + ART123 + + LA DESCRIZIONE DELLA FORNITURA PUO' SUPERARE I CENTO CARATTERI CHE RAPPRESENTAVANO IL PRECEDENTE LIMITE DIMENSIONALE. TALE LIMITE NELLA NUOVA VERSIONE E' STATO PORTATO A MILLE CARATTERI + 5.00 + 1.00 + 5.00 + 22.00 + + + 2 + FORNITURE VARIE PER UFFICIO + 10.00 + 2.00 + 20.00 + 22.00 + + + 22.00 + 25.00 + 5.50 + I + + + + TP01 + + MP01 + 2015-01-30 + 32.50 + + + + diff --git a/l10n_it_edi_fatturhello/tests/responses/download_file_headers_success b/l10n_it_edi_fatturhello/tests/responses/download_file_headers_success new file mode 100644 index 000000000000..f868352a330e --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/download_file_headers_success @@ -0,0 +1,4 @@ +{ + "content-type": "text/xml", + "content-disposition": "attachment;filename=IT01234567890_FPR03.xml" +} diff --git a/l10n_it_edi_fatturhello/tests/responses/download_list_empty_success b/l10n_it_edi_fatturhello/tests/responses/download_list_empty_success new file mode 100644 index 000000000000..3082fba06fd6 --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/download_list_empty_success @@ -0,0 +1,5 @@ +{ + "success": true, + "total": 2, + "rows": [] +} diff --git a/l10n_it_edi_fatturhello/tests/responses/download_list_success b/l10n_it_edi_fatturhello/tests/responses/download_list_success new file mode 100644 index 000000000000..c4243445abaa --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/download_list_success @@ -0,0 +1,54 @@ +{ + "success": true, + "total": 2, + "rows": [ + { + "stato": "", + "clifor": "Cliente pubblico 1", + "totale": "1,22€", + "datafattura": "20/04/2018", + "tipodocumento": "Fattura", + "scadenza": "20/05/2018", + "action": " xml", + "statosdi": "", + "statopa": "3", + "statotesto": "ricevuta", + "identificativo": "dsds1-B458-44C0-879B-B4052E952E22", + "fatturaelettronica": false, + "idallegato": "201801000121235", + "idallegatoxml": "201801000121236" + }, + { + "stato": "", + "clifor": "Cliente pubblico 1", + "totale": "1,22€", + "datafattura": "20/04/2018", + "tipodocumento": "Fattura", + "scadenza": "20/05/2018", + "action": " xml", + "statosdi": "", + "statopa": "3", + "statotesto": "ricevuta", + "identificativo": "dsds1-B458-44C0-879B-B4052E952E22", + "fatturaelettronica": false, + "idallegato": "201701000121235", + "idallegatoxml": "201701000121236" + }, + { + "stato": "", + "clifor": "Cliente pubblico 1", + "totale": "12200,00€", + "datafattura": "23/04/2018", + "tipodocumento": "Fattura", + "scadenza": "23/05/2018", + "action": " xml", + "statosdi": "", + "statopa": "1", + "statotesto": "spedita", + "identificativo": "C0693A9B-6A34-40FD-ACCB-dssssss", + "fatturaelettronica": true, + "idallegato": "201701000120235", + "idallegatoxml": "201701000120236" + } + ] + } diff --git a/l10n_it_edi_fatturhello/tests/responses/download_years_list_empty_success b/l10n_it_edi_fatturhello/tests/responses/download_years_list_empty_success new file mode 100644 index 000000000000..b76e44e19492 --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/download_years_list_empty_success @@ -0,0 +1 @@ +[] diff --git a/l10n_it_edi_fatturhello/tests/responses/download_years_list_success b/l10n_it_edi_fatturhello/tests/responses/download_years_list_success new file mode 100644 index 000000000000..a955fbf0d682 --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/download_years_list_success @@ -0,0 +1,3 @@ +[ + "2020" +] diff --git a/l10n_it_edi_fatturhello/tests/responses/login_fail b/l10n_it_edi_fatturhello/tests/responses/login_fail new file mode 100644 index 000000000000..42ae85619940 --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/login_fail @@ -0,0 +1,3 @@ +{ + "success": false +} diff --git a/l10n_it_edi_fatturhello/tests/responses/login_success b/l10n_it_edi_fatturhello/tests/responses/login_success new file mode 100644 index 000000000000..5a62113ac3bd --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/login_success @@ -0,0 +1,15 @@ +{ + "aziende": [ + { + "azienda": "ACME SRL", + "codice": "IKZFSKX6645VVG595QG7", + "menulight": true, + "cf": "06363391001", + "pi": "06363391001" + } + ], + "causali": [], + "success": true, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJTQiIsImV4cCI6MTUzNTUxMDEzNCwidGlwbyI6InNlc3Npb25lIiwiaWR1dGVudGUiOiI5NDgiLCJtZW51bGlnaHQiOiJUcnVlIiwiaWRzdHVkaW8iOiIwIn0.IUWPp2zDkesDJfoxeo42Sq3Ppf7jR0sY6rbtEVi2xs", + "authtoken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJTQiIsImV4cCI6MTU2NzAxNzMzNCwidGlwbyI6ImF1dGgiLCJyZWFsbSI6ImJwbyIsImlkIjoiOTQ4In0.9y9gW2LAXYYB1jonR9cOClwNSr8dssssFzbDiodJc" +} diff --git a/l10n_it_edi_fatturhello/tests/responses/status_success b/l10n_it_edi_fatturhello/tests/responses/status_success new file mode 100644 index 000000000000..11ada4734374 --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/status_success @@ -0,0 +1,15 @@ +{ + "success": true, + "rows": [ + { + "Data": "01/07/2020 14:51:00", + "Esito": "Fattura ricevuta correttamente dal SDI.", + "Nome": "IT04075500373_EMZjW.xml" + }, + { + "Data": "01/07/2020 10:45:00", + "Esito": "Inoltrata al Sdi", + "Nome": "IT04075500373_EMZjW.xml" + } + ] +} diff --git a/l10n_it_edi_fatturhello/tests/responses/upload_success b/l10n_it_edi_fatturhello/tests/responses/upload_success new file mode 100644 index 000000000000..e1a665f91572 --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/responses/upload_success @@ -0,0 +1,14 @@ +{ + "success": true, + "error": "", + "name": "9151_1470.pdf", + "protocollo": "201701000021074", + "files": [ + { + "success": true, + "error": "", + "name": "9151_1470.pdf", + "protocollo": "201701000021074" + } + ] +} diff --git a/l10n_it_edi_fatturhello/tests/test_download.py b/l10n_it_edi_fatturhello/tests/test_download.py new file mode 100644 index 000000000000..9dc45d54af4d --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/test_download.py @@ -0,0 +1,71 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest import mock + +from odoo import tests + +from odoo.addons.l10n_it_edi_fatturhello.tests.common import ( + REQUEST_PATH, + Common, +) + + +@tests.tagged("post_install", "-at_install") +class TestDownload(Common): + def _get_bills_capturer(self): + return tests.RecordCapturer( + self.env["account.move"], + [ + ("move_type", "=", "in_invoice"), + ], + ) + + def test_success(self): + """E-Bills can be downloaded.""" + # Arrange + company = self.company + self._login(company) + cron = self.cron + # pre-condition + self.assertIn(company, cron.user_id.company_ids) + self.assertFalse(company.fatturhello_last_downloaded_e_bill_identifer) + + # Act + with ( + mock.patch(REQUEST_PATH) as mock_request, + self._get_bills_capturer() as bills_capturer, + ): + mock_request.side_effect = [ + self._get_response("login_success"), + self._get_response("download_years_list_success"), + self._get_response("download_list_success"), + self._get_response("download_list_empty_success"), + self._get_response( + "download_file_content_success", + headers_name="download_file_headers_success", + ), + self._get_response( + "download_file_content_success", + headers_name="download_file_headers_success", + ), + ] + cron.method_direct_trigger() + + # Assert + self.assertEqual( + company.fatturhello_last_downloaded_e_bill_identifer, + "201801000121236", + ) + bills = bills_capturer.records + self.assertEqual( + bills.l10n_it_edi_attachment_id.name, "IT01234567890_FPR03.xml" + ) + self.assertRecordValues( + bills.sorted("ref"), + [ + { + "ref": "123", + }, + ], + ) diff --git a/l10n_it_edi_fatturhello/tests/test_login.py b/l10n_it_edi_fatturhello/tests/test_login.py new file mode 100644 index 000000000000..6fcff47ce50e --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/test_login.py @@ -0,0 +1,51 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest import mock + +from odoo import exceptions + +from odoo.addons.l10n_it_edi_fatturhello.tests.common import REQUEST_PATH, Common + + +class TestLogin(Common): + def test_success(self): + """A successful login populates the authtoken and the identifier.""" + # Arrange + company = self.company + wizard = self._get_login_wizard(company) + # pre-condition + self.assertFalse(company.fatturhello_login_authtoken) + + # Act + with mock.patch(REQUEST_PATH) as mock_request: + mock_request.return_value = self._get_response("login_success") + wizard.confirm() + + # Assert + self.assertTrue(company.fatturhello_login_authtoken) + self.assertEqual( + company.fatturhello_identifier, + "IKZFSKX6645VVG595QG7", + ) + + def test_fail(self): + """A failed login raises an exception.""" + # Arrange + company = self.company + wizard = self._get_login_wizard(company) + # pre-condition + self.assertFalse(company.fatturhello_login_authtoken) + + # Act + with ( + mock.patch(REQUEST_PATH) as mock_request, + self.assertRaises(exceptions.UserError) as exc, + ): + mock_request.return_value = self._get_response("login_fail") + wizard.confirm() + exc_message = exc.exception.args[0] + + # Assert + self.assertIn("Login with user and password failed", exc_message) + self.assertFalse(company.fatturhello_login_authtoken) diff --git a/l10n_it_edi_fatturhello/tests/test_upload.py b/l10n_it_edi_fatturhello/tests/test_upload.py new file mode 100644 index 000000000000..67f81117c65d --- /dev/null +++ b/l10n_it_edi_fatturhello/tests/test_upload.py @@ -0,0 +1,140 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime +from unittest import mock + +from odoo import tests + +from odoo.addons.l10n_it_edi_fatturhello.tests.common import ( + REQUEST_PATH, + Common, +) + + +@tests.tagged("post_install", "-at_install") +class TestUpload(Common): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company.update( + { + "fatturhello_is_used": True, + "l10n_it_codice_fiscale": "06363391001", + "vat": "06363391001", + } + ) + cls.cron.user_id.company_ids |= cls.company + + def test_success(self): + """An invoice can be uploaded.""" + # Arrange + self._login(self.company) + invoice = self.init_invoice( + "out_invoice", + company=self.company, + partner=self.italian_partner_a, + amounts=[ + 100, + ], + taxes=self.default_tax, + post=True, + ) + # pre-condition + self.assertEqual(invoice.state, "posted") + self.assertFalse(invoice.l10n_it_edi_state) + + # Act + with mock.patch(REQUEST_PATH) as mock_request: + mock_request.side_effect = [ + self._get_response("login_success"), + self._get_response("upload_success"), + ] + invoice.action_l10n_it_edi_send() + + # Assert + self.assertEqual(invoice.l10n_it_edi_state, "sent_to_fatturhello") + self.assertEqual(invoice.fatturhello_protocol, "201701000021074") + + def test_status_update(self): + """The status of uploaded e-invoices is updated + based on Fatturhello status.""" + # Arrange + cron = self.cron + self._login(self.company) + invoice = self.init_invoice( + "out_invoice", + company=self.company, + partner=self.italian_partner_a, + amounts=[ + 100, + ], + taxes=self.default_tax, + post=True, + ) + with mock.patch(REQUEST_PATH) as mock_request: + mock_request.side_effect = [ + self._get_response("login_success"), + self._get_response("upload_success"), + ] + invoice.action_l10n_it_edi_send() + e_invoice = invoice.l10n_it_edi_attachment_id + # Change the file name to match the one in test's status updates + e_invoice.name = "IT04075500373_EMZjW.xml" + + def mock_commit(): + pass + + # pre-condition + self.assertEqual(invoice.l10n_it_edi_state, "sent_to_fatturhello") + self.assertFalse(invoice.fatturhello_last_processed_status_datetime) + + # Act + with ( + mock.patch(REQUEST_PATH) as mock_request, + mock.patch.object(self.env.cr, "commit", mock_commit), + ): + mock_request.side_effect = [ + self._get_response("login_success"), + self._get_response("download_years_list_empty_success"), + self._get_response("login_success"), + self._get_response("status_success"), + ] + cron.method_direct_trigger() + + # Assert + self.assertEqual(invoice.l10n_it_edi_state, "forwarded") + self.assertEqual( + invoice.fatturhello_last_processed_status_datetime, + datetime(2020, 7, 1, 14, 51), + ) + + def test_sent_to_fatturhello_unlink(self): + """An e-invoice that has been sent to fatturhello can be deleted.""" + # Arrange + self._login(self.company) + invoice = self.init_invoice( + "out_invoice", + company=self.company, + partner=self.italian_partner_a, + amounts=[ + 100, + ], + taxes=self.default_tax, + post=True, + ) + with mock.patch(REQUEST_PATH) as mock_request: + mock_request.side_effect = [ + self._get_response("login_success"), + self._get_response("upload_success"), + ] + invoice.action_l10n_it_edi_send() + e_invoice = invoice.l10n_it_edi_attachment_id + # pre-condition + self.assertEqual(invoice.l10n_it_edi_state, "sent_to_fatturhello") + + # Act + e_invoice.unlink() + + # Assert + self.assertFalse(e_invoice.exists()) diff --git a/l10n_it_edi_fatturhello/views/account_move_views.xml b/l10n_it_edi_fatturhello/views/account_move_views.xml new file mode 100644 index 000000000000..cbb8d18e8f14 --- /dev/null +++ b/l10n_it_edi_fatturhello/views/account_move_views.xml @@ -0,0 +1,41 @@ + + + + + Add fatturhello fields to Invoice search view + account.move + + + + + + + + + + Add fatturhello fields to Electronic Invoice form view + account.move + + + + + + + + + + + diff --git a/l10n_it_edi_fatturhello/views/res_config_settings_views.xml b/l10n_it_edi_fatturhello/views/res_config_settings_views.xml new file mode 100644 index 000000000000..154d34c95fe1 --- /dev/null +++ b/l10n_it_edi_fatturhello/views/res_config_settings_views.xml @@ -0,0 +1,49 @@ + + + + + Add Fatturhello fields to Configuration Settings form + res.config.settings + + + + + + + + +