From 477c90c165e3459cf56387df1a4d0631276d09b3 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 5 Jun 2025 12:46:10 +0200 Subject: [PATCH 1/3] [ADD] l10n_it_fatturapa_fatturhello --- l10n_it_fatturapa_fatturhello/README.rst | 201 ++++++ l10n_it_fatturapa_fatturhello/__init__.py | 4 + l10n_it_fatturapa_fatturhello/__manifest__.py | 28 + .../data/ir_cron_data.xml | 34 ++ .../data/neutralize.sql | 3 + .../data/sdi_channel_data.xml | 11 + .../i18n/l10n_it_fatturapa_fatturhello.pot | 496 +++++++++++++++ .../models/__init__.py | 7 + .../models/account_move.py | 22 + .../models/connector.py | 576 ++++++++++++++++++ .../models/fatturapa_attachment_out.py | 66 ++ .../models/res_company.py | 20 + .../models/sdi_channel.py | 275 +++++++++ l10n_it_fatturapa_fatturhello/pyproject.toml | 3 + .../readme/CONFIGURE.md | 23 + .../readme/CONTRIBUTORS.md | 2 + .../readme/CREDITS.md | 3 + .../readme/DESCRIPTION.md | 19 + .../readme/ROADMAP.md | 1 + l10n_it_fatturapa_fatturhello/readme/USAGE.md | 23 + .../security/ir.model.access.csv | 2 + .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 524 ++++++++++++++++ .../tests/__init__.py | 5 + l10n_it_fatturapa_fatturhello/tests/common.py | 77 +++ .../responses/download_file_content_success | 154 +++++ .../responses/download_file_headers_success | 4 + .../responses/download_list_empty_success | 5 + .../tests/responses/download_list_success | 54 ++ .../responses/download_years_list_success | 3 + .../tests/responses/login_fail | 3 + .../tests/responses/login_success | 15 + .../tests/responses/status_success | 15 + .../tests/responses/upload_success | 14 + .../tests/test_download.py | 96 +++ .../tests/test_login.py | 51 ++ .../tests/test_upload.py | 132 ++++ .../views/fatturapa_attachment_out_views.xml | 56 ++ .../views/res_company_views.xml | 40 ++ .../views/sdi_channel_views.xml | 51 ++ .../wizards/__init__.py | 3 + .../wizards/login.py | 66 ++ .../wizards/login_views.xml | 31 + 43 files changed, 3218 insertions(+) create mode 100644 l10n_it_fatturapa_fatturhello/README.rst create mode 100644 l10n_it_fatturapa_fatturhello/__init__.py create mode 100644 l10n_it_fatturapa_fatturhello/__manifest__.py create mode 100644 l10n_it_fatturapa_fatturhello/data/ir_cron_data.xml create mode 100644 l10n_it_fatturapa_fatturhello/data/neutralize.sql create mode 100644 l10n_it_fatturapa_fatturhello/data/sdi_channel_data.xml create mode 100644 l10n_it_fatturapa_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot create mode 100644 l10n_it_fatturapa_fatturhello/models/__init__.py create mode 100644 l10n_it_fatturapa_fatturhello/models/account_move.py create mode 100644 l10n_it_fatturapa_fatturhello/models/connector.py create mode 100644 l10n_it_fatturapa_fatturhello/models/fatturapa_attachment_out.py create mode 100644 l10n_it_fatturapa_fatturhello/models/res_company.py create mode 100644 l10n_it_fatturapa_fatturhello/models/sdi_channel.py create mode 100644 l10n_it_fatturapa_fatturhello/pyproject.toml create mode 100644 l10n_it_fatturapa_fatturhello/readme/CONFIGURE.md create mode 100644 l10n_it_fatturapa_fatturhello/readme/CONTRIBUTORS.md create mode 100644 l10n_it_fatturapa_fatturhello/readme/CREDITS.md create mode 100644 l10n_it_fatturapa_fatturhello/readme/DESCRIPTION.md create mode 100644 l10n_it_fatturapa_fatturhello/readme/ROADMAP.md create mode 100644 l10n_it_fatturapa_fatturhello/readme/USAGE.md create mode 100644 l10n_it_fatturapa_fatturhello/security/ir.model.access.csv create mode 100644 l10n_it_fatturapa_fatturhello/static/description/icon.png create mode 100644 l10n_it_fatturapa_fatturhello/static/description/index.html create mode 100644 l10n_it_fatturapa_fatturhello/tests/__init__.py create mode 100644 l10n_it_fatturapa_fatturhello/tests/common.py create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/download_file_content_success create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/download_file_headers_success create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/download_list_empty_success create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/download_list_success create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/download_years_list_success create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/login_fail create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/login_success create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/status_success create mode 100644 l10n_it_fatturapa_fatturhello/tests/responses/upload_success create mode 100644 l10n_it_fatturapa_fatturhello/tests/test_download.py create mode 100644 l10n_it_fatturapa_fatturhello/tests/test_login.py create mode 100644 l10n_it_fatturapa_fatturhello/tests/test_upload.py create mode 100644 l10n_it_fatturapa_fatturhello/views/fatturapa_attachment_out_views.xml create mode 100644 l10n_it_fatturapa_fatturhello/views/res_company_views.xml create mode 100644 l10n_it_fatturapa_fatturhello/views/sdi_channel_views.xml create mode 100644 l10n_it_fatturapa_fatturhello/wizards/__init__.py create mode 100644 l10n_it_fatturapa_fatturhello/wizards/login.py create mode 100644 l10n_it_fatturapa_fatturhello/wizards/login_views.xml diff --git a/l10n_it_fatturapa_fatturhello/README.rst b/l10n_it_fatturapa_fatturhello/README.rst new file mode 100644 index 000000000000..32444738c358 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/README.rst @@ -0,0 +1,201 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================================================ +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/license-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/16.0/l10n_it_fatturapa_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-16-0/l10n-italy-16-0-l10n_it_fatturapa_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=16.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 "Importazione +e-fatture da Fatturhello". 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 CRON "Import E-Bills from +Fatturhello". 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** + +Nella lista dei canali SdI, scegliere o creare un nuovo canale di tipo +"Fatturhello" ed effettuare il login. + +In Impostazioni > Utenti e aziende > Aziende, selezionare un'azienda e +nella scheda "E-fattura" impostare il canale SdI configurato nel campo +"Canale SdI". Se alcune fatture di Fatturhello sono già presenti in +Odoo, abilitare la modalità debug e valorizzare "Identificativo +dell'ultima fattura scaricata". 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 the list of the ES channels, choose or create a new channel of type +"Fatturhello" and login. + +In Settings > Users & Companies > Companies, select a company and in the +tab "E-invoice", set the configured ES channel in the field "ES +channel". If some Fatturhello electronic bills are already present in +Odoo, enable Debug mode and fill "Last downloaded E-Bill identifier". 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, attivare il CRON "Importazione +e-fatture da Fatturhello". + +Per aggiornare lo stato delle fatture elettroniche inviate, attivare il +CRON "Aggiornamento stato e-fatture caricate su Fatturhello". + +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, activate the "Import E-Bills +from Fatturhello" CRON. + +In order to update the status of sent electronic invoices, activate the +"Update the status of E-Invoices uploaded to 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-aleuffre| image:: https://github.com/aleuffre.png?size=40px + :target: https://github.com/aleuffre + :alt: aleuffre +.. |maintainer-SirPyTech| image:: https://github.com/SirPyTech.png?size=40px + :target: https://github.com/SirPyTech + :alt: SirPyTech + +Current `maintainers `__: + +|maintainer-aleuffre| |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_fatturapa_fatturhello/__init__.py b/l10n_it_fatturapa_fatturhello/__init__.py new file mode 100644 index 000000000000..d6ade57604db --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/__manifest__.py b/l10n_it_fatturapa_fatturhello/__manifest__.py new file mode 100644 index 000000000000..399f249b534f --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "ITA - Fattura elettronica - Supporto Fatturhello", + "version": "16.0.1.0.0", + "category": "Localization/Italy", + "summary": "Invio e ricezione fatture elettroniche tramite Fatturhello", + "author": "PyTech, Odoo Community Association (OCA)", + "maintainers": [ + "aleuffre", + "SirPyTech", + ], + "website": "https://github.com/OCA/l10n-italy", + "license": "AGPL-3", + "depends": [ + "l10n_it_sdi_channel", + ], + "data": [ + "data/ir_cron_data.xml", + "data/sdi_channel_data.xml", + "security/ir.model.access.csv", + "views/fatturapa_attachment_out_views.xml", + "views/res_company_views.xml", + "views/sdi_channel_views.xml", + "wizards/login_views.xml", + ], +} diff --git a/l10n_it_fatturapa_fatturhello/data/ir_cron_data.xml b/l10n_it_fatturapa_fatturhello/data/ir_cron_data.xml new file mode 100644 index 000000000000..7fb4290ba06e --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/data/ir_cron_data.xml @@ -0,0 +1,34 @@ + + + + + Import E-Bills from Fatturhello + + + -1 + 1 + hours + code + +model._fatturhello_import_e_bills_cron() + + + + + Update the status of E-Invoices uploaded to Fatturhello + + + -1 + 1 + hours + code + +model._fatturhello_update_e_invoices_status_cron() + + + diff --git a/l10n_it_fatturapa_fatturhello/data/neutralize.sql b/l10n_it_fatturapa_fatturhello/data/neutralize.sql new file mode 100644 index 000000000000..5912d038dce0 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/data/neutralize.sql @@ -0,0 +1,3 @@ +-- Remove token for Fatturhello authentication +UPDATE sdi_channel +SET fatturhello_login_authtoken = null; diff --git a/l10n_it_fatturapa_fatturhello/data/sdi_channel_data.xml b/l10n_it_fatturapa_fatturhello/data/sdi_channel_data.xml new file mode 100644 index 000000000000..fc2f12980b8b --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/data/sdi_channel_data.xml @@ -0,0 +1,11 @@ + + + + + Fatturhello + fatturhello + + diff --git a/l10n_it_fatturapa_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot b/l10n_it_fatturapa_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot new file mode 100644 index 000000000000..c9e548742f18 --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/models/__init__.py b/l10n_it_fatturapa_fatturhello/models/__init__.py new file mode 100644 index 000000000000..acd643cc186c --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/models/__init__.py @@ -0,0 +1,7 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_move +from . import fatturapa_attachment_out +from . import connector +from . import res_company +from . import sdi_channel diff --git a/l10n_it_fatturapa_fatturhello/models/account_move.py b/l10n_it_fatturapa_fatturhello/models/account_move.py new file mode 100644 index 000000000000..54a0e956c4fa --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/models/account_move.py @@ -0,0 +1,22 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +from odoo.addons.l10n_it_fatturapa_out.models.account import ( + fatturapa_attachment_state_mapping, +) + +fatturapa_attachment_state_mapping["sent_to_fatturhello"] = "sent_to_fatturhello" + + +class AccountMove(models.Model): + _inherit = "account.move" + + fatturapa_state = fields.Selection( + selection_add=[ + # Before the `sent` status + ("sent_to_fatturhello", "Sent to Fatturhello"), + ("sent",), + ], + ) diff --git a/l10n_it_fatturapa_fatturhello/models/connector.py b/l10n_it_fatturapa_fatturhello/models/connector.py new file mode 100644 index 000000000000..84ed7c4eb019 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/models/connector.py @@ -0,0 +1,576 @@ +# 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_fatturapa_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.fiscalcode + 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_e_invoice_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"], + "state": "sent_to_fatturhello", + } + + def upload_e_invoice(self, base_url, session_token, company_identifier, e_invoice): + """Upload an e-invoice for a company. + + :param company_identifier: The identifier of the Company + where the file will be uploaded. + """ + e_invoice.ensure_one() + upload_result = self._upload_post( + base_url, + session_token, + company_identifier, + e_invoice.name, + e_invoice.raw, + ) + e_invoice.update(self._prepare_e_invoice_upload_values(upload_result)) + return True + + 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_fatturapa_fatturhello/models/fatturapa_attachment_out.py b/l10n_it_fatturapa_fatturhello/models/fatturapa_attachment_out.py new file mode 100644 index 000000000000..4601727ea123 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/models/fatturapa_attachment_out.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 api, fields, models + + +class FatturaPAAttachmentOut(models.Model): + _inherit = "fatturapa.attachment.out" + + fatturhello_protocol = fields.Char( + readonly=True, + help="Identifier assigned during upload to Fatturhello.", + ) + state = fields.Selection( + selection_add=[ + # Before the `sent` status + ("sent_to_fatturhello", "Sent to Fatturhello"), + ("sent",), + ], + ondelete={ + "sent_to_fatturhello": "set default", + }, + ) + 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.", + ) + + 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 + + @api.model + def get_file_vat(self): + file_vat = super().get_file_vat() + company = self.env.company + if company.sdi_channel_id.channel_type == "fatturhello": + file_vat = self._fatturhello_adapt_file_name(file_vat) + return file_vat + + def _is_sent_to_sdi(self): + if self.state == "sent_to_fatturhello": + is_sent_to_sdi = False + else: + is_sent_to_sdi = super()._is_sent_to_sdi() + return is_sent_to_sdi + + def _fatturhello_get_e_invoice_identifier(self): + """How the uploaded invoice can be identified for API calls.""" + self.ensure_one() + return self.name + + @api.model + def _fatturhello_get_noupdate_statuses(self): + """E-Invoices in these statuses will not fetch updates from Fatturhello.""" + return ( + "rejected", + "validated", + "accepted", + ) diff --git a/l10n_it_fatturapa_fatturhello/models/res_company.py b/l10n_it_fatturapa_fatturhello/models/res_company.py new file mode 100644 index 000000000000..c6dec76f4615 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/models/res_company.py @@ -0,0 +1,20 @@ +# 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.", + ) diff --git a/l10n_it_fatturapa_fatturhello/models/sdi_channel.py b/l10n_it_fatturapa_fatturhello/models/sdi_channel.py new file mode 100644 index 000000000000..cf66e4f3e6dd --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/models/sdi_channel.py @@ -0,0 +1,275 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models + +from ..models.connector import ( + SENT_FROM_FATTURHELLO_NOTIFICATION_TYPE, +) + +LOGIN_DURATION = relativedelta( + years=1, +) + +_logger = logging.getLogger(__name__) + + +class SdIChannel(models.Model): + _name = "sdi.channel" + _inherit = [ + "sdi.channel", + "mail.activity.mixin", + ] + + channel_type = fields.Selection( + selection_add=[ + ("fatturhello", "Fatturhello"), + ], + ondelete={ + "fatturhello": "cascade", + }, + ) + 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 send_via_fatturhello(self, attachment_out_ids): + """Send the e-invoices `attachment_out_ids` using Fatturhello.""" + self.ensure_one() + connector = self.env["l10n_it_fatturapa_fatturhello.connector"] + url = self.fatturhello_base_url + session_token = connector.get_session_token( + url, + self.fatturhello_login_authtoken, + ) + company_identifier = self.company_id.fatturhello_identifier + for attachment in attachment_out_ids: + connector.upload_e_invoice( + url, + session_token, + company_identifier, + attachment, + ) + return True + + def _process_single_notification( + self, + attachment, + notification_type, + parsed_notification, + ): + if attachment.channel_type == "fatturhello": + # Process the special Fatturhello notification. + if notification_type == SENT_FROM_FATTURHELLO_NOTIFICATION_TYPE: + result = attachment.write( + { + "state": "sent", + } + ) + else: + result = super()._process_single_notification( + attachment, + notification_type, + parsed_notification, + ) + + if result: + receipt_datetime_node = parsed_notification.find("DataOraRicezione") + receipt_datetime = fields.Datetime.from_string( + receipt_datetime_node.text + ) + attachment.fatturhello_last_processed_status_datetime = receipt_datetime + else: + result = super()._process_single_notification( + attachment, + notification_type, + parsed_notification, + ) + return result + + def fatturhello_action_login(self): + """Action to open the login wizard.""" + login_wizard = self.env["l10n_it_fatturapa_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.company_id.fatturhello_identifier = credentials["company_identifier"] + self.activity_schedule( + act_type_xmlid="mail.mail_activity_data_todo", + summary=_("Renew login"), + date_deadline=fields.Date.context_today(self) + LOGIN_DURATION, + ) + return True + + @api.model + def _fatturhello_import_e_bills_cron(self): + """Method executed by CRON to import E-Bills.""" + connector = self.env["l10n_it_fatturapa_fatturhello.connector"] + # Not using sudo so that we can do only + # whatever the CRON's user can do + companies = self.env.companies.filtered( + lambda company: company.sdi_channel_type == "fatturhello" + ) + for company in companies: + channel = company.sdi_channel_id + # Login + url = channel.fatturhello_base_url + session_token = connector.get_session_token( + url, + channel.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, + ) + if e_bills_identifiers: + company.fatturhello_last_downloaded_e_bill_identifer = ( + e_bills_identifiers[-1] + ) + + # Download each E-Bill's data + file_name_content_dict = dict() + 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_dict[file_name] = file_content + + # Create E-Bills + self.receive_fe( + file_name_content_dict, + dict(), + channel_id=channel.id, + company_id=company.id, + ) + + @api.model + def _fatturhello_get_attachments_for_status_update(self, companies): + """E-Invoices in `companies` to be updated with Fatturhello data. + + Grouped by Company. + """ + noupdate_statuses = self.env[ + "fatturapa.attachment.out" + ]._fatturhello_get_noupdate_statuses() + e_invoices_data = self.env["fatturapa.attachment.out"].search_read( + domain=[ + ("state", "not in", noupdate_statuses), + ("company_id", "in", companies.ids), + ], + fields=[ + "id", + "company_id", + ], + load=None, + ) + attachments_by_company = dict() + for e_invoice_data in e_invoices_data: + company = self.env["res.company"].browse(e_invoice_data["company_id"]) + attachment = self.env["fatturapa.attachment.out"].browse( + e_invoice_data["id"] + ) + if company not in attachments_by_company: + attachments_by_company[company] = attachment + else: + attachments_by_company[company] |= attachment + return attachments_by_company + + @api.model + def _fatturhello_update_e_invoices_status_cron(self): + """Method executed by CRON to update the status of E-Invoices.""" + connector = self.env["l10n_it_fatturapa_fatturhello.connector"] + # Not using sudo so that we can do only + # whatever the CRON's user can do + companies = self.env.companies.filtered( + lambda company: company.sdi_channel_type == "fatturhello" + ) + attachments_by_company_id = self._fatturhello_get_attachments_for_status_update( + companies + ) + sdi_response_name_content_dict = dict() + for company, attachments in attachments_by_company_id.items(): + channel = company.sdi_channel_id + # Login + url = channel.fatturhello_base_url + session_token = connector.get_session_token( + url, + channel.fatturhello_login_authtoken, + ) + + # Download each E-Invoice's status + attachment_to_statuses_dict = { + attachment: connector.get_e_invoice_status_list( + url, + session_token, + company.fatturhello_identifier, + attachment._fatturhello_get_e_invoice_identifier(), + ) + for attachment in attachments + } + + # Parse the Fatturhello status into a SdI message + for attachment, statuses in attachment_to_statuses_dict.items(): + last_processed_status_datetime = ( + attachment.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( + attachment.name, + status_index, + status, + ) + sdi_response_name_content_dict[name] = content + + # Update attachments + self.receive_notification( + sdi_response_name_content_dict, + ) diff --git a/l10n_it_fatturapa_fatturhello/pyproject.toml b/l10n_it_fatturapa_fatturhello/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/l10n_it_fatturapa_fatturhello/readme/CONFIGURE.md b/l10n_it_fatturapa_fatturhello/readme/CONFIGURE.md new file mode 100644 index 000000000000..be1b801acf90 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/readme/CONFIGURE.md @@ -0,0 +1,23 @@ +**Italiano** + +Nella lista dei canali SdI, scegliere o creare un nuovo canale di tipo "Fatturhello" ed effettuare il login. + +In Impostazioni > Utenti e aziende > Aziende, selezionare un'azienda e nella scheda "E-fattura" impostare il canale SdI configurato nel campo "Canale SdI". +Se alcune fatture di Fatturhello sono già presenti in Odoo, abilitare la modalità debug e valorizzare "Identificativo dell'ultima fattura scaricata". +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 the list of the ES channels, choose or create a new channel of type "Fatturhello" and login. + +In Settings > Users & Companies > Companies, select a company and in the tab "E-invoice", set the configured ES channel in the field "ES channel". +If some Fatturhello electronic bills are already present in Odoo, enable Debug mode and fill "Last downloaded E-Bill identifier". +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_fatturapa_fatturhello/readme/CONTRIBUTORS.md b/l10n_it_fatturapa_fatturhello/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..6e720b67df64 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [PyTech](https://www.pytech.it): + - Simone Rubino \<\> diff --git a/l10n_it_fatturapa_fatturhello/readme/CREDITS.md b/l10n_it_fatturapa_fatturhello/readme/CREDITS.md new file mode 100644 index 000000000000..c1cba3ffaa31 --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/readme/DESCRIPTION.md b/l10n_it_fatturapa_fatturhello/readme/DESCRIPTION.md new file mode 100644 index 000000000000..bed8b80adbeb --- /dev/null +++ b/l10n_it_fatturapa_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 "Importazione e-fatture da Fatturhello". +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 CRON "Import E-Bills from Fatturhello". +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_fatturapa_fatturhello/readme/ROADMAP.md b/l10n_it_fatturapa_fatturhello/readme/ROADMAP.md new file mode 100644 index 000000000000..b369c6a71093 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/readme/ROADMAP.md @@ -0,0 +1 @@ +- Parse more types of status notification diff --git a/l10n_it_fatturapa_fatturhello/readme/USAGE.md b/l10n_it_fatturapa_fatturhello/readme/USAGE.md new file mode 100644 index 000000000000..df135b428e05 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/readme/USAGE.md @@ -0,0 +1,23 @@ +**Italiano** + +Per ricevere le fatture elettroniche, attivare il CRON "Importazione e-fatture da Fatturhello". + +Per aggiornare lo stato delle fatture elettroniche inviate, attivare il CRON "Aggiornamento stato e-fatture caricate su Fatturhello". + +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, activate the "Import E-Bills from Fatturhello" CRON. + +In order to update the status of sent electronic invoices, activate the "Update the status of E-Invoices uploaded to 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_fatturapa_fatturhello/security/ir.model.access.csv b/l10n_it_fatturapa_fatturhello/security/ir.model.access.csv new file mode 100644 index 000000000000..ed65857cb4d5 --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello.model_l10n_it_fatturapa_fatturhello_login,account.group_account_manager,1,1,1,1 diff --git a/l10n_it_fatturapa_fatturhello/static/description/icon.png b/l10n_it_fatturapa_fatturhello/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 zcmbt)WmufcvhH9Zc!C8B?l8#UE&&o;gF7=g3=D(IAOS+K1lK^25Zv7%L4sRw_uvvF z*qyAk?>c**=lnR&y+1yw{;I3Hy6Ua2{<d0kcR+VvBo; zA_X`>;1;xAPL9rQqFxd#f5{a^zW*uaW+r3+U{|fRunu`GZhy$X z8_|Zi{zd#vIokczl8Xh*4Wi@i0+C?Rg1AB5VOEg8B>buLFCi~r5DPd2ED7QP2>^LO zKpr7+?*I1bPaFSLLEa0l2$tj*;u8Qtc=&(RUc*VK@ zjIN{I--GfO@vl+&r^eqy_BZ3dndN_PDzMc*W^!?dIsWAWU@LBjBg6^f4F6*!-hUYh zY$Xb}gF8b0%S1Ac@c%Rs()UCiEu3v6SiFE>h_!{gBb-H2{e=wB5o!YkT0>#LKZFw$ z?CuD0Gvfsb(|XbVxx0AL0%`gG2X+6|f;jiTHU9shtjoW-{2!| zMN*WuOj6elhD4zqgjNpX>F#JP{)hAbenX<+FPr>7jXM&q{|x+pbj8cU<=>Ej zWE1_%qoFVzDAZB%g@v<+1ud%<#2E~ML11jOV5pUZoXktGmzB38%te^i-3o9i$lge>z>tBcK|P2K0H9w{l#|i%$~egM)Ys{q>p<9yaE*%v2cy1wXE{AXqG1_b znfyg@Fq*e@yC)^(@$R*j^E;skyEM6pmL$1ctg*mWiWM&q1{nj>E^)Odw$RPr zhjesSk}k}@-e_%uZTy0t_*TJD&6%*HV0KH>xE@oBex6CL@`Ty3nH_2OF#M?6j(j|9 znRKGSfp3Q2i+|>}w?>8g$>r`|OcvG5r;p)z8DO8+O>EvYQ=_~`p}9!ReUEjUnNL@6 z+C*aoo67(sd|7QgW54@V9Y8PnBW$Q+7ZsRFA}Vj*viA!yWUfb!s*yJi6JKsXZCH4j z*B%nJpad-DDvJ8d>xrxkkh6A}i7V3nULqHCiG~|)YY6{NE3M}c^s#PQhzhsJUf^QW zR+F;up-dN*!)M1ZYl@d0HoqfVD2PNiQcPdzq4NDKO!8mUl{!t*ntBg_+-+lRlI0~Lr>5v!PiQj|hD7B-YFIs~6hIY*R6USZA zlb}=UxqxpSzIsL3pPmiuixCN|3LFBd?0Ih8Y6GWQ;U>dkdXtQaQ&8H|TGAQbuHY=F z_R83&B{1_hP7L#$^eAe?GPB_83y#HZKTwD>e-@E2P>Gk$BBb9|Ivfmdp za~s>3=aj(;xmz8n)sI}uFO$|C>0CZbcTY$Bq6~L-Bc9=vl@X#0S~Q@j8iKzuPeQE_ zQSI)wNz~CvJ>!%QszoCfUm9}h^DL!WYAN|FtMO#kpDXq74sYC87(uvv*jiCjV?Ta& zgO1D0OP3TEN3YnBpD6GnmsEolzEbGM{&VlTz_)J(o{nl0+TmNt{xL%L6G&UR$^aYC zQOA#W7R%9JsC5oTZJE>_?!Ci}mNH{0ObyUd%Q!k%5J8Z`8sR!m`~|Taje`(bLD7=a z-{-=d7w;k@DIrgU{I@K}eN`>S**Lg<@ChAf$M(&kV9TLUixqFQ>YoYHrI!K#R6`S> z%?d5hQ@&;Gje<|uRQZb%Hhibocl9(buI?=0aZW{JYXx?ZS@Lr%G8L<d+riEi2~+{HfHK{K^VrGYNi{2-WJOiC>Pz?f*)cxKCl>1H1=$jb!^ zpmYw>eoiM0Hy7$xbbX_e5o*+{7T2&-t%-h4i7MMo;k|tSqQAeNkwHS9hWY#EV7r3| zTmOmN{;b9OUZpp`LP(I9Wo%R#$b6YdH7GD4*p6>a2N2A04pQ*n;INQMh%+mj;x7>S z_(H?uJ^n!r1)kJH1*s+%$al#?C^Cw{H@RA^QGB=Dubyc)XUaY>f`(VKTlIO-YNCp{1n zOl*>jT?Dtf5fD$DY-j&B*Xmn|2-u2OB zBL@-lFs5lhcQKXBR*cIXmi%~EJcc^5#Xpg!E^A6sXf1#$qJGRpmU~A zcdj-cvBfx(fIRAMU(1obztJR%I7v3R-%$#~r!0sS^I(iC*5i6296*88A7I=_JhU3p zya!aCti0R5*RFT%LW0R|;u&oJ6=P-c$le4J0bi}u!!@;xzao|l6fJ{;Mld9hGhrJg zr_B)=4yktp)yPB@tCC_L9h1>GzXD6DA!W7xt{1)8!07~gONkEWC8@y%lciB{9ojy) zWm$drJ_9uVJ>Q$-`@q%OM7_S>(K=__CGYB~@@mE^Z=eT|x0Rv?Z-N)LLWR zod*Zy3v)iMX@usPX-OKBDgC8yq?fMhqf8H)A&C)Hi29YFn!NVf5!J0-F{wC&L5-3`#id=4?=2>Zp6Pdu4N6#bG&atu7 z8IET&ciXy_Tp4YjMx3yIAbw#_e2#jgGJ~ogkv-|M7|%Gio%2@mnS89NKUOM#Bzg4_ z9e9oN;^m>G*#?)AawODi6YckRPmkSKD_4b4WFpj|@|eS!B0WN@?QscYzTH`~6e%iz z!z1>ps)CG37%(E=kZ_>re)@ODv^0^=rWU^*m;6M&gD10EYImO98JVabRe5{#wrogYUKPB@_(#e7Ej9_x;n1oHDj5GawU)A&1hWj|HzJB(q{vMTX>jOW;Jz zBsW&SqTaR7!NXXg_A}$XnFpg_n)Zi;{e9eb*k|b(y$a}12boJ7rqQXQpVhU8HxHTl zt8Ln!KLFyfq!%}hdMXle^qajw2g6S{z&7tQ6J(w9 z3+!HTO{_TqM{9o$RR~lKFf4b4(xLUP?QG;McNFQc_Yd_mig9Ejy9%q~Ye>rIn3};U z)w&1@QCK;cC(;x0G&YuSad+>{c@ZsFJcUdcs@PP-x{mrO)|6_#CjMlXsMJx;Cr?FF zVFrlt@$Z-Ll^*7d0#`5Uez@bb{Xn(BQLhScBhF!6+aIso0=l{PP7P(6-ru>nVy%AP z+|eZpY(ooMU7rtG$l#14v=Z?@ebOjm(A2)5k_${|wAA$oq+;42wiS78ezjgWWnTrF z`1!i2h{fM91aD8uxz?tZpE(PsL37e3$*I6%un5Bzzpn10p`j72R;3=Oaug_|Z(y)@ z9$SJN@-5d1tNIy0=7|d&_HAnDx!yDd-u#qmfuDh)0a_CVje{hvQz9rDFHJTpQ0Dg@ zGQ3t*gZlcFSXfx%OG@Cds&NDROxd^osY_)abmo^dKMUY!R~kGH%*;rutPF@Mx$zrv z6Q1soKnYYRW#;Bi-!H)>Br0<`y+Wy~p7_<>{ljuG`Dpje=v1x}-ND<)bWBr|<}v6B zkDTUZ^@VsH>CyR}ml4j2rB{}0q8eGwX>ExkI9yZN0)(P}$N(yi$AxmBY#Xj`(7zs{ zJbn2&jE`-*0lww_r;|fNaWm_xp;c9JHIv|RExZGKP%18qjgYa);`N-^VqXNVz{~)~ z?^&D;ouy!pKPy?%@xH`A zSR z7x%N3@o&{YEjfa|1;*eW_4TU{ zt;qCcY3Hj(<0DJuny*QL!y!StcG{>bhpUP%eVMq=1xcR>yZT8X9)1;rXOmQjPcANs zr>&Qb{rr66;s|4v3iGmQlMjr9j;G6pqNs%;TsyVNd3{i~hpDX8ugdcnd&UQJzj)rH zh>S6#n`cCJ9CwHv<2Ht$o`R5(h#r||VB?%J?s5W48;^o)b`Pi1^~}5{Y19lg{&W@LfHt*gc1`w$RfLrK{~H?A1$5 z;5v?AIhpN%gQsR6+Act9-3y z8>jCTMnWQq-^s3#Lb|WalgB$k3F>}lyCxs<2&A;LS0}s#<|hPx9kM#B+Lu2DiD_3P zelg;N!80(j@HNc2pXs}re%sHi+{aqBt~qUOy86?zN>7)yiCEJqy@2Gh#gzJE6j6Rx zBQK{77zW?gLWtQ20Dzntu16k9^N>DQ@Nmbx*mOg=F=k)8VJfM%y(Xu41;8YCz+@K| z9u7vhlT`BOnk_oMTeC;u@OhhoTeA`^34^iMihCLM_uVD>rI-9@4l7ocZl@DJ8FWZU zB0lRBIqkHj4#pE&mD(X!e!~;G$`7f47k* zOznM2@`&KM(|f5}sz)z%2}yJ5YmMj5Zwzr-W?v3R&@KuJ+l0zo==N@)nsbMHqHV}w z7#_ntMGCNM21RuH^SYG+RH0sHUsF2z7ams57@2xbPj0y5)8h+caqv@P^q!do+}>+X zzUBx|mikTawzXWYzJ4(AqAJpBF4ObmD_@gyg->oFGB6`k(8+?rFRV5P1yDkFM=8(c z%RI)iG(rKtq-^V%B_(R9;tk6WIzA?x@cESTXg zWYDBxkoNB5v6J8BP&n@HVtBNb@r+XYpjgub zR4oE*$ffXJuh2g8TCaLnpNoSxJ~Jx@ayx9z5Osa)=AI#bg^5eQb<6gpR%c+Qs#N*e z@XE4pAmjdI#0%pV7sIN>mNa^jTkd=<==2_#t-}9Ju&Z^|Lp$%B92@eN%=MRc)LK$% z@!XAg;dQ8bt=@ZNey7+a(dy^o;QKGP@Rb5NJYQRrGEC{J=FB(Irw-MAfoP(9RK;)&jlxSCT=W;ODCf($WqRFhqN#LR^qVhK zWhEp4`{Nnk;n0FHj}eNCZpRM`Y-@MIM&pvr7zQOZ3Ik5;CmZbR99b&22(!-07YNF) z$o0MKej-jnvQV39{TH4r2R5univa1{ASc|VOTi4c@`t2FId|xkh5typ-rdU;1j){adk@*+( zkHj{5B~eSy&HrPOOvl_FJ98)0V;^d`0-u0FTslgiLBQVGSTiSyu zgMGAu&R}SbNa-DgKJb?;fe3Qys$?=;5?V`eRiq*Kj$I`}Z*x4rC~eNM=DsOq(=nUW>(+7o@O8K-_U(X? zTyg032nXKax5W~SF5|eBj%r8Fa>i!ejC72*sd}zJ)t7Xy!gFvM`c4@*Iw>z$u)j_l zR-Uqxymg}>Ti>i%9j*4kwfC33i~kyIQ``n)r(L z!|H2*)Mwj4dk%e*L0tgFdW185>j4<7YwLXwcOsed`%6mS{+=&d@d!B}GkbDV*0 zNIWzW^|trz!&;qeI&mPiVDOUL70xpqVv0fpN9tjpu)@1LD9D<9}9{57j9!W$`zC6&i zl9lKkmPh`x)5+h>>JtiRNNBW5$_)%-)#+SVSGsjX2T=+SRX05>yJZd`1hyk<@{%1+ zDu^k>J$d*Qz6BZMwHx!@O**^Tx&fsHDw%$@J0nfj^je^Ihy*aIx{B(hkBvSvh46Z9 zRO)BjjXL_IHXKo~$4es=8Wxk;Y+&nVBCXA;=MVuLgVn8Mk(*y^+kP3f?Pr~4^A}hXj9UHS}qeI%XKD3KhHnkrNH0(Y20BWl&!Kfm`EVh2;i5C zpirU^K0nc2-I{cqvjZKVx z=&hH#-d=gDWjVE}cMNAPJf;#NYdQ=h`twjX6yquXuCNgGx1~uk{YHAmFpQF`ZLGC=~ukEyj?cFDI zH=@XvV#AY1EY4qb`y*;Ki>KuFB|2|toL7__Cr0S1Dl{s#y0=~7HSq~&7lpBc*VLua zvv3r&-LM*{hq%IYP7<@)dG-G$kMrZaqs(MYoZ zugEeJ@u(ip9rMoVtoFe;dF`^Br5x7v!rr5`hb5mJ#ocGqXHnm9m`yILjd0>UQSMv) z^v}l5^bM6RZ6M%{mkI) zHOoSp&dX)*xUt+kXscna#a`XxI;Ul2Sxa^i5sZc=(Q)oA^2-_;!pfYHAul+oA@Ilelm;rw@FYR+SIaWS?;_ zUdw<|qqaYq(nqu>rG48E9dYAoT6GH;QRuBYK1}W#C_Z_?7~k*pJ3?MzVt&rhZTsBy zw?nN$_Z>kimtwWcy`0?G#!)&7GjOcxCQps@p&ml8>~z(t=sjhR$6aFh!Vw5GA(lTh z5GM)jCwloa6a}7mdfqNYE7oi`Jv$m5>5qR%9eZ=)=a z+K4j5NpcDHHdepCS+P*{@o=yNp&TE(Sd4b0Notqso-Kt_mhDk1<-fa>T4KdY2N`U) zxu41vD%T&k$Gl?CW81%7r#-o1TZ0&PCcy}L4TPiV;sz`|S!&w8-s$rLdM zF&)>@`7=)65PWn#oi|8tXNb|((2ojf9d0fNZ^l7xY~dX~%*Xf-v2W-2n$i~s!4?H; z2qbQscFN21tqB{|x1+(^G~xQSrvX&Y;V-%?b1}zjBQX{GOFcVYTcwm>>}>6^HA=$x zn+z^Biv_5}0!#@7z1~YXJFCT2?D^jm+kH7jAqBo?M@ZdMl|2|66oLnSJXUOJtVLxe z0vH)N^t*qrjq=eFRMV>BFEfS)-2RzKlt973;d3D}4edwIE>kGc5-o=JV56ird)RlS z{Jg@0t-b#Ife80%!E~(7`qkZ8O~Q-8_{j7G&tqwX&&>^tm-#*{v7j-f1n0}mCR#7P z-4FkajD2$9?4Fc7-C_|0Z_G^bxIs%tWk|aFgSQ(qkM+5PRh=g&ZeAZg35$-kn~}_;~&fP-dCNCzg>{gyW!~LZpn?aZ~Va3~H0Ta)z z<4XPVk@;#%1S@fq<(2#8T04#8$mz>vM;(jek0>Qh!K%t5*4tU(fVYwD3Ri~=D!AmI zV$Dt#TEDX7{lpW%tF&DOlTO)vZodn_%wYu~)ZQ}Qo^cBbDHd{YajkzNxttQW>ST<^ z2~^xhB_y1sjIF5;xchvCn{QVugIE2eYZDZ!-Y-4lJdb34*k({@M zJ5!9Di^||~(IZ4iOoAbtggao+CaYvJynmB^;4r-tY2gS_*P!?U?hlEX;l+^*{%B2n z)|1j9wOHQQ^5Xha>{Cu8_w^8=#6;Dz7kU~RgTqn;ynDm6{xdlkf2vk0UK^oS3yVy4 zE+v&qnlYtPHBk#X&2}r7`@K`J@^e~Qm?iRJ*tbAaZDZTmB&mWMkZp7Kj7^kth#_uX z5z>gC(8Xz|Ie(+#&wiF3;Aey|Db(R*-U)!6;l_5@u?-$>j0SgEl5+c}Lfe-$p-dFH zB_$bC<)x6#A_2Uuo8=^l1@}vK!gvbF#b&MoH8ac3xMxUz$LFb8KU(x$YhtHanM_sw zYOFMBX2iNNSe&a}!;G9nv(tsW4@%3iQcqczOCF*JOBQ@4Orw=o?_vc(9$hfO`>U6& zyY_CUa9pASiJpmv`@oR!k;&$`h8!)$uS=}d-fPddfIdMDUW@%3y1LI(1Q=e$)sz(QC*E;Nfl99YTgk+|@jl`+iF?<_D?4YqV0Zl)lO8YWC@1ZWW^mi{5ePQN<~FQ2NMG$|K{py5akJa zkezmqhN)>MGMp$7=sOo2(7ppv``dCIwf&MaQQis7S596kkiw8Do(jO?EY4iJ4Hec6 z4Hymzu`w)cI9Pbq6GPtTP)x&Lmk;FT=ZCB4>(5}c0?;2l`p&?>&<;2(P8a3lOTNP# zdEzF5qDpkRR&PZC&cS{7xD@qV;(g5X%xI?m$9Q + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

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 “Importazione +e-fatture da Fatturhello”. 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 CRON “Import E-Bills from +Fatturhello”. 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

+

Nella lista dei canali SdI, scegliere o creare un nuovo canale di tipo +“Fatturhello” ed effettuare il login.

+

In Impostazioni > Utenti e aziende > Aziende, selezionare un’azienda e +nella scheda “E-fattura” impostare il canale SdI configurato nel campo +“Canale SdI”. Se alcune fatture di Fatturhello sono già presenti in +Odoo, abilitare la modalità debug e valorizzare “Identificativo +dell’ultima fattura scaricata”. 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 the list of the ES channels, choose or create a new channel of type +“Fatturhello” and login.

+

In Settings > Users & Companies > Companies, select a company and in the +tab “E-invoice”, set the configured ES channel in the field “ES +channel”. If some Fatturhello electronic bills are already present in +Odoo, enable Debug mode and fill “Last downloaded E-Bill identifier”. 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, attivare il CRON “Importazione +e-fatture da Fatturhello”.

+

Per aggiornare lo stato delle fatture elettroniche inviate, attivare il +CRON “Aggiornamento stato e-fatture caricate su Fatturhello”.

+

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, activate the “Import E-Bills +from Fatturhello” CRON.

+

In order to update the status of sent electronic invoices, activate the +“Update the status of E-Invoices uploaded to 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:

+

aleuffre 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_fatturapa_fatturhello/tests/__init__.py b/l10n_it_fatturapa_fatturhello/tests/__init__.py new file mode 100644 index 000000000000..5007d588dca5 --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/tests/common.py b/l10n_it_fatturapa_fatturhello/tests/common.py new file mode 100644 index 000000000000..52d911c9bc21 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/tests/common.py @@ -0,0 +1,77 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json +from unittest import mock + +from requests import Response, codes + +from odoo import tests +from odoo.modules.module import get_resource_path +from odoo.tools import file_open + +MODULE = "l10n_it_fatturapa_fatturhello" +REQUEST_PATH = f"odoo.addons.{MODULE}.models.connector.requests.request" + + +class Common(tests.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.channel = cls.env.ref(f"{MODULE}.sdi_channel_fatturhello") + cls.channel.sudo().company_id = cls.env.company + cls.env.company.update( + { + "fiscalcode": "06363391001", + "vat": "06363391001", + "sdi_channel_id": cls.channel.id, + } + ) + + 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 = get_resource_path(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, channel, values=None): + """Return the login wizard for `channel`, populated with `values`. + + If `values` is omitted, default values will be used. + """ + if values is None: + values = { + "username": "username", + "password": "password", + } + action = channel.fatturhello_action_login() + login_wizard = self._get_records_from_action(action, values=values) + return login_wizard + + def _login(self, channel, values=None): + wizard = self._get_login_wizard(channel) + 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_fatturapa_fatturhello/tests/responses/download_file_content_success b/l10n_it_fatturapa_fatturhello/tests/responses/download_file_content_success new file mode 100644 index 000000000000..8cf5d74ae883 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/tests/responses/download_file_content_success @@ -0,0 +1,154 @@ + + + + + + IT + 05979361218 + + 00001 + FPR12 + 0000000 + + + + + + IT + 02780790107 + + + SOCIETA' ALPHA SRL + + RF01 + + + VIALE ROMA 543 + 07100 + SASSARI + SS + IT + + + + + 03533590174 + + 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 + + + + + + + TD01 + EUR + 2014-12-20 + 456 + 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 + + + + + 1 + PRESTAZIONE DEL SEGUENTE SERVIZIO PROFESSIONALE: LA DESCRIZIONE DELLA PRESTAZIONE PUO' SUPERARE I CENTO CARATTERI CHE RAPPRESENTAVANO IL PRECEDENTE LIMITE DIMENSIONALE. TALE LIMITE NELLA NUOVA VERSIONE E' STATO PORTATO A MILLE CARATTERI + 2000.00 + 2000.00 + 22.00 + + + 22.00 + 2000.00 + 440.00 + I + + + + TP01 + + MP19 + 2015-01-28 + 2440.00 + + + + diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/download_file_headers_success b/l10n_it_fatturapa_fatturhello/tests/responses/download_file_headers_success new file mode 100644 index 000000000000..f868352a330e --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/tests/responses/download_list_empty_success b/l10n_it_fatturapa_fatturhello/tests/responses/download_list_empty_success new file mode 100644 index 000000000000..3082fba06fd6 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/tests/responses/download_list_empty_success @@ -0,0 +1,5 @@ +{ + "success": true, + "total": 2, + "rows": [] +} diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/download_list_success b/l10n_it_fatturapa_fatturhello/tests/responses/download_list_success new file mode 100644 index 000000000000..c4243445abaa --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/tests/responses/download_years_list_success b/l10n_it_fatturapa_fatturhello/tests/responses/download_years_list_success new file mode 100644 index 000000000000..a955fbf0d682 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/tests/responses/download_years_list_success @@ -0,0 +1,3 @@ +[ + "2020" +] diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/login_fail b/l10n_it_fatturapa_fatturhello/tests/responses/login_fail new file mode 100644 index 000000000000..42ae85619940 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/tests/responses/login_fail @@ -0,0 +1,3 @@ +{ + "success": false +} diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/login_success b/l10n_it_fatturapa_fatturhello/tests/responses/login_success new file mode 100644 index 000000000000..5a62113ac3bd --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/tests/responses/status_success b/l10n_it_fatturapa_fatturhello/tests/responses/status_success new file mode 100644 index 000000000000..11ada4734374 --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/tests/responses/upload_success b/l10n_it_fatturapa_fatturhello/tests/responses/upload_success new file mode 100644 index 000000000000..e1a665f91572 --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello/tests/test_download.py b/l10n_it_fatturapa_fatturhello/tests/test_download.py new file mode 100644 index 000000000000..017b68e3ebb5 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/tests/test_download.py @@ -0,0 +1,96 @@ +# 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_fatturapa_fatturhello.tests.common import ( + MODULE, + REQUEST_PATH, + Common, +) + + +@tests.tagged("post_install", "-at_install") +class TestDownload(Common): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.cron = cls.env.ref(f"{MODULE}.ir_cron_fatturhello_import_e_bills") + cls.cron.user_id.company_ids |= cls.env.company + + def _get_e_bills_capturer(self): + return tests.RecordCapturer(self.env["fatturapa.attachment.in"], []) + + 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 + self._login(self.channel) + company = self.env.company + cron = self.cron + # pre-condition + self.assertIn(company, cron.user_id.company_ids) + self.assertEqual(company.sdi_channel_id, self.channel) + self.assertFalse(company.fatturhello_last_downloaded_e_bill_identifer) + + # Act + with ( + mock.patch(REQUEST_PATH) as mock_request, + self._get_e_bills_capturer() as e_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", + ) + e_bills = e_bills_capturer.records + e_bill = e_bills[0] + self.assertEqual(e_bill.name, "IT01234567890_FPR03.xml") + # The created E-Bill can be imported + wizard = ( + self.env["wizard.import.fatturapa"] + .with_context( + active_ids=e_bill.ids, + active_model=e_bill._name, + ) + .create({}) + ) + with self._get_bills_capturer() as bills_capturer: + wizard.importFatturaPA() + bills = bills_capturer.records + self.assertRecordValues( + bills.sorted("ref"), + [ + { + "ref": "123", + }, + { + "ref": "456", + }, + ], + ) diff --git a/l10n_it_fatturapa_fatturhello/tests/test_login.py b/l10n_it_fatturapa_fatturhello/tests/test_login.py new file mode 100644 index 000000000000..1c395831a495 --- /dev/null +++ b/l10n_it_fatturapa_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_fatturapa_fatturhello.tests.common import REQUEST_PATH, Common + + +class TestLogin(Common): + def test_success(self): + """A successful login populates the authtoken and the identifier.""" + # Arrange + channel = self.channel + wizard = self._get_login_wizard(channel) + # pre-condition + self.assertFalse(channel.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(channel.fatturhello_login_authtoken) + self.assertEqual( + channel.company_id.fatturhello_identifier, + "IKZFSKX6645VVG595QG7", + ) + + def test_fail(self): + """A failed login raises an exception.""" + # Arrange + channel = self.channel + wizard = self._get_login_wizard(channel) + # pre-condition + self.assertFalse(channel.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(channel.fatturhello_login_authtoken) diff --git a/l10n_it_fatturapa_fatturhello/tests/test_upload.py b/l10n_it_fatturapa_fatturhello/tests/test_upload.py new file mode 100644 index 000000000000..256cc8de2dd5 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/tests/test_upload.py @@ -0,0 +1,132 @@ +# 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_fatturapa_fatturhello.tests.common import ( + MODULE, + REQUEST_PATH, + Common, +) +from odoo.addons.l10n_it_fatturapa_out.tests.fatturapa_common import FatturaPACommon + + +@tests.tagged("post_install", "-at_install") +class TestUpload(Common, FatturaPACommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env.company.update( + { + "country_id": cls.env.ref("base.it").id, + "state_id": cls.env.ref("base.state_it_rm").id, + "street": "Via Roma", + "city": "Roma", + "zip": "00101", + "fatturapa_fiscal_position_id": cls.env.ref( + "l10n_it_fatturapa.fatturapa_RF01" + ).id, + } + ) + cls.cron = cls.env.ref(f"{MODULE}.ir_cron_fatturhello_update_e_invoices_status") + cls.cron.user_id.company_ids |= cls.env.company + + def test_success(self): + """An invoice can be uploaded.""" + # Arrange + self._login(self.channel) + invoice = self.init_invoice( + "out_invoice", + partner=self.res_partner_fatturapa_0, + amounts=[ + 100, + ], + ) + # pre-condition + self.assertEqual(invoice.state, "draft") + self.assertFalse(invoice.fatturapa_attachment_out_id) + + # 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_open_export_send_sdi() + + # Assert + e_invoice = invoice.fatturapa_attachment_out_id + self.assertEqual(e_invoice.state, "sent_to_fatturhello") + self.assertEqual(invoice.fatturapa_state, "sent_to_fatturhello") + self.assertEqual(e_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.channel) + invoice = self.init_invoice( + "out_invoice", + partner=self.res_partner_fatturapa_0, + amounts=[ + 100, + ], + ) + with mock.patch(REQUEST_PATH) as mock_request: + mock_request.side_effect = [ + self._get_response("login_success"), + self._get_response("upload_success"), + ] + invoice.action_open_export_send_sdi() + e_invoice = invoice.fatturapa_attachment_out_id + # Change the file name to match the one in test's status updates + e_invoice.name = "IT04075500373_EMZjW.xml" + # pre-condition + self.assertEqual(e_invoice.state, "sent_to_fatturhello") + self.assertFalse(e_invoice.fatturhello_last_processed_status_datetime) + + # Act + with mock.patch(REQUEST_PATH) as mock_request: + mock_request.side_effect = [ + self._get_response("login_success"), + self._get_response("status_success"), + ] + cron.method_direct_trigger() + + # Assert + self.assertEqual(e_invoice.state, "validated") + self.assertEqual( + e_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.channel) + invoice = self.init_invoice( + "out_invoice", + partner=self.res_partner_fatturapa_0, + amounts=[ + 100, + ], + ) + with mock.patch(REQUEST_PATH) as mock_request: + mock_request.side_effect = [ + self._get_response("login_success"), + self._get_response("upload_success"), + ] + invoice.action_open_export_send_sdi() + e_invoice = invoice.fatturapa_attachment_out_id + # pre-condition + self.assertEqual(e_invoice.state, "sent_to_fatturhello") + + # Act + e_invoice.unlink() + + # Assert + self.assertFalse(e_invoice.exists()) diff --git a/l10n_it_fatturapa_fatturhello/views/fatturapa_attachment_out_views.xml b/l10n_it_fatturapa_fatturhello/views/fatturapa_attachment_out_views.xml new file mode 100644 index 000000000000..5afb744a44fa --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/views/fatturapa_attachment_out_views.xml @@ -0,0 +1,56 @@ + + + + + Add fatturhello fields to Electronic Invoice search view + fatturapa.attachment.out + + + + + + + + + + Add fatturhello fields to Electronic Invoice form view + fatturapa.attachment.out + + + + + + + + + + + + + + diff --git a/l10n_it_fatturapa_fatturhello/views/res_company_views.xml b/l10n_it_fatturapa_fatturhello/views/res_company_views.xml new file mode 100644 index 000000000000..50562eaa5098 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/views/res_company_views.xml @@ -0,0 +1,40 @@ + + + + + Add Fatturhello fields to Company form + res.company + + + + + + + + + + diff --git a/l10n_it_fatturapa_fatturhello/views/sdi_channel_views.xml b/l10n_it_fatturapa_fatturhello/views/sdi_channel_views.xml new file mode 100644 index 000000000000..945e88954fd1 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/views/sdi_channel_views.xml @@ -0,0 +1,51 @@ + + + + + Add Fatturhello fields to SdI channel form + sdi.channel + + +
+
+ + + + + + + + +
+
+
diff --git a/l10n_it_fatturapa_fatturhello/wizards/__init__.py b/l10n_it_fatturapa_fatturhello/wizards/__init__.py new file mode 100644 index 000000000000..b410d63985e7 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/wizards/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import login diff --git a/l10n_it_fatturapa_fatturhello/wizards/login.py b/l10n_it_fatturapa_fatturhello/wizards/login.py new file mode 100644 index 000000000000..f74e8e1643d8 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/wizards/login.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 _, api, exceptions, fields, models + + +class Login(models.TransientModel): + _name = "l10n_it_fatturapa_fatturhello.login" + _description = "Login to Fatturhello" + + channel_id = fields.Many2one( + comodel_name="sdi.channel", + default=lambda model: model.env.context.get("active_id"), + domain=[ + ("channel_type", "=", "fatturhello"), + ], + required=True, + ) + username = fields.Char( + required=True, + compute="_compute_username", + store=True, + readonly=False, + ) + password = fields.Char( + required=True, + ) + + @api.depends( + "channel_id", + ) + def _compute_username(self): + for login_wiz in self: + login_wiz.username = login_wiz.channel_id.fatturhello_username + + def _check_channel_config(self, channel): + """Ensure that `channel` is ready for login to fatturhello.""" + if channel.channel_type != "fatturhello": + raise exceptions.UserError(_("You can only login to Fatturhello channels")) + if not channel.fatturhello_base_url: + raise exceptions.UserError(_("Configure the base URL in order to login")) + return True + + @api.onchange("channel_id") + def onchange_channel(self): + """Check channel configuration.""" + self.ensure_one() + if channel := self.channel_id: + self._check_channel_config(channel) + else: + raise exceptions.UserError(_("Please select a channel to login to")) + + def confirm(self): + """Login and save the returned token in `channel_id`.""" + self.ensure_one() + credentials = self.env["l10n_it_fatturapa_fatturhello.connector"].get_secrets( + self.channel_id.fatturhello_base_url, + self.channel_id.company_id, + self.username, + self.password, + ) + credentials["username"] = self.username + self.channel_id._fatturhello_update_credentials(credentials) + # Remove the password from the DB + self.password = "" + return True diff --git a/l10n_it_fatturapa_fatturhello/wizards/login_views.xml b/l10n_it_fatturapa_fatturhello/wizards/login_views.xml new file mode 100644 index 000000000000..246744b91549 --- /dev/null +++ b/l10n_it_fatturapa_fatturhello/wizards/login_views.xml @@ -0,0 +1,31 @@ + + + + + Login to Fatturhello for view + l10n_it_fatturapa_fatturhello.login + +
+ + + + + + + +
+
+
+
+
+
From 2d05eddc50bd068cea90af6fac921cc1afc3791c Mon Sep 17 00:00:00 2001 From: mymage Date: Mon, 11 May 2026 07:01:16 +0000 Subject: [PATCH 2/3] Added translation using Weblate (Italian) --- l10n_it_fatturapa_fatturhello/i18n/it.po | 538 +++++++++++++++++++++++ 1 file changed, 538 insertions(+) create mode 100644 l10n_it_fatturapa_fatturhello/i18n/it.po diff --git a/l10n_it_fatturapa_fatturhello/i18n/it.po b/l10n_it_fatturapa_fatturhello/i18n/it.po new file mode 100644 index 000000000000..58b645e722c0 --- /dev/null +++ b/l10n_it_fatturapa_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" From cc27be3306e40dfe776a9b1c6c73dcda0640fa44 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Thu, 23 Jul 2026 12:29:55 +0200 Subject: [PATCH 3/3] [MIG] l10n_it_fatturapa_fatturhello: Migration to 18.0 --- .../README.rst | 84 ++--- .../__init__.py | 0 .../__manifest__.py | 13 +- .../data/neutralize.sql | 2 +- .../i18n/it.po | 0 .../i18n/l10n_it_fatturapa_fatturhello.pot | 0 .../migrations/18.0.1.0.0/post-migration.py | 117 ++++++ .../models/__init__.py | 3 +- .../models/account_move.py | 336 ++++++++++++++++++ .../models/connector.py | 22 +- l10n_it_edi_fatturhello/models/res_company.py | 66 ++++ .../models/res_config_settings.py | 38 ++ .../pyproject.toml | 0 .../readme/CONFIGURE.md | 10 +- .../readme/CONTRIBUTORS.md | 0 .../readme/CREDITS.md | 0 .../readme/DESCRIPTION.md | 4 +- .../readme/ROADMAP.md | 0 .../readme/USAGE.md | 8 +- .../security/ir.model.access.csv | 2 +- .../static/description/icon.png | Bin .../static/description/index.html | 96 +++-- .../tests/__init__.py | 0 .../tests/common.py | 38 +- .../responses/download_file_content_success | 42 +-- .../responses/download_file_headers_success | 0 .../responses/download_list_empty_success | 0 .../tests/responses/download_list_success | 0 .../download_years_list_empty_success | 1 + .../responses/download_years_list_success | 0 .../tests/responses/login_fail | 0 .../tests/responses/login_success | 0 .../tests/responses/status_success | 0 .../tests/responses/upload_success | 0 .../tests/test_download.py | 39 +- .../tests/test_login.py | 20 +- .../tests/test_upload.py | 84 +++-- .../views/account_move_views.xml | 41 +++ .../views/res_config_settings_views.xml | 49 +++ .../wizards/__init__.py | 0 l10n_it_edi_fatturhello/wizards/login.py | 66 ++++ .../wizards/login_views.xml | 4 +- .../data/ir_cron_data.xml | 34 -- .../data/sdi_channel_data.xml | 11 - .../models/account_move.py | 22 -- .../models/fatturapa_attachment_out.py | 66 ---- .../models/res_company.py | 20 -- .../models/sdi_channel.py | 275 -------------- .../views/fatturapa_attachment_out_views.xml | 56 --- .../views/res_company_views.xml | 40 --- .../views/sdi_channel_views.xml | 51 --- .../wizards/login.py | 66 ---- 52 files changed, 908 insertions(+), 918 deletions(-) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/README.rst (61%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/__init__.py (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/__manifest__.py (67%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/data/neutralize.sql (82%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/i18n/it.po (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/i18n/l10n_it_fatturapa_fatturhello.pot (100%) create mode 100644 l10n_it_edi_fatturhello/migrations/18.0.1.0.0/post-migration.py rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/models/__init__.py (68%) create mode 100644 l10n_it_edi_fatturhello/models/account_move.py rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/models/connector.py (97%) create mode 100644 l10n_it_edi_fatturhello/models/res_company.py create mode 100644 l10n_it_edi_fatturhello/models/res_config_settings.py rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/pyproject.toml (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/readme/CONFIGURE.md (58%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/readme/CONTRIBUTORS.md (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/readme/CREDITS.md (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/readme/DESCRIPTION.md (78%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/readme/ROADMAP.md (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/readme/USAGE.md (58%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/security/ir.model.access.csv (55%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/static/description/icon.png (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/static/description/index.html (75%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/__init__.py (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/common.py (68%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/download_file_content_success (70%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/download_file_headers_success (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/download_list_empty_success (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/download_list_success (100%) create mode 100644 l10n_it_edi_fatturhello/tests/responses/download_years_list_empty_success rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/download_years_list_success (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/login_fail (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/login_success (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/status_success (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/responses/upload_success (100%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/test_download.py (62%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/test_login.py (66%) rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/tests/test_upload.py (55%) create mode 100644 l10n_it_edi_fatturhello/views/account_move_views.xml create mode 100644 l10n_it_edi_fatturhello/views/res_config_settings_views.xml rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/wizards/__init__.py (100%) create mode 100644 l10n_it_edi_fatturhello/wizards/login.py rename {l10n_it_fatturapa_fatturhello => l10n_it_edi_fatturhello}/wizards/login_views.xml (87%) delete mode 100644 l10n_it_fatturapa_fatturhello/data/ir_cron_data.xml delete mode 100644 l10n_it_fatturapa_fatturhello/data/sdi_channel_data.xml delete mode 100644 l10n_it_fatturapa_fatturhello/models/account_move.py delete mode 100644 l10n_it_fatturapa_fatturhello/models/fatturapa_attachment_out.py delete mode 100644 l10n_it_fatturapa_fatturhello/models/res_company.py delete mode 100644 l10n_it_fatturapa_fatturhello/models/sdi_channel.py delete mode 100644 l10n_it_fatturapa_fatturhello/views/fatturapa_attachment_out_views.xml delete mode 100644 l10n_it_fatturapa_fatturhello/views/res_company_views.xml delete mode 100644 l10n_it_fatturapa_fatturhello/views/sdi_channel_views.xml delete mode 100644 l10n_it_fatturapa_fatturhello/wizards/login.py diff --git a/l10n_it_fatturapa_fatturhello/README.rst b/l10n_it_edi_fatturhello/README.rst similarity index 61% rename from l10n_it_fatturapa_fatturhello/README.rst rename to l10n_it_edi_fatturhello/README.rst index 32444738c358..cdfddc2ed165 100644 --- a/l10n_it_fatturapa_fatturhello/README.rst +++ b/l10n_it_edi_fatturhello/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ================================================ ITA - Fattura elettronica - Supporto Fatturhello ================================================ @@ -17,17 +13,17 @@ ITA - Fattura elettronica - Supporto Fatturhello .. |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/license-AGPL--3-blue.png +.. |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/16.0/l10n_it_fatturapa_fatturhello + :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-16-0/l10n-italy-16-0-l10n_it_fatturapa_fatturhello + :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=16.0 + :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| @@ -37,10 +33,10 @@ ITA - Fattura elettronica - Supporto Fatturhello 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 "Importazione -e-fatture da Fatturhello". Ad ogni esecuzione tutte le fatture -elettroniche successive a quella configurata (a meno di duplicati) -verranno scaricate. +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. @@ -50,9 +46,9 @@ https://documenter.getpostman.com/view/3340600/RWTiyLCz. This module allows you to send and receive electronic invoice/bill XML files via Fatturhello (https://fatturhello.it). -The receipt happens periodically, with the CRON "Import E-Bills from -Fatturhello". During each execution all the E-Bills subsequent the -configured one (except for duplicates) will be downloaded. +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. @@ -67,16 +63,14 @@ Configuration **Italiano** -Nella lista dei canali SdI, scegliere o creare un nuovo canale di tipo -"Fatturhello" ed effettuare il login. +In Fatturazione > Configurazione > Impostazioni, abilitare "Usa +Fatturhello" ed effettuare il login. -In Impostazioni > Utenti e aziende > Aziende, selezionare un'azienda e -nella scheda "E-fattura" impostare il canale SdI configurato nel campo -"Canale SdI". Se alcune fatture di Fatturhello sono già presenti in -Odoo, abilitare la modalità debug e valorizzare "Identificativo -dell'ultima fattura scaricata". Se "Identificativo dell'ultima fattura -scaricata" è vuoto, verranno scaricate tutte le fatture elettroniche -presenti in Fatturhello. +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* @@ -85,15 +79,13 @@ Per inviare in automatico a SdI le fatture caricate da Odoo, abilitare **English** -In the list of the ES channels, choose or create a new channel of type -"Fatturhello" and login. +In Invoicing > Configuration > Settings, enable "Use Fatturhello" and +login. -In Settings > Users & Companies > Companies, select a company and in the -tab "E-invoice", set the configured ES channel in the field "ES -channel". If some Fatturhello electronic bills are already present in -Odoo, enable Debug mode and fill "Last downloaded E-Bill identifier". If -"Last downloaded E-Bill identifier" is empty, all the E-Bills in -Fatturhello will be downloaded. +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* @@ -105,11 +97,9 @@ Usage **Italiano** -Per ricevere le fatture elettroniche, attivare il CRON "Importazione -e-fatture da Fatturhello". - -Per aggiornare lo stato delle fatture elettroniche inviate, attivare il -CRON "Aggiornamento stato e-fatture caricate su Fatturhello". +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 @@ -121,11 +111,9 @@ questo caso: **English** -In order to receive the electronic bills, activate the "Import E-Bills -from Fatturhello" CRON. - -In order to update the status of sent electronic invoices, activate the -"Update the status of E-Invoices uploaded to Fatturhello" CRON. +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 @@ -146,7 +134,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -185,17 +173,17 @@ 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-aleuffre| image:: https://github.com/aleuffre.png?size=40px - :target: https://github.com/aleuffre - :alt: aleuffre +.. |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-aleuffre| |maintainer-SirPyTech| +|maintainer-HekkiMelody| |maintainer-SirPyTech| -This module is part of the `OCA/l10n-italy `_ project on GitHub. +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_fatturapa_fatturhello/__init__.py b/l10n_it_edi_fatturhello/__init__.py similarity index 100% rename from l10n_it_fatturapa_fatturhello/__init__.py rename to l10n_it_edi_fatturhello/__init__.py diff --git a/l10n_it_fatturapa_fatturhello/__manifest__.py b/l10n_it_edi_fatturhello/__manifest__.py similarity index 67% rename from l10n_it_fatturapa_fatturhello/__manifest__.py rename to l10n_it_edi_fatturhello/__manifest__.py index 399f249b534f..a3e286edbd73 100644 --- a/l10n_it_fatturapa_fatturhello/__manifest__.py +++ b/l10n_it_edi_fatturhello/__manifest__.py @@ -3,26 +3,23 @@ { "name": "ITA - Fattura elettronica - Supporto Fatturhello", - "version": "16.0.1.0.0", + "version": "18.0.1.0.0", "category": "Localization/Italy", "summary": "Invio e ricezione fatture elettroniche tramite Fatturhello", "author": "PyTech, Odoo Community Association (OCA)", "maintainers": [ - "aleuffre", + "HekkiMelody", "SirPyTech", ], "website": "https://github.com/OCA/l10n-italy", "license": "AGPL-3", "depends": [ - "l10n_it_sdi_channel", + "l10n_it_edi_sdi", ], "data": [ - "data/ir_cron_data.xml", - "data/sdi_channel_data.xml", "security/ir.model.access.csv", - "views/fatturapa_attachment_out_views.xml", - "views/res_company_views.xml", - "views/sdi_channel_views.xml", + "views/account_move_views.xml", + "views/res_config_settings_views.xml", "wizards/login_views.xml", ], } diff --git a/l10n_it_fatturapa_fatturhello/data/neutralize.sql b/l10n_it_edi_fatturhello/data/neutralize.sql similarity index 82% rename from l10n_it_fatturapa_fatturhello/data/neutralize.sql rename to l10n_it_edi_fatturhello/data/neutralize.sql index 5912d038dce0..d9ca6ef2129a 100644 --- a/l10n_it_fatturapa_fatturhello/data/neutralize.sql +++ b/l10n_it_edi_fatturhello/data/neutralize.sql @@ -1,3 +1,3 @@ -- Remove token for Fatturhello authentication -UPDATE sdi_channel +UPDATE res_company SET fatturhello_login_authtoken = null; diff --git a/l10n_it_fatturapa_fatturhello/i18n/it.po b/l10n_it_edi_fatturhello/i18n/it.po similarity index 100% rename from l10n_it_fatturapa_fatturhello/i18n/it.po rename to l10n_it_edi_fatturhello/i18n/it.po diff --git a/l10n_it_fatturapa_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot b/l10n_it_edi_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot similarity index 100% rename from l10n_it_fatturapa_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot rename to l10n_it_edi_fatturhello/i18n/l10n_it_fatturapa_fatturhello.pot 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_fatturapa_fatturhello/models/__init__.py b/l10n_it_edi_fatturhello/models/__init__.py similarity index 68% rename from l10n_it_fatturapa_fatturhello/models/__init__.py rename to l10n_it_edi_fatturhello/models/__init__.py index acd643cc186c..50d15fe21ffd 100644 --- a/l10n_it_fatturapa_fatturhello/models/__init__.py +++ b/l10n_it_edi_fatturhello/models/__init__.py @@ -1,7 +1,6 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from . import account_move -from . import fatturapa_attachment_out from . import connector from . import res_company -from . import sdi_channel +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_fatturapa_fatturhello/models/connector.py b/l10n_it_edi_fatturhello/models/connector.py similarity index 97% rename from l10n_it_fatturapa_fatturhello/models/connector.py rename to l10n_it_edi_fatturhello/models/connector.py index 84ed7c4eb019..a19e63a11b5b 100644 --- a/l10n_it_fatturapa_fatturhello/models/connector.py +++ b/l10n_it_edi_fatturhello/models/connector.py @@ -21,7 +21,7 @@ class FatturhelloConnector(models.AbstractModel): - _name = "l10n_it_fatturapa_fatturhello.connector" + _name = "l10n_it_edi_fatturhello.connector" _description = "Manage communication with Fatturhello API." def _get_relative_paths(self): @@ -96,7 +96,7 @@ def _get_company_data(self, company, companies_data): each representing the data of one company. .""" company.ensure_one() - company_vat, company_fc = company.vat, company.fiscalcode + 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 @@ -233,29 +233,31 @@ def _upload_post( return result - def _prepare_e_invoice_upload_values(self, upload_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"], - "state": "sent_to_fatturhello", + "l10n_it_edi_state": "sent_to_fatturhello", } - def upload_e_invoice(self, base_url, session_token, company_identifier, e_invoice): + 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. """ - e_invoice.ensure_one() + move.ensure_one() upload_result = self._upload_post( base_url, session_token, company_identifier, - e_invoice.name, - e_invoice.raw, + file_name, + file_content, ) - e_invoice.update(self._prepare_e_invoice_upload_values(upload_result)) - return True + move.update(self._prepare_move_upload_values(upload_result)) + return upload_result def _get_sdi_files_list( self, 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_fatturapa_fatturhello/pyproject.toml b/l10n_it_edi_fatturhello/pyproject.toml similarity index 100% rename from l10n_it_fatturapa_fatturhello/pyproject.toml rename to l10n_it_edi_fatturhello/pyproject.toml diff --git a/l10n_it_fatturapa_fatturhello/readme/CONFIGURE.md b/l10n_it_edi_fatturhello/readme/CONFIGURE.md similarity index 58% rename from l10n_it_fatturapa_fatturhello/readme/CONFIGURE.md rename to l10n_it_edi_fatturhello/readme/CONFIGURE.md index be1b801acf90..10297f7f84ff 100644 --- a/l10n_it_fatturapa_fatturhello/readme/CONFIGURE.md +++ b/l10n_it_edi_fatturhello/readme/CONFIGURE.md @@ -1,9 +1,8 @@ **Italiano** -Nella lista dei canali SdI, scegliere o creare un nuovo canale di tipo "Fatturhello" ed effettuare il login. +In Fatturazione > Configurazione > Impostazioni, abilitare "Usa Fatturhello" ed effettuare il login. -In Impostazioni > Utenti e aziende > Aziende, selezionare un'azienda e nella scheda "E-fattura" impostare il canale SdI configurato nel campo "Canale SdI". -Se alcune fatture di Fatturhello sono già presenti in Odoo, abilitare la modalità debug e valorizzare "Identificativo dell'ultima fattura scaricata". +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* @@ -12,10 +11,9 @@ Per inviare in automatico a SdI le fatture caricate da Odoo, abilitare "Spedisci **English** -In the list of the ES channels, choose or create a new channel of type "Fatturhello" and login. +In Invoicing > Configuration > Settings, enable "Use Fatturhello" and login. -In Settings > Users & Companies > Companies, select a company and in the tab "E-invoice", set the configured ES channel in the field "ES channel". -If some Fatturhello electronic bills are already present in Odoo, enable Debug mode and fill "Last downloaded E-Bill identifier". +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* diff --git a/l10n_it_fatturapa_fatturhello/readme/CONTRIBUTORS.md b/l10n_it_edi_fatturhello/readme/CONTRIBUTORS.md similarity index 100% rename from l10n_it_fatturapa_fatturhello/readme/CONTRIBUTORS.md rename to l10n_it_edi_fatturhello/readme/CONTRIBUTORS.md diff --git a/l10n_it_fatturapa_fatturhello/readme/CREDITS.md b/l10n_it_edi_fatturhello/readme/CREDITS.md similarity index 100% rename from l10n_it_fatturapa_fatturhello/readme/CREDITS.md rename to l10n_it_edi_fatturhello/readme/CREDITS.md diff --git a/l10n_it_fatturapa_fatturhello/readme/DESCRIPTION.md b/l10n_it_edi_fatturhello/readme/DESCRIPTION.md similarity index 78% rename from l10n_it_fatturapa_fatturhello/readme/DESCRIPTION.md rename to l10n_it_edi_fatturhello/readme/DESCRIPTION.md index bed8b80adbeb..6b4e69f9aa75 100644 --- a/l10n_it_fatturapa_fatturhello/readme/DESCRIPTION.md +++ b/l10n_it_edi_fatturhello/readme/DESCRIPTION.md @@ -3,7 +3,7 @@ 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 "Importazione e-fatture da Fatturhello". +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. @@ -13,7 +13,7 @@ La documentazione delle API è https://documenter.getpostman.com/view/3340600/RW This module allows you to send and receive electronic invoice/bill XML files via Fatturhello (https://fatturhello.it). -The receipt happens periodically, with the CRON "Import E-Bills from Fatturhello". +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_fatturapa_fatturhello/readme/ROADMAP.md b/l10n_it_edi_fatturhello/readme/ROADMAP.md similarity index 100% rename from l10n_it_fatturapa_fatturhello/readme/ROADMAP.md rename to l10n_it_edi_fatturhello/readme/ROADMAP.md diff --git a/l10n_it_fatturapa_fatturhello/readme/USAGE.md b/l10n_it_edi_fatturhello/readme/USAGE.md similarity index 58% rename from l10n_it_fatturapa_fatturhello/readme/USAGE.md rename to l10n_it_edi_fatturhello/readme/USAGE.md index df135b428e05..8718380b9d40 100644 --- a/l10n_it_fatturapa_fatturhello/readme/USAGE.md +++ b/l10n_it_edi_fatturhello/readme/USAGE.md @@ -1,8 +1,6 @@ **Italiano** -Per ricevere le fatture elettroniche, attivare il CRON "Importazione e-fatture da Fatturhello". - -Per aggiornare lo stato delle fatture elettroniche inviate, attivare il CRON "Aggiornamento stato e-fatture caricate su Fatturhello". +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: @@ -12,9 +10,7 @@ In questo caso: **English** -In order to receive the electronic bills, activate the "Import E-Bills from Fatturhello" CRON. - -In order to update the status of sent electronic invoices, activate the "Update the status of E-Invoices uploaded to Fatturhello" CRON. +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: diff --git a/l10n_it_fatturapa_fatturhello/security/ir.model.access.csv b/l10n_it_edi_fatturhello/security/ir.model.access.csv similarity index 55% rename from l10n_it_fatturapa_fatturhello/security/ir.model.access.csv rename to l10n_it_edi_fatturhello/security/ir.model.access.csv index ed65857cb4d5..753ad2046474 100644 --- a/l10n_it_fatturapa_fatturhello/security/ir.model.access.csv +++ b/l10n_it_edi_fatturhello/security/ir.model.access.csv @@ -1,2 +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_fatturapa_fatturhello.model_l10n_it_fatturapa_fatturhello_login,account.group_account_manager,1,1,1,1 +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_fatturapa_fatturhello/static/description/icon.png b/l10n_it_edi_fatturhello/static/description/icon.png similarity index 100% rename from l10n_it_fatturapa_fatturhello/static/description/icon.png rename to l10n_it_edi_fatturhello/static/description/icon.png diff --git a/l10n_it_fatturapa_fatturhello/static/description/index.html b/l10n_it_edi_fatturhello/static/description/index.html similarity index 75% rename from l10n_it_fatturapa_fatturhello/static/description/index.html rename to l10n_it_edi_fatturhello/static/description/index.html index a6244d488412..8a114c81f0ce 100644 --- a/l10n_it_fatturapa_fatturhello/static/description/index.html +++ b/l10n_it_edi_fatturhello/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +ITA - Fattura elettronica - Supporto Fatturhello -
+
+

ITA - Fattura elettronica - Supporto Fatturhello

- - -Odoo Community Association - -
-

ITA - Fattura elettronica - Supporto Fatturhello

-

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

+

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 “Importazione -e-fatture da Fatturhello”. Ad ogni esecuzione tutte le fatture -elettroniche successive a quella configurata (a meno di duplicati) -verranno scaricate.

+

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 CRON “Import E-Bills from -Fatturhello”. During each execution all the E-Bills subsequent the -configured one (except for duplicates) will be downloaded.

+

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

@@ -409,40 +404,35 @@

ITA - Fattura elettronica - Supporto Fatturhello

-

Configuration

+

Configuration

Italiano

-

Nella lista dei canali SdI, scegliere o creare un nuovo canale di tipo -“Fatturhello” ed effettuare il login.

-

In Impostazioni > Utenti e aziende > Aziende, selezionare un’azienda e -nella scheda “E-fattura” impostare il canale SdI configurato nel campo -“Canale SdI”. Se alcune fatture di Fatturhello sono già presenti in -Odoo, abilitare la modalità debug e valorizzare “Identificativo -dell’ultima fattura scaricata”. Se “Identificativo dell’ultima fattura -scaricata” è vuoto, verranno scaricate tutte le fatture elettroniche -presenti in Fatturhello.

+

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 the list of the ES channels, choose or create a new channel of type -“Fatturhello” and login.

-

In Settings > Users & Companies > Companies, select a company and in the -tab “E-invoice”, set the configured ES channel in the field “ES -channel”. If some Fatturhello electronic bills are already present in -Odoo, enable Debug mode and fill “Last downloaded E-Bill identifier”. If -“Last downloaded E-Bill identifier” is empty, all the E-Bills in -Fatturhello will be downloaded.

+

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

+

Usage

Italiano

-

Per ricevere le fatture elettroniche, attivare il CRON “Importazione -e-fatture da Fatturhello”.

-

Per aggiornare lo stato delle fatture elettroniche inviate, attivare il -CRON “Aggiornamento stato e-fatture caricate su Fatturhello”.

+

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:

@@ -452,10 +442,9 @@

Usage

  • In Odoo: ricreare la fattura elettronica e inviarla.
  • English

    -

    In order to receive the electronic bills, activate the “Import E-Bills -from Fatturhello” CRON.

    -

    In order to update the status of sent electronic invoices, activate the -“Update the status of E-Invoices uploaded to Fatturhello” CRON.

    +

    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:

    @@ -466,29 +455,29 @@

    Usage

    -

    Known issues / Roadmap

    +

    Known issues / Roadmap

    • Parse more types of status notification
    -

    Bug Tracker

    +

    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.

    +feedback.

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • PyTech
    -

    Contributors

    +

    Contributors

    -

    Other credits

    +

    Other credits

    The development of this module has been financially supported by:

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -513,12 +502,11 @@

    Maintainers

    mission is to support the collaborative development of Odoo features and promote its widespread use.

    Current maintainers:

    -

    aleuffre SirPyTech

    -

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

    +

    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_fatturapa_fatturhello/tests/__init__.py b/l10n_it_edi_fatturhello/tests/__init__.py similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/__init__.py rename to l10n_it_edi_fatturhello/tests/__init__.py diff --git a/l10n_it_fatturapa_fatturhello/tests/common.py b/l10n_it_edi_fatturhello/tests/common.py similarity index 68% rename from l10n_it_fatturapa_fatturhello/tests/common.py rename to l10n_it_edi_fatturhello/tests/common.py index 52d911c9bc21..4b90c691176c 100644 --- a/l10n_it_fatturapa_fatturhello/tests/common.py +++ b/l10n_it_edi_fatturhello/tests/common.py @@ -2,31 +2,33 @@ # 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 import tests -from odoo.modules.module import get_resource_path -from odoo.tools import file_open +from odoo.tools import file_open, file_path -MODULE = "l10n_it_fatturapa_fatturhello" +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(tests.TransactionCase): +class Common(TestItEdi): @classmethod def setUpClass(cls): super().setUpClass() - cls.channel = cls.env.ref(f"{MODULE}.sdi_channel_fatturhello") - cls.channel.sudo().company_id = cls.env.company - cls.env.company.update( + cls.company.update( { - "fiscalcode": "06363391001", + "fatturhello_is_used": True, + "l10n_it_codice_fiscale": "06363391001", "vat": "06363391001", - "sdi_channel_id": cls.channel.id, } ) + 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`. @@ -43,7 +45,7 @@ def _get_records_from_action(self, action, values=None): def _get_file_content(self, name): """Get the binary content of the file `name` in test responses.""" - path = get_resource_path(MODULE, "tests", "responses", name) + path = file_path(join(MODULE, "tests", "responses", name)) with file_open(path, mode="rb") as f: return f.read() @@ -56,8 +58,8 @@ def _get_response(self, name, headers_name=None): response.headers = json.loads(self._get_file_content(headers_name).decode()) return response - def _get_login_wizard(self, channel, values=None): - """Return the login wizard for `channel`, populated with `values`. + 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. """ @@ -66,12 +68,16 @@ def _get_login_wizard(self, channel, values=None): "username": "username", "password": "password", } - action = channel.fatturhello_action_login() + 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, channel, values=None): - wizard = self._get_login_wizard(channel) + 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_fatturapa_fatturhello/tests/responses/download_file_content_success b/l10n_it_edi_fatturhello/tests/responses/download_file_content_success similarity index 70% rename from l10n_it_fatturapa_fatturhello/tests/responses/download_file_content_success rename to l10n_it_edi_fatturhello/tests/responses/download_file_content_success index 8cf5d74ae883..54cc69718d85 100644 --- a/l10n_it_fatturapa_fatturhello/tests/responses/download_file_content_success +++ b/l10n_it_edi_fatturhello/tests/responses/download_file_content_success @@ -32,7 +32,7 @@ - 03533590174 + 06363391001 BETA GAMMA @@ -111,44 +111,4 @@ - - - - TD01 - EUR - 2014-12-20 - 456 - 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 - - - - - 1 - PRESTAZIONE DEL SEGUENTE SERVIZIO PROFESSIONALE: LA DESCRIZIONE DELLA PRESTAZIONE PUO' SUPERARE I CENTO CARATTERI CHE RAPPRESENTAVANO IL PRECEDENTE LIMITE DIMENSIONALE. TALE LIMITE NELLA NUOVA VERSIONE E' STATO PORTATO A MILLE CARATTERI - 2000.00 - 2000.00 - 22.00 - - - 22.00 - 2000.00 - 440.00 - I - - - - TP01 - - MP19 - 2015-01-28 - 2440.00 - - - diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/download_file_headers_success b/l10n_it_edi_fatturhello/tests/responses/download_file_headers_success similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/responses/download_file_headers_success rename to l10n_it_edi_fatturhello/tests/responses/download_file_headers_success diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/download_list_empty_success b/l10n_it_edi_fatturhello/tests/responses/download_list_empty_success similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/responses/download_list_empty_success rename to l10n_it_edi_fatturhello/tests/responses/download_list_empty_success diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/download_list_success b/l10n_it_edi_fatturhello/tests/responses/download_list_success similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/responses/download_list_success rename to l10n_it_edi_fatturhello/tests/responses/download_list_success 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_fatturapa_fatturhello/tests/responses/download_years_list_success b/l10n_it_edi_fatturhello/tests/responses/download_years_list_success similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/responses/download_years_list_success rename to l10n_it_edi_fatturhello/tests/responses/download_years_list_success diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/login_fail b/l10n_it_edi_fatturhello/tests/responses/login_fail similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/responses/login_fail rename to l10n_it_edi_fatturhello/tests/responses/login_fail diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/login_success b/l10n_it_edi_fatturhello/tests/responses/login_success similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/responses/login_success rename to l10n_it_edi_fatturhello/tests/responses/login_success diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/status_success b/l10n_it_edi_fatturhello/tests/responses/status_success similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/responses/status_success rename to l10n_it_edi_fatturhello/tests/responses/status_success diff --git a/l10n_it_fatturapa_fatturhello/tests/responses/upload_success b/l10n_it_edi_fatturhello/tests/responses/upload_success similarity index 100% rename from l10n_it_fatturapa_fatturhello/tests/responses/upload_success rename to l10n_it_edi_fatturhello/tests/responses/upload_success diff --git a/l10n_it_fatturapa_fatturhello/tests/test_download.py b/l10n_it_edi_fatturhello/tests/test_download.py similarity index 62% rename from l10n_it_fatturapa_fatturhello/tests/test_download.py rename to l10n_it_edi_fatturhello/tests/test_download.py index 017b68e3ebb5..9dc45d54af4d 100644 --- a/l10n_it_fatturapa_fatturhello/tests/test_download.py +++ b/l10n_it_edi_fatturhello/tests/test_download.py @@ -5,8 +5,7 @@ from odoo import tests -from odoo.addons.l10n_it_fatturapa_fatturhello.tests.common import ( - MODULE, +from odoo.addons.l10n_it_edi_fatturhello.tests.common import ( REQUEST_PATH, Common, ) @@ -14,15 +13,6 @@ @tests.tagged("post_install", "-at_install") class TestDownload(Common): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.cron = cls.env.ref(f"{MODULE}.ir_cron_fatturhello_import_e_bills") - cls.cron.user_id.company_ids |= cls.env.company - - def _get_e_bills_capturer(self): - return tests.RecordCapturer(self.env["fatturapa.attachment.in"], []) - def _get_bills_capturer(self): return tests.RecordCapturer( self.env["account.move"], @@ -34,18 +24,17 @@ def _get_bills_capturer(self): def test_success(self): """E-Bills can be downloaded.""" # Arrange - self._login(self.channel) - company = self.env.company + company = self.company + self._login(company) cron = self.cron # pre-condition self.assertIn(company, cron.user_id.company_ids) - self.assertEqual(company.sdi_channel_id, self.channel) self.assertFalse(company.fatturhello_last_downloaded_e_bill_identifer) # Act with ( mock.patch(REQUEST_PATH) as mock_request, - self._get_e_bills_capturer() as e_bills_capturer, + self._get_bills_capturer() as bills_capturer, ): mock_request.side_effect = [ self._get_response("login_success"), @@ -68,29 +57,15 @@ def test_success(self): company.fatturhello_last_downloaded_e_bill_identifer, "201801000121236", ) - e_bills = e_bills_capturer.records - e_bill = e_bills[0] - self.assertEqual(e_bill.name, "IT01234567890_FPR03.xml") - # The created E-Bill can be imported - wizard = ( - self.env["wizard.import.fatturapa"] - .with_context( - active_ids=e_bill.ids, - active_model=e_bill._name, - ) - .create({}) - ) - with self._get_bills_capturer() as bills_capturer: - wizard.importFatturaPA() bills = bills_capturer.records + self.assertEqual( + bills.l10n_it_edi_attachment_id.name, "IT01234567890_FPR03.xml" + ) self.assertRecordValues( bills.sorted("ref"), [ { "ref": "123", }, - { - "ref": "456", - }, ], ) diff --git a/l10n_it_fatturapa_fatturhello/tests/test_login.py b/l10n_it_edi_fatturhello/tests/test_login.py similarity index 66% rename from l10n_it_fatturapa_fatturhello/tests/test_login.py rename to l10n_it_edi_fatturhello/tests/test_login.py index 1c395831a495..6fcff47ce50e 100644 --- a/l10n_it_fatturapa_fatturhello/tests/test_login.py +++ b/l10n_it_edi_fatturhello/tests/test_login.py @@ -5,17 +5,17 @@ from odoo import exceptions -from odoo.addons.l10n_it_fatturapa_fatturhello.tests.common import REQUEST_PATH, Common +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 - channel = self.channel - wizard = self._get_login_wizard(channel) + company = self.company + wizard = self._get_login_wizard(company) # pre-condition - self.assertFalse(channel.fatturhello_login_authtoken) + self.assertFalse(company.fatturhello_login_authtoken) # Act with mock.patch(REQUEST_PATH) as mock_request: @@ -23,19 +23,19 @@ def test_success(self): wizard.confirm() # Assert - self.assertTrue(channel.fatturhello_login_authtoken) + self.assertTrue(company.fatturhello_login_authtoken) self.assertEqual( - channel.company_id.fatturhello_identifier, + company.fatturhello_identifier, "IKZFSKX6645VVG595QG7", ) def test_fail(self): """A failed login raises an exception.""" # Arrange - channel = self.channel - wizard = self._get_login_wizard(channel) + company = self.company + wizard = self._get_login_wizard(company) # pre-condition - self.assertFalse(channel.fatturhello_login_authtoken) + self.assertFalse(company.fatturhello_login_authtoken) # Act with ( @@ -48,4 +48,4 @@ def test_fail(self): # Assert self.assertIn("Login with user and password failed", exc_message) - self.assertFalse(channel.fatturhello_login_authtoken) + self.assertFalse(company.fatturhello_login_authtoken) diff --git a/l10n_it_fatturapa_fatturhello/tests/test_upload.py b/l10n_it_edi_fatturhello/tests/test_upload.py similarity index 55% rename from l10n_it_fatturapa_fatturhello/tests/test_upload.py rename to l10n_it_edi_fatturhello/tests/test_upload.py index 256cc8de2dd5..67f81117c65d 100644 --- a/l10n_it_fatturapa_fatturhello/tests/test_upload.py +++ b/l10n_it_edi_fatturhello/tests/test_upload.py @@ -6,48 +6,43 @@ from odoo import tests -from odoo.addons.l10n_it_fatturapa_fatturhello.tests.common import ( - MODULE, +from odoo.addons.l10n_it_edi_fatturhello.tests.common import ( REQUEST_PATH, Common, ) -from odoo.addons.l10n_it_fatturapa_out.tests.fatturapa_common import FatturaPACommon @tests.tagged("post_install", "-at_install") -class TestUpload(Common, FatturaPACommon): +class TestUpload(Common): @classmethod def setUpClass(cls): super().setUpClass() - cls.env.company.update( + cls.company.update( { - "country_id": cls.env.ref("base.it").id, - "state_id": cls.env.ref("base.state_it_rm").id, - "street": "Via Roma", - "city": "Roma", - "zip": "00101", - "fatturapa_fiscal_position_id": cls.env.ref( - "l10n_it_fatturapa.fatturapa_RF01" - ).id, + "fatturhello_is_used": True, + "l10n_it_codice_fiscale": "06363391001", + "vat": "06363391001", } ) - cls.cron = cls.env.ref(f"{MODULE}.ir_cron_fatturhello_update_e_invoices_status") - cls.cron.user_id.company_ids |= cls.env.company + cls.cron.user_id.company_ids |= cls.company def test_success(self): """An invoice can be uploaded.""" # Arrange - self._login(self.channel) + self._login(self.company) invoice = self.init_invoice( "out_invoice", - partner=self.res_partner_fatturapa_0, + company=self.company, + partner=self.italian_partner_a, amounts=[ 100, ], + taxes=self.default_tax, + post=True, ) # pre-condition - self.assertEqual(invoice.state, "draft") - self.assertFalse(invoice.fatturapa_attachment_out_id) + self.assertEqual(invoice.state, "posted") + self.assertFalse(invoice.l10n_it_edi_state) # Act with mock.patch(REQUEST_PATH) as mock_request: @@ -55,75 +50,88 @@ def test_success(self): self._get_response("login_success"), self._get_response("upload_success"), ] - invoice.action_open_export_send_sdi() + invoice.action_l10n_it_edi_send() # Assert - e_invoice = invoice.fatturapa_attachment_out_id - self.assertEqual(e_invoice.state, "sent_to_fatturhello") - self.assertEqual(invoice.fatturapa_state, "sent_to_fatturhello") - self.assertEqual(e_invoice.fatturhello_protocol, "201701000021074") + 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.channel) + self._login(self.company) invoice = self.init_invoice( "out_invoice", - partner=self.res_partner_fatturapa_0, + 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_open_export_send_sdi() - e_invoice = invoice.fatturapa_attachment_out_id + 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(e_invoice.state, "sent_to_fatturhello") - self.assertFalse(e_invoice.fatturhello_last_processed_status_datetime) + 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: + 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(e_invoice.state, "validated") + self.assertEqual(invoice.l10n_it_edi_state, "forwarded") self.assertEqual( - e_invoice.fatturhello_last_processed_status_datetime, + 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.channel) + self._login(self.company) invoice = self.init_invoice( "out_invoice", - partner=self.res_partner_fatturapa_0, + 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_open_export_send_sdi() - e_invoice = invoice.fatturapa_attachment_out_id + invoice.action_l10n_it_edi_send() + e_invoice = invoice.l10n_it_edi_attachment_id # pre-condition - self.assertEqual(e_invoice.state, "sent_to_fatturhello") + self.assertEqual(invoice.l10n_it_edi_state, "sent_to_fatturhello") # Act e_invoice.unlink() 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 + + + + + + + + +