Skip to content

Commit 68de173

Browse files
committed
l10n_it_edi_oss
1 parent 77e3b95 commit 68de173

File tree

11 files changed

+320
-0
lines changed

11 files changed

+320
-0
lines changed

l10n_it_edi_oss/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from . import models
2+
from . import wizard
3+
4+
OSS_EXEMPT_REASON = "N3.2"
5+
OSS_LAW_REFERENCE = "Art. 41 D.L. 331/1993"
6+
7+
8+
def _l10n_it_edi_oss_post_init_hook(env):
9+
oss_taxes = env["account.tax"].search(
10+
[
11+
("oss_country_id", "!=", False),
12+
("company_id.account_fiscal_country_id.code", "=", "IT"),
13+
"|",
14+
("l10n_it_exempt_reason", "=", False),
15+
("l10n_it_exempt_reason", "!=", OSS_EXEMPT_REASON),
16+
]
17+
)
18+
if oss_taxes:
19+
oss_taxes.write(
20+
{
21+
"l10n_it_exempt_reason": OSS_EXEMPT_REASON,
22+
"l10n_it_law_reference": OSS_LAW_REFERENCE,
23+
}
24+
)

l10n_it_edi_oss/__manifest__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2026 Lorenzo Battistini
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
4+
{
5+
"name": "Italy - E-invoicing - OSS",
6+
"version": "18.0.1.0.0",
7+
"category": "Accounting/Localizations/EDI",
8+
"development_status": "Beta",
9+
"summary": "Bridge module between l10n_eu_oss_oca and Italian electronic invoicing",
10+
"author": "Lorenzo Battistini, Odoo Community Association (OCA)",
11+
"website": "https://github.com/OCA/l10n-italy",
12+
"license": "AGPL-3",
13+
"depends": [
14+
"l10n_it_edi",
15+
"l10n_eu_oss_oca",
16+
],
17+
"installable": True,
18+
"post_init_hook": "_l10n_it_edi_oss_post_init_hook",
19+
}

l10n_it_edi_oss/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import account_move
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2026 Lorenzo Battistini
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
4+
from odoo import api, models
5+
6+
7+
class AccountMove(models.Model):
8+
_inherit = "account.move"
9+
10+
@api.model
11+
def _l10n_it_edi_grouping_function_base_lines(self, base_line, tax_data):
12+
result = super()._l10n_it_edi_grouping_function_base_lines(
13+
base_line, tax_data
14+
)
15+
if result and tax_data and tax_data["tax"].oss_country_id:
16+
result["tax_amount_field"] = 0.0
17+
return result
18+
19+
@api.model
20+
def _l10n_it_edi_grouping_function_tax_lines(self, base_line, tax_data):
21+
result = super()._l10n_it_edi_grouping_function_tax_lines(
22+
base_line, tax_data
23+
)
24+
if result and tax_data and tax_data["tax"].oss_country_id:
25+
result["tax_amount_field"] = 0.0
26+
result["is_oss"] = True
27+
return result
28+
29+
def _l10n_it_edi_add_base_lines_xml_values(
30+
self, base_lines_aggregated_values, is_downpayment
31+
):
32+
super()._l10n_it_edi_add_base_lines_xml_values(
33+
base_lines_aggregated_values, is_downpayment
34+
)
35+
for base_line, _aggregated_values in base_lines_aggregated_values:
36+
oss_taxes = base_line["tax_ids"].filtered(lambda t: t.oss_country_id)
37+
if oss_taxes:
38+
base_line["it_values"]["altri_dati_gestionali_list"].append(
39+
{
40+
"tipo_dato": "OSS",
41+
"riferimento_testo": f"{oss_taxes[0].amount:.2f}",
42+
"riferimento_numero": None,
43+
"riferimento_data": None,
44+
}
45+
)
46+
47+
def _l10n_it_edi_get_tax_lines_xml_values(
48+
self, base_lines_aggregated_values, values_per_grouping_key
49+
):
50+
tax_lines = super()._l10n_it_edi_get_tax_lines_xml_values(
51+
base_lines_aggregated_values, values_per_grouping_key
52+
)
53+
# Set imposta to 0.0 for OSS entries.
54+
# Iterate values_per_grouping_key in the same order as super() to
55+
# correlate grouping keys with the returned tax_lines by index.
56+
idx = 0
57+
for values in values_per_grouping_key.values():
58+
grouping_key = values["grouping_key"]
59+
if not grouping_key or grouping_key.get("skip"):
60+
continue
61+
if grouping_key.get("is_oss") and idx < len(tax_lines):
62+
tax_lines[idx]["imposta"] = 0.0
63+
idx += 1
64+
return tax_lines
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Lorenzo Battistini
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Modulo tecnico per l'integrazione tra `l10n_eu_oss_oca` (regime OSS - One Stop Shop)
2+
e `l10n_it_edi` (fatturazione elettronica italiana).
3+
4+
Quando viene emessa una fattura con un'imposta OSS, il modulo modifica la generazione
5+
del file XML FatturaPA per:
6+
7+
- Impostare AliquotaIVA a 0,00 (invece dell'aliquota estera effettiva)
8+
- Aggiungere il codice Natura N3.2 (non imponibile - cessioni intracomunitarie)
9+
- Aggiungere un blocco AltriDatiGestionali con TipoDato "OSS" e l'aliquota effettiva
10+
- Impostare Imposta a 0,00 nel riepilogo DatiRiepilogo
11+
- Aggiungere il RiferimentoNormativo con il riferimento all'Art. 41 D.L. 331/1993
12+
13+
Inoltre, quando vengono create nuove imposte OSS tramite il wizard di `l10n_eu_oss_oca`,
14+
il modulo imposta automaticamente il codice Natura e il riferimento normativo.
15+
16+
---
17+
18+
Technical module integrating `l10n_eu_oss_oca` (OSS - One Stop Shop regime) with
19+
`l10n_it_edi` (Italian electronic invoicing).
20+
21+
When an invoice is issued with an OSS tax, this module modifies the FatturaPA XML
22+
generation to:
23+
24+
- Set AliquotaIVA to 0.00 (instead of the actual foreign tax rate)
25+
- Add the Natura code N3.2 (non-taxable - intra-community transfers)
26+
- Add an AltriDatiGestionali block with TipoDato "OSS" and the actual tax rate
27+
- Set Imposta to 0.00 in the DatiRiepilogo summary
28+
- Add the RiferimentoNormativo with the reference to Art. 41 D.L. 331/1993
29+
30+
Additionally, when new OSS taxes are created through the `l10n_eu_oss_oca` wizard,
31+
the module automatically sets the Natura code and law reference.

l10n_it_edi_oss/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_edi_oss_export
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?xml version='1.0' encoding='UTF-8' ?>
2+
<p:FatturaElettronica
3+
xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
6+
xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd"
7+
versione="FPR12"
8+
>
9+
<FatturaElettronicaHeader>
10+
<DatiTrasmissione>
11+
<IdTrasmittente>
12+
<IdPaese>IT</IdPaese>
13+
<IdCodice>01234560157</IdCodice>
14+
</IdTrasmittente>
15+
<ProgressivoInvio>___ignore___</ProgressivoInvio>
16+
<FormatoTrasmissione>FPR12</FormatoTrasmissione>
17+
<CodiceDestinatario>XXXXXXX</CodiceDestinatario>
18+
<ContattiTrasmittente>
19+
<Telefono>0266766700</Telefono>
20+
<Email>test@test.it</Email>
21+
</ContattiTrasmittente>
22+
</DatiTrasmissione>
23+
<CedentePrestatore>
24+
<DatiAnagrafici>
25+
<IdFiscaleIVA>
26+
<IdPaese>IT</IdPaese>
27+
<IdCodice>01234560157</IdCodice>
28+
</IdFiscaleIVA>
29+
<CodiceFiscale>01234560157</CodiceFiscale>
30+
<Anagrafica>
31+
<Denominazione>company_2_data</Denominazione>
32+
</Anagrafica>
33+
<RegimeFiscale>RF01</RegimeFiscale>
34+
</DatiAnagrafici>
35+
<Sede>
36+
<Indirizzo>1234 Test Street</Indirizzo>
37+
<CAP>12345</CAP>
38+
<Comune>Prova</Comune>
39+
<Nazione>IT</Nazione>
40+
</Sede>
41+
</CedentePrestatore>
42+
<CessionarioCommittente>
43+
<DatiAnagrafici>
44+
<IdFiscaleIVA>
45+
<IdPaese>FR</IdPaese>
46+
<IdCodice>23334175221</IdCodice>
47+
</IdFiscaleIVA>
48+
<Anagrafica>
49+
<Denominazione>French Partner</Denominazione>
50+
</Anagrafica>
51+
</DatiAnagrafici>
52+
<Sede>
53+
<Indirizzo>1 Rue de la Paix</Indirizzo>
54+
<CAP>00000</CAP>
55+
<Comune>Paris</Comune>
56+
<Nazione>FR</Nazione>
57+
</Sede>
58+
</CessionarioCommittente>
59+
</FatturaElettronicaHeader>
60+
<FatturaElettronicaBody>
61+
<DatiGenerali>
62+
<DatiGeneraliDocumento>
63+
<TipoDocumento>TD01</TipoDocumento>
64+
<Divisa>EUR</Divisa>
65+
<Data>2019-01-01</Data>
66+
<Numero>___ignore___</Numero>
67+
<ImportoTotaleDocumento>120.00</ImportoTotaleDocumento>
68+
</DatiGeneraliDocumento>
69+
</DatiGenerali>
70+
<DatiBeniServizi>
71+
<DettaglioLinee>
72+
<NumeroLinea>1</NumeroLinea>
73+
<Descrizione>test line</Descrizione>
74+
<Quantita>1.00</Quantita>
75+
<PrezzoUnitario>100.00000000</PrezzoUnitario>
76+
<PrezzoTotale>100.00000000</PrezzoTotale>
77+
<AliquotaIVA>0.00</AliquotaIVA>
78+
<Natura>N3.2</Natura>
79+
<AltriDatiGestionali>
80+
<TipoDato>OSS</TipoDato>
81+
<RiferimentoTesto>20.00</RiferimentoTesto>
82+
</AltriDatiGestionali>
83+
</DettaglioLinee>
84+
<DatiRiepilogo>
85+
<AliquotaIVA>0.00</AliquotaIVA>
86+
<Natura>N3.2</Natura>
87+
<ImponibileImporto>100.00</ImponibileImporto>
88+
<Imposta>0.00</Imposta>
89+
<EsigibilitaIVA>I</EsigibilitaIVA>
90+
<RiferimentoNormativo>Art. 41 D.L. 331/1993</RiferimentoNormativo>
91+
</DatiRiepilogo>
92+
</DatiBeniServizi>
93+
<DatiPagamento>
94+
<CondizioniPagamento>TP02</CondizioniPagamento>
95+
<DettaglioPagamento>
96+
<ModalitaPagamento>MP05</ModalitaPagamento>
97+
<DataScadenzaPagamento>2019-01-01</DataScadenzaPagamento>
98+
<ImportoPagamento>120.00</ImportoPagamento>
99+
<CodicePagamento>___ignore___</CodicePagamento>
100+
</DettaglioPagamento>
101+
</DatiPagamento>
102+
</FatturaElettronicaBody>
103+
</p:FatturaElettronica>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright 2026 Lorenzo Battistini
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
4+
from odoo.tests import tagged
5+
6+
from odoo.addons.l10n_it_edi.tests.common import TestItEdi
7+
8+
9+
@tagged("post_install_l10n", "post_install", "-at_install")
10+
class TestEdiOssExport(TestItEdi):
11+
@classmethod
12+
def setUpClass(cls):
13+
super().setUpClass()
14+
cls.module = "l10n_it_edi_oss"
15+
16+
cls.french_partner = cls.env["res.partner"].create(
17+
{
18+
"name": "French Partner",
19+
"vat": "FR23334175221",
20+
"country_id": cls.env.ref("base.fr").id,
21+
"street": "1 Rue de la Paix",
22+
"city": "Paris",
23+
"company_id": False,
24+
"is_company": True,
25+
"invoice_edi_format": "it_edi_xml",
26+
}
27+
)
28+
29+
cls.oss_tax_fr = (
30+
cls.env["account.tax"]
31+
.with_company(cls.company)
32+
.create(
33+
{
34+
"name": "OSS for EU to France: 20.0",
35+
"amount": 20.0,
36+
"amount_type": "percent",
37+
"type_tax_use": "sale",
38+
"oss_country_id": cls.env.ref("base.fr").id,
39+
"l10n_it_exempt_reason": "N3.2",
40+
"l10n_it_law_reference": "Art. 41 D.L. 331/1993",
41+
"sequence": 1000,
42+
}
43+
)
44+
)
45+
46+
def test_oss_invoice_export(self):
47+
"""OSS invoice exports with AliquotaIVA=0.00, Natura=N3.2,
48+
AltriDatiGestionali with OSS data, and Imposta=0.00."""
49+
invoice = self.init_invoice(
50+
"out_invoice",
51+
amounts=[100],
52+
company=self.company,
53+
partner=self.french_partner,
54+
taxes=self.oss_tax_fr,
55+
)
56+
invoice.invoice_date_due = invoice.date
57+
invoice.action_post()
58+
self._assert_export_invoice(invoice, "oss_invoice.xml")

l10n_it_edi_oss/wizard/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import l10n_eu_oss_wizard

0 commit comments

Comments
 (0)