Initial commit: L10N_Europe packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:52 +02:00
commit 9803722600
2377 changed files with 380711 additions and 0 deletions

View file

@ -0,0 +1,50 @@
# Italy - E-invoicing
E-invoice implementation
## Installation
```bash
pip install odoo-bringout-oca-ocb-l10n_it_edi
```
## Dependencies
This addon depends on:
- l10n_it
- account_edi
- account_edi_proxy_client
## Manifest Information
- **Name**: Italy - E-invoicing
- **Version**: 0.3
- **Category**: Accounting/Localizations/EDI
- **License**: LGPL-3
- **Installable**: False
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `l10n_it_edi`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph L10n_it_edi Module - l10n_it_edi
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for l10n_it_edi. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,7 @@
# Dependencies
This addon depends on:
- [l10n_it](../../odoo-bringout-oca-ocb-l10n_it)
- [account_edi](../../odoo-bringout-oca-ocb-account_edi)
- [account_edi_proxy_client](../../odoo-bringout-oca-ocb-account_edi_proxy_client)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon l10n_it_edi or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-ocb-l10n_it_edi"
# or
uv pip install odoo-bringout-oca-ocb-l10n_it_edi"
```

View file

@ -0,0 +1,23 @@
# Models
Detected core models and extensions in l10n_it_edi.
```mermaid
classDiagram
class account_tax
class l10n_it_ddt
class res_company
class res_partner
class account_chart_template
class account_edi_format
class account_move
class account_tax
class mail_template
class res_company
class res_config_settings
class res_partner
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: l10n_it_edi. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon l10n_it_edi
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,34 @@
# Security
Access control and security definitions in l10n_it_edi.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[ir.model.access.csv](../l10n_it_edi/security/ir.model.access.csv)**
- 0 model access rules
## Record Rules
Row-level security rules defined in:
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[ir.model.access.csv](../l10n_it_edi/security/ir.model.access.csv)**
- Model access permissions (CRUD rights)
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon l10n_it_edi
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import tools
from odoo import api, SUPERUSER_ID
def _l10n_it_edi_update_export_tax(env):
chart_template = env.ref('l10n_it.l10n_it_chart_template_generic', raise_if_not_found=False)
if chart_template:
for company in env['res.company'].search([('chart_template_id', '=', chart_template.id)]):
tax = env.ref(f'l10n_it.{company.id}_00eu', raise_if_not_found=False)
if tax:
tax.write({
'l10n_it_has_exoneration': True,
'l10n_it_kind_exoneration': 'N3.2',
'l10n_it_law_reference': 'Art. 41, DL n. 331/93',
})
service_tax = env.ref(f'l10n_it.{company.id}_00eus', raise_if_not_found=False)
if service_tax:
service_tax.write({
'l10n_it_has_exoneration': True,
'l10n_it_kind_exoneration': 'N3.2',
'l10n_it_law_reference': 'Art. 7ter, DPR 633/1972',
})
def _l10n_it_edi_post_init(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
_l10n_it_edi_update_export_tax(env)

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Italy - E-invoicing',
'icon': '/l10n_it/static/description/icon.png',
'version': '0.3',
'depends': [
'l10n_it',
# Although account_edi is a dependency of account_edi_proxy_client,
# it is here because it's in the auto-install
'account_edi',
'account_edi_proxy_client',
],
'auto_install': False,
'author': 'Odoo',
'description': """
E-invoice implementation
""",
'category': 'Accounting/Localizations/EDI',
'website': 'http://www.odoo.com/',
'data': [
'security/ir.model.access.csv',
'data/account_edi_data.xml',
'data/invoice_it_template.xml',
'data/invoice_it_simplified_template.xml',
'data/ir_cron.xml',
'views/res_config_settings_views.xml',
'views/l10n_it_view.xml',
],
'demo': [
'data/account_invoice_demo.xml',
],
'post_init_hook': '_l10n_it_edi_post_init',
'license': 'LGPL-3',
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="edi_fatturaPA" model="account.edi.format">
<field name="name">Fattura PA (IT)</field>
<field name="code">fattura_pa</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- add VAT, codice fiscal and tax system for main company -->
<record id="l10n_it.demo_company_it" model="res.company">
<field name="vat">IT01654010345</field>
<field name="street">Test Street</field>
<field name="city">Prova</field>
<field name="zip">12345</field>
<field name="l10n_it_codice_fiscale">01654010345</field>
<field name="l10n_it_tax_system">RF01</field>
<field name="zip">12345</field>
</record>
<record id="l10n_it.partner_demo_company_it" model="res.partner">
<field name="l10n_it_pa_index">0803HR0</field>
</record>
<record id="partner_demo_it" model="res.partner">
<field name="name">Palazzo dell'Arte</field>
<field name="vat">IT00000010215</field>
<field name="street">Piazza Marconi 5</field>
<field name="city">Cremona</field>
<field name="country_id" ref="base.it"/>
<field name="state_id" ref="base.state_it_cr"/>
<field name="zip">26000</field>
<field name="email">info@partner.itexample.com</field>
<field name="website">www.itexample.com</field>
</record>
<record id="demo_l10n_it_edi_bank" model="res.partner.bank">
<field name="acc_type">iban</field>
<field name="acc_number">BE71096123456769</field>
<field name="bank_id" ref="base.bank_bnp"/>
<field name="partner_id" ref="l10n_it.partner_demo_company_it"/>
<field name="company_id" ref="l10n_it.demo_company_it"/>
</record>
<record id="demo_l10n_it_edi_partner_a" model="res.partner">
<field name="name">Biscotti Oslenghi</field>
<field name="company_type">company</field>
<field name="country_id" ref="base.it"/>
<field name="street">1234 Strada del Caffè</field>
<field name="city">Milano</field>
<field name="zip">20100</field>
<field name="vat">IT06289781004</field>
<field name="l10n_it_codice_fiscale">06289781004</field>
<field name="l10n_it_pa_index">N8MIMM9</field>
</record>
<record id="demo_l10n_it_edi_partner_pa" model="res.partner">
<field name="name">Agenzia Regionale Emergenza Urgenza</field>
<field name="company_type">company</field>
<field name="country_id" ref="base.it"/>
<field name="street">Via Alfredo Campanini 6</field>
<field name="city">Milano</field>
<field name="zip">20124</field>
<field name="vat">IT11513540960</field>
<field name="l10n_it_codice_fiscale">11513540960</field>
<field name="l10n_it_pa_index">SOOTJS</field>
</record>
<record id="demo_l10n_it_edi_partner_pa_fiscal_position" model="ir.property" forcecreate="0">
<field name="name">property_account_position_id</field>
<field name="fields_id" search="[('model', '=', 'res.partner'), ('name', '=', 'property_account_position_id')]"/>
<field name="res_id" model="res.partner" eval="'res.partner,' + str(obj().env.ref('l10n_it_edi.demo_l10n_it_edi_partner_pa').id)"/>
<field name="value" eval="'account.fiscal.position,'
+ str(ref('l10n_it.' + str(ref('l10n_it.demo_company_it')) + '_split_payment_fiscal_position'))"/>
<field name="company_id" ref="l10n_it.demo_company_it"/>
</record>
<record id="demo_l10n_it_edi_proxy_user" model="account_edi_proxy_client.user">
<field name="id_client">demo_id_client</field>
<field name="company_id" ref="l10n_it.demo_company_it"/>
<field name="edi_format_id" ref="l10n_it_edi.edi_fatturaPA"/>
<field name="edi_identification">01654010345</field>
<field name="private_key">1234</field>
<field name="refresh_token">demo</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="account_invoice_line_it_simplified_FatturaPA">
<DatiBeniServizi>
<Descrizione>
<t t-esc="format_alphanumeric(line.name[:1000])"/>
<t t-if="not line.name" t-esc="'NO NAME'"/>
</Descrizione>
<Importo t-esc="format_monetary(line.price_total, currency)"/>
<DatiIVA>
<Imposta t-esc="format_monetary(line.price_total - line.price_subtotal, currency)"/>
</DatiIVA>
<Natura t-if="line.tax_ids.l10n_it_has_exoneration" t-esc="line.tax_ids.l10n_it_kind_exoneration"/>
</DatiBeniServizi>
</template>
<template id="account_invoice_it_simplified_FatturaPA_export">
<t t-set="currency" t-value="record.currency_id or record.company_currency_id"/>
<t t-set="bank" t-value="record.partner_bank_id"/>
<p:FatturaElettronicaSemplificata t-att-versione="formato_trasmissione" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.0 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.0/Schema_del_file_xml_FatturaPA_versione_1.0.xsd">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese t-esc="get_vat_country(record.company_id.vat)"/>
<IdCodice t-esc="normalize_codice_fiscale(record.company_id.l10n_it_codice_fiscale) or get_vat_number(record.company_id.vat)"/>
</IdTrasmittente>
<ProgressivoInvio t-esc="format_alphanumeric(record.name.replace('/','')[-10:])"/>
<FormatoTrasmissione t-esc="formato_trasmissione"/>
<CodiceDestinatario t-if="record.commercial_partner_id.l10n_it_pa_index" t-esc="record.commercial_partner_id.l10n_it_pa_index.upper()"/>
<CodiceDestinatario t-if="not record.commercial_partner_id.l10n_it_pa_index" t-esc="'0000000'"/>
<PECDestinatario t-if="record.commercial_partner_id.l10n_it_pec_email" t-esc="record.commercial_partner_id.l10n_it_pec_email"/>
</DatiTrasmissione>
<CedentePrestatore>
<IdFiscaleIVA>
<IdPaese t-esc="get_vat_country(record.company_id.vat)"/>
<IdCodice t-esc="get_vat_number(record.company_id.vat)"/>
</IdFiscaleIVA>
<CodiceFiscale t-if="record.company_id.l10n_it_codice_fiscale" t-esc="normalize_codice_fiscale(record.company_id.l10n_it_codice_fiscale)"/>
<Denominazione t-esc="format_alphanumeric(record.company_id.partner_id.display_name[:80])"/>
<t t-call="l10n_it_edi.account_invoice_it_FatturaPA_sede">
<t t-set="partner" t-value="record.company_id.partner_id"/>
</t>
<RappresentanteFiscale t-if="record.company_id.l10n_it_has_tax_representative">
<IdFiscaleIVA>
<IdPaese t-esc="get_vat_country(record.company_id.l10n_it_tax_representative_partner_id.vat)"/>
<IdCodice t-esc="get_vat_number(record.company_id.l10n_it_tax_representative_partner_id.vat)"/>
</IdFiscaleIVA>
<Anagrafica>
<Denominazione t-if="record.commercial_partner_id.is_company" t-esc="format_alphanumeric(record.commercial_partner_id.display_name[:80])"/>
<Nome t-if="not record.commercial_partner_id.is_company" t-esc="format_alphanumeric(' '.join(record.commercial_partner_id.name.split()[:1])[:60])"/>
<Cognome t-if="not record.commercial_partner_id.is_company" t-esc="format_alphanumeric(' '.join(record.commercial_partner_id.name.split()[1:])[:60])"/>
</Anagrafica>
</RappresentanteFiscale>
<IscrizioneREA t-if="record.company_id.l10n_it_has_eco_index">
<Ufficio t-esc="record.company_id.l10n_it_eco_index_office.code"/>
<NumeroREA t-esc="format_alphanumeric(record.company_id.l10n_it_eco_index_number)"/>
<CapitaleSociale t-if="record.company_id.l10n_it_eco_index_share_capital != 0" t-esc="format_numbers_two(record.company_id.l10n_it_eco_index_share_capital)"/>
<SocioUnico t-if="record.company_id.l10n_it_eco_index_sole_shareholder != 'NO'" t-esc="record.company_id.l10n_it_eco_index_sole_shareholder"/>
<StatoLiquidazione t-esc="record.company_id.l10n_it_eco_index_liquidation_state"/>
</IscrizioneREA>
<RegimeFiscale t-esc="record.company_id.l10n_it_tax_system"/>
</CedentePrestatore>
<CessionarioCommittente>
<IdentificativiFiscali>
<IdFiscaleIVA t-if="record.commercial_partner_id.vat and in_eu(record.commercial_partner_id)">
<IdPaese t-esc="get_vat_country(record.commercial_partner_id.vat)"/>
<IdCodice t-esc="get_vat_number(record.commercial_partner_id.vat)"/>
</IdFiscaleIVA>
<CodiceFiscale t-if="record.commercial_partner_id.l10n_it_codice_fiscale" t-esc="normalize_codice_fiscale(record.commercial_partner_id.l10n_it_codice_fiscale)"/>
</IdentificativiFiscali>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<!--2.1.1-->
<TipoDocumento t-esc="document_type"/>
<Divisa t-esc="currency.name"/>
<Data t-esc="format_date(record.invoice_date)"/>
<Numero t-esc="format_alphanumeric(record.name[-20:])"/>
</DatiGeneraliDocumento>
<DatiFatturaRettificata t-if="record.move_type == 'out_refund' and record.reversed_entry_id">
<NumeroFR t-esc="format_alphanumeric(record.reversed_entry_id.name[-20:])"/>
<DataFR t-esc="format_date(record.reversed_entry_id.invoice_date)"/>
<ElementiRettificati t-esc="format_alphanumeric(record.ref[:1000])"/>
</DatiFatturaRettificata>
</DatiGenerali>
<!-- Invoice lines. -->
<t t-foreach="record.invoice_line_ids.filtered(lambda l: l.display_type not in ('line_note', 'line_section'))" t-as="line">
<t t-call="l10n_it_edi.account_invoice_line_it_simplified_FatturaPA"/>
</t>
<Allegati t-if="pdf">
<NomeAttachment t-esc="format_alphanumeric(pdf_name[:60])"/>
<FormatoAttachment t-translation="off">PDF</FormatoAttachment>
<Attachment t-esc="pdf"/>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronicaSemplificata>
</template>
</data>
</odoo>

View file

@ -0,0 +1,229 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="account_invoice_line_it_FatturaPA">
<DettaglioLinee>
<NumeroLinea t-esc="line_dict['line_number']"/>
<CodiceArticolo t-if="line.product_id.barcode">
<CodiceTipo t-translation="off">EAN</CodiceTipo>
<CodiceValore t-esc="format_alphanumeric(line.product_id.barcode)[:35]"/>
</CodiceArticolo>
<CodiceArticolo t-elif="line.product_id.default_code">
<CodiceTipo t-translation="off">INTERNAL</CodiceTipo>
<CodiceValore t-esc="format_alphanumeric(line.product_id.default_code)[:35]"/>
</CodiceArticolo>
<Descrizione t-esc="format_alphanumeric(line_dict['description'])[:1000]"/>
<Quantita t-esc="format_numbers(abs(line.quantity))"/>
<UnitaMisura t-if="line.product_uom_id and line.product_uom_id.category_id != env.ref('uom.product_uom_categ_unit')" t-esc="format_alphanumeric(line.product_uom_id.name)[:10]"/>
<PrezzoUnitario t-esc="'%.06f' % (line_dict['unit_price'])"/>
<ScontoMaggiorazione t-if="line.discount != 0">
<Tipo t-esc="discount_type(line.discount)"/>
<Percentuale t-esc="format_numbers(abs(line.discount))"/>
</ScontoMaggiorazione>
<PrezzoTotale t-esc="format_monetary(line_dict['subtotal_price'], currency)"/>
<AliquotaIVA t-if="vat_tax.amount_type == 'percent'" t-esc="format_numbers(vat_tax.amount)"/>
<AliquotaIVA t-elif="vat_tax.amount_type != 'percent'" t-esc="'0.00'"/>
<Natura t-if="vat_tax.l10n_it_has_exoneration" t-esc="vat_tax.l10n_it_kind_exoneration"/>
<AltriDatiGestionali t-if="conversion_rate">
<TipoDato t-translation="off">DIVISA</TipoDato>
<RiferimentoTesto t-esc="format_alphanumeric(record.currency_id.name)"/>
<RiferimentoNumero t-esc="'%.06f' % line.price_subtotal"/>
</AltriDatiGestionali>
<AltriDatiGestionali t-if="conversion_rate">
<TipoDato t-translation="off">CAMBIO</TipoDato>
<RiferimentoNumero t-esc="conversion_rate"/>
<RiferimentoData t-esc="format_date(record.invoice_date)"/>
</AltriDatiGestionali>
</DettaglioLinee>
</template>
<template id="account_invoice_it_FatturaPA_export">
<p:FatturaElettronica t-att-versione="formato_trasmissione" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese t-esc="get_vat_country(sender.vat)"/>
<IdCodice t-esc="normalize_codice_fiscale(sender.l10n_it_codice_fiscale) or get_vat_number(sender.vat)"/>
</IdTrasmittente>
<ProgressivoInvio t-esc="format_alphanumeric(record.name.replace('/','')[-10:])"/>
<FormatoTrasmissione t-esc="formato_trasmissione"/>
<CodiceDestinatario t-esc="codice_destinatario"/>
<ContattiTrasmittente>
<Telefono t-if="sender_partner.phone" t-esc="format_alphanumeric(format_phone(sender_partner.phone))"/>
<Telefono t-elif="sender_partner.mobile" t-esc="format_alphanumeric(format_phone(sender_partner.mobile))"/>
<Email t-if="sender_partner.email" t-esc="sender_partner.email[:256]"/>
</ContattiTrasmittente>
<PECDestinatario t-if="not is_self_invoice and partner.l10n_it_pec_email" t-esc="partner.l10n_it_pec_email[:256]"/>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese t-out="seller_info['country_code']"/>
<IdCodice t-out="seller_info['vat']"/>
</IdFiscaleIVA>
<CodiceFiscale t-if="seller.l10n_it_codice_fiscale" t-esc="normalize_codice_fiscale(seller.l10n_it_codice_fiscale)"/>
<Anagrafica>
<Denominazione t-esc="format_alphanumeric(seller_partner.display_name[:80])"/>
</Anagrafica>
<RegimeFiscale t-esc="regime_fiscale"/>
</DatiAnagrafici>
<t t-call="l10n_it_edi.account_invoice_it_FatturaPA_sede">
<t t-set="partner" t-value="seller_partner"/>
</t>
<IscrizioneREA t-if="not is_self_invoice and company.l10n_it_has_eco_index">
<Ufficio t-esc="company.l10n_it_eco_index_office.code"/>
<NumeroREA t-esc="format_alphanumeric(company.l10n_it_eco_index_number)"/>
<CapitaleSociale t-if="company.l10n_it_eco_index_share_capital != 0" t-esc="format_numbers_two(company.l10n_it_eco_index_share_capital)"/>
<SocioUnico t-if="company.l10n_it_eco_index_sole_shareholder != 'NO'" t-esc="company.l10n_it_eco_index_sole_shareholder"/>
<StatoLiquidazione t-esc="company.l10n_it_eco_index_liquidation_state"/>
</IscrizioneREA>
</CedentePrestatore>
<RappresentanteFiscale t-if="not is_self_invoice and representative">
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese t-esc="get_vat_country(representative.vat)"/>
<IdCodice t-esc="get_vat_number(representative.vat)"/>
</IdFiscaleIVA>
<CodiceFiscale t-if="representative.l10n_it_codice_fiscale" t-esc="normalize_codice_fiscale(representative.l10n_it_codice_fiscale)"/>
<Anagrafica>
<t t-if="representative.is_company">
<Denominazione t-esc="format_alphanumeric(representative.display_name[:80])"/>
</t>
<t t-else="">
<Nome t-esc="format_alphanumeric(' '.join(representative.name.split()[:1])[:60])"/>
<Cognome t-esc="format_alphanumeric(' '.join(representative.name.split()[1:])[:60])"/>
</t>
</Anagrafica>
</DatiAnagrafici>
</RappresentanteFiscale>
<CessionarioCommittente>
<DatiAnagrafici>
<IdFiscaleIVA t-if="buyer_info['vat']">
<IdPaese t-out="buyer_info['country_code']"/>
<IdCodice t-out="buyer_info['vat']"/>
</IdFiscaleIVA>
<CodiceFiscale t-if="buyer.l10n_it_codice_fiscale" t-esc="normalize_codice_fiscale(buyer.l10n_it_codice_fiscale)"/>
<Anagrafica>
<t t-if="buyer_is_company">
<Denominazione t-esc="format_alphanumeric(buyer.display_name[:80])"/>
</t>
<t t-else="">
<Nome t-esc="format_alphanumeric(' '.join(buyer.name.split()[:1])[:60])"/>
<Cognome t-esc="format_alphanumeric(' '.join(buyer.name.split()[1:])[:60])"/>
</t>
</Anagrafica>
</DatiAnagrafici>
<t t-call="l10n_it_edi.account_invoice_it_FatturaPA_sede">
<t t-set="partner" t-value="buyer_partner"/>
</t>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento t-esc="document_type"/>
<Divisa t-esc="currency.name"/>
<Data t-esc="format_date(get_move_invoice_template_date(record))"/>
<Numero t-esc="format_alphanumeric(record.name[-20:])"/>
<DatiBollo t-if="record.l10n_it_stamp_duty">
<BolloVirtuale t-translation="off">SI</BolloVirtuale>
<ImportoBollo t-esc="format_numbers(record.l10n_it_stamp_duty)"/>
</DatiBollo>
<ImportoTotaleDocumento t-esc="format_monetary(document_total, currency)"/>
</DatiGeneraliDocumento>
<DatiOrdineAcquisto t-if="origin_document_type == 'purchase_order'">
<t t-call="l10n_it_edi.account_invoice_FatturaPA_origin_document"/>
</DatiOrdineAcquisto>
<DatiOrdineAcquisto t-elif="record.ref and not record.reversed_entry_id">
<IdDocumento t-esc="format_alphanumeric(record.ref[:20])"/>
</DatiOrdineAcquisto>
<DatiContratto t-if="origin_document_type == 'contract'">
<t t-call="l10n_it_edi.account_invoice_FatturaPA_origin_document"/>
</DatiContratto>
<DatiConvenzione t-if="origin_document_type == 'agreement'">
<t t-call="l10n_it_edi.account_invoice_FatturaPA_origin_document"/>
</DatiConvenzione>
<DatiFattureCollegate t-if="record.reversed_entry_id">
<IdDocumento t-out="format_alphanumeric(record.reversed_entry_id.name[-20:])"/>
<Data t-out="format_date(get_move_invoice_template_date(record.reversed_entry_id))"/>
</DatiFattureCollegate>
<DatiFattureCollegate t-foreach="downpayment_moves" t-as="downpayment_move">
<IdDocumento t-out="format_alphanumeric(downpayment_move.name[-20:])"/>
<Data t-out="format_date(get_move_invoice_template_date(downpayment_move))"/>
</DatiFattureCollegate>
<DatiDDT t-if="record.l10n_it_ddt_id">
<NumeroDDT t-esc="format_alphanumeric(record.l10n_it_ddt_id.name[-20:])"/>
<DataDDT t-esc="format_date(record.l10n_it_ddt_id.date)"/>
</DatiDDT>
</DatiGenerali>
<DatiBeniServizi>
<t t-foreach="invoice_lines" t-as="line_dict">
<t t-set="line" t-value="line_dict['line']"/>
<t t-set="vat_tax" t-value="line_dict['vat_tax']"/>
<t t-call="l10n_it_edi.account_invoice_line_it_FatturaPA"/>
</t>
<t t-foreach="tax_lines" t-as="tax_line">
<t t-set="tax" t-value="tax_line['tax']"/>
<t t-set="has_exoneration" t-value="tax.l10n_it_has_exoneration"/>
<t t-set="kind_exoneration" t-value="tax.l10n_it_kind_exoneration"/>
<DatiRiepilogo>
<AliquotaIVA t-esc="format_numbers(tax.amount)"/>
<Natura t-if="has_exoneration" t-esc="kind_exoneration"/>
<Arrotondamento t-if="tax_line.get('rounding')" t-esc="format_numbers(-tax_line['rounding'])"/>
<t t-if="rc_refund">
<ImponibileImporto t-esc="format_monetary(balance_multiplicator * tax_line['base_amount'], currency)"/>
<Imposta t-esc="format_monetary(balance_multiplicator * tax_line['tax_amount'], currency)"/>
</t>
<t t-else="">
<ImponibileImporto t-esc="format_monetary(tax_line['base_amount'], currency)"/>
<Imposta t-esc="format_monetary(tax_line['tax_amount'], currency)"/>
</t>
<EsigibilitaIVA t-esc="tax_line['exigibility_code']"/>
<RiferimentoNormativo t-if="tax.l10n_it_law_reference" t-esc="format_alphanumeric(tax.l10n_it_law_reference[:100])"/>
</DatiRiepilogo>
</t>
</DatiBeniServizi>
<DatiPagamento t-if="partner_bank and record.move_type != 'out_refund'">
<t t-set="payments" t-value="record.line_ids.filtered(lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable'))"/>
<CondizioniPagamento t-translation="off"><t t-if="len(payments) == 1">TP02</t><t t-else="">TP01</t></CondizioniPagamento>
<t t-foreach="payments" t-as="payment">
<DettaglioPagamento>
<ModalitaPagamento t-translation="off">MP05</ModalitaPagamento>
<DataScadenzaPagamento t-esc="format_date(payment.date_maturity)"/>
<ImportoPagamento t-esc="format_monetary(abs(payment.amount_currency), currency)"/>
<IstitutoFinanziario t-if="partner_bank.bank_id" t-esc="format_alphanumeric(partner_bank.bank_id.name[:80])"/>
<IBAN t-if="partner_bank.acc_type == 'iban'" t-esc="partner_bank.sanitized_acc_number"/>
<BIC t-elif="partner_bank.acc_type == 'bank' and partner_bank.bank_id.bic" t-esc="partner_bank.bank_id.bic"/>
<CodicePagamento t-if="record.payment_reference" t-esc="format_alphanumeric(record.payment_reference[:60])"/>
</DettaglioPagamento>
</t>
</DatiPagamento>
<Allegati t-if="pdf">
<NomeAttachment t-esc="format_alphanumeric(pdf_name[:60])"/>
<FormatoAttachment t-translation="off">PDF</FormatoAttachment>
<Attachment t-esc="pdf"/>
</Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>
</template>
<template id="account_invoice_it_FatturaPA_sede">
<Sede>
<Indirizzo><t t-if="partner.street or partner.street2" t-esc="format_alphanumeric((partner.street or '') + ' ' + (partner.street2 or ''))[:60]"/></Indirizzo>
<CAP><t t-if="partner.country_id.code != 'IT'" t-esc="'00000'"/><t t-elif="partner.zip" t-esc="partner.zip"/></CAP>
<Comune t-esc="format_alphanumeric(partner.city[:60])"/>
<Provincia t-if="partner.country_id.code == 'IT' and partner.state_id" t-esc="partner.state_id.code[:2]"/>
<Nazione t-esc="partner.country_id.code"/>
</Sede>
</template>
<template id="account_invoice_FatturaPA_origin_document">
<IdDocumento t-if="origin_document_name" t-esc="format_alphanumeric(origin_document_name[:20])"/>
<Data t-if="origin_document_date" t-esc="format_date(origin_document_date)"/>
<CodiceCUP t-if="cup" t-esc="format_alphanumeric(cup)[-15:]"/>
<CodiceCIG t-if="cig" t-esc="format_alphanumeric(cig)[-15:]"/>
</template>
</data>
</odoo>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_cron_receive_fattura_pa_invoice" model="ir.cron">
<field name="name">FatturaPA: Receive invoices from the exchange system</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="model_id" ref="account_edi.model_account_edi_format"/>
<field name="code">model._cron_receive_fattura_pa()</field>
<field name="doall" eval="False"/>
<field name="state">code</field>
</record>
</odoo>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import res_partner
from . import res_company
from . import res_config_settings
from . import account_chart_template
from . import account_invoice
from . import account_edi_format
from . import ddt
from . import mail_template

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
def _load(self, company):
"""
Override normal default taxes, which are the ones with lowest sequence.
"""
result = super()._load(company)
template = company.chart_template_id
if template == self.env.ref('l10n_it.l10n_it_chart_template_generic'):
company.account_sale_tax_id = self.env.ref(f'l10n_it.{company.id}_22v')
company.account_purchase_tax_id = self.env.ref(f'l10n_it.{company.id}_22am')
tax = self.env.ref(f'l10n_it.{company.id}_00eu', raise_if_not_found=False)
if tax:
tax.write({
'l10n_it_has_exoneration': True,
'l10n_it_kind_exoneration': 'N3.2',
'l10n_it_law_reference': 'Art. 41, DL n. 331/93',
})
service_tax = self.env.ref(f'l10n_it.{company.id}_00eus', raise_if_not_found=False)
if service_tax:
service_tax.write({
'l10n_it_has_exoneration': True,
'l10n_it_kind_exoneration': 'N3.2',
'l10n_it_law_reference': 'Art. 7ter, DPR 633/1972',
})
return result

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,485 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from functools import reduce
import logging
import re
from datetime import datetime
from odoo import api, fields, models, _
from odoo.tools import float_repr, float_compare
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_it_edi_transaction = fields.Char(copy=False, string="FatturaPA Transaction")
l10n_it_edi_attachment_id = fields.Many2one('ir.attachment', copy=False, string="FatturaPA Attachment", ondelete="restrict")
l10n_it_stamp_duty = fields.Float(string="Dati Bollo", readonly=True, states={'draft': [('readonly', False)]})
l10n_it_ddt_id = fields.Many2one('l10n_it.ddt', string='DDT', readonly=True, states={'draft': [('readonly', False)]}, copy=False)
l10n_it_einvoice_name = fields.Char(compute='_compute_l10n_it_einvoice')
l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", compute='_compute_l10n_it_einvoice')
def _get_l10n_it_amount_split_payment(self):
self.ensure_one()
if not self.is_sale_document(False):
return 0.0
sign = -1 if self.move_type == "out_invoice" else 1
return sum(sign * line.balance for line in self.line_ids.filtered(lambda l: l.tax_line_id and l.tax_line_id._l10n_it_is_split_payment()))
@api.depends('edi_document_ids', 'edi_document_ids.attachment_id')
def _compute_l10n_it_einvoice(self):
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
for invoice in self:
einvoice = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == fattura_pa).sudo()
invoice.l10n_it_einvoice_id = einvoice.attachment_id
invoice.l10n_it_einvoice_name = einvoice.attachment_id.name
@api.depends('l10n_it_edi_transaction')
def _compute_show_reset_to_draft_button(self):
super(AccountMove, self)._compute_show_reset_to_draft_button()
for move in self.filtered(lambda m: m.l10n_it_edi_transaction):
move.show_reset_to_draft_button = False
def invoice_generate_xml(self):
self.ensure_one()
report_name = self.env['account.edi.format']._l10n_it_edi_generate_electronic_invoice_filename(self)
data = "<?xml version='1.0' encoding='UTF-8'?>" + str(self._l10n_it_edi_export_invoice_as_xml())
description = _('Italian invoice: %s', self.move_type)
attachment = self.env['ir.attachment'].create({
'name': report_name,
'res_id': self.id,
'res_model': self._name,
'raw': data.encode(),
'description': description,
'type': 'binary',
})
self.message_post(
body=(_("E-Invoice is generated on %s by %s") % (fields.Datetime.now(), self.env.user.display_name))
)
return {'attachment': attachment}
def _is_commercial_partner_pa(self):
"""
Returns True if the destination of the FatturaPA belongs to the Public Administration.
"""
return len(self.commercial_partner_id.l10n_it_pa_index or '') == 6
def _l10n_it_edi_prepare_fatturapa_line_details(self, reverse_charge_refund=False, is_downpayment=False, convert_to_euros=True):
""" Returns a list of dictionaries passed to the template for the invoice lines (DettaglioLinee)
"""
invoice_lines = []
lines = self.invoice_line_ids.filtered(lambda l: not l.display_type in ('line_note', 'line_section'))
for num, line in enumerate(lines):
sign = -1 if line.move_id.is_inbound() else 1
price_subtotal = (line.balance * sign) if convert_to_euros else line.price_subtotal
# The price_subtotal should be inverted when the line is a reverse charge refund.
if reverse_charge_refund:
price_subtotal = -price_subtotal
# Unit price
price_unit = 0
if line.quantity and line.discount != 100.0:
price_unit = price_subtotal / ((1 - (line.discount or 0.0) / 100.0) * abs(line.quantity))
else:
price_unit = line.price_unit
description = line.name
# Down payment lines:
# If there was a down paid amount that has been deducted from this move,
# we need to put a reference to the down payment invoice in the DatiFattureCollegate tag
downpayment_moves = self.env['account.move']
if not is_downpayment and line.price_subtotal < 0:
downpayment_moves = line._get_downpayment_lines().mapped("move_id")
if downpayment_moves:
downpayment_moves_description = ', '.join([m.name for m in downpayment_moves])
sep = ', ' if description else ''
description = f"{description}{sep}{downpayment_moves_description}"
vat_tax = line.tax_ids.flatten_taxes_hierarchy().filtered(lambda t: t._l10n_it_filter_kind('vat') and t.amount >= 0)
invoice_lines.append({
'line': line,
'line_number': num + 1,
'description': description or 'NO NAME',
'unit_price': price_unit,
'subtotal_price': price_subtotal,
'vat_tax': vat_tax,
'downpayment_moves': downpayment_moves,
})
return invoice_lines
def _l10n_it_edi_prepare_fatturapa_tax_details(self, tax_details, reverse_charge_refund=False):
""" Returns a list of dictionaries passed to the template for the invoice lines (DatiRiepilogo)
"""
tax_lines = []
for _tax_name, tax_dict in tax_details['tax_details'].items():
# The assumption is that the company currency is EUR.
tax = tax_dict['tax']
base_amount = tax_dict['base_amount']
tax_amount = tax_dict['tax_amount']
tax_rate = tax.amount
tax_exigibility_code = (
'S' if tax._l10n_it_is_split_payment()
else 'D' if tax.tax_exigibility == 'on_payment'
else 'I' if tax.tax_exigibility == 'on_invoice'
else False
)
expected_base_amount = tax_amount * 100 / tax_rate if tax_rate else False
# Constraints within the edi make local rounding on price included taxes a problem.
# To solve this there is a <Arrotondamento> or 'rounding' field, such that:
# taxable base = sum(taxable base for each unit) + Arrotondamento
if tax.price_include and tax.amount_type == 'percent':
if expected_base_amount and float_compare(base_amount, expected_base_amount, 2):
tax_dict['rounding'] = base_amount - (tax_amount * 100 / tax_rate)
tax_dict['base_amount'] = base_amount - tax_dict['rounding']
tax_line_dict = {
'tax': tax,
'rounding': tax_dict.get('rounding', False),
'base_amount': tax_dict['base_amount'],
'tax_amount': tax_dict['tax_amount'],
'exigibility_code': tax_exigibility_code,
}
tax_lines.append(tax_line_dict)
return tax_lines
def _l10n_it_edi_filter_fatturapa_tax_details(self, line, tax_values):
"""Filters tax details to only include the positive amounted lines regarding VAT taxes."""
repartition_line = tax_values['tax_repartition_line']
return (repartition_line.factor_percent >= 0 and repartition_line.tax_id.amount >= 0)
def _prepare_fatturapa_export_values(self):
self.ensure_one()
def format_date(dt):
# Format the date in the italian standard.
dt = dt or datetime.now()
return dt.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
def format_monetary(number, currency):
# Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001).
return float_repr(number, min(2, currency.decimal_places))
def format_numbers(number):
#format number to str with between 2 and 8 decimals (event if it's .00)
number_splited = str(number).split('.')
if len(number_splited) == 1:
return "%.02f" % number
cents = number_splited[1]
if len(cents) > 8:
return "%.08f" % number
return float_repr(number, max(2, len(cents)))
def format_numbers_two(number):
#format number to str with 2 (event if it's .00)
return "%.02f" % number
def discount_type(discount):
return 'SC' if discount > 0 else 'MG'
def format_phone(number):
if not number:
return False
number = number.replace(' ', '').replace('/', '').replace('.', '')
if len(number) > 4 and len(number) < 13:
return number
return False
def get_vat_number(vat):
if vat[:2].isdecimal():
return vat.replace(' ', '')
return vat[2:].replace(' ', '')
def get_vat_country(vat):
if vat[:2].isdecimal():
return 'IT'
return vat[:2].upper()
def format_alphanumeric(text_to_convert):
return text_to_convert.encode('latin-1', 'replace').decode('latin-1') if text_to_convert else False
def get_move_invoice_template_date(move):
return move.date if self.env['account.edi.format']._l10n_it_edi_is_self_invoice(move) else move.invoice_date
def get_vat_values(partner):
""" Generate the VAT and country code needed by l10n_it_edi XML export.
VAT number:
If there is a VAT number and the partner is not in EU and San Marino, then the exported value is 'OO99999999999'
If there is a VAT number and the partner is in EU or San Marino, then remove the country prefix
If there is no VAT and the partner is not in Italy, then the exported value is '0000000'
If there is no VAT and the partner is in Italy, the VAT is not set and Codice Fiscale will be relevant in the XML.
If there is no VAT and no Codice Fiscale, the invoice is not even exported, so this case is not handled.
Country:
First, take the country configured on the partner.
If there's a codice fiscale and no country, the country is 'IT'.
"""
europe = self.env.ref('base.europe', raise_if_not_found=False)
in_eu = europe and partner.country_id and partner.country_id in europe.country_ids
is_sm = partner.country_code == 'SM'
normalized_vat = partner.vat
normalized_country = partner.country_code
has_vat = partner.vat and not partner.vat in ['/', 'NA']
if has_vat:
normalized_vat = partner.vat.replace(' ', '')
if in_eu:
# If the partner is from the EU, the country-code prefix of the VAT must be taken away
if not normalized_vat[:2].isdecimal():
normalized_vat = normalized_vat[2:]
# If customer is from San Marino
elif is_sm:
normalized_vat = normalized_vat if normalized_vat[:2].isdecimal() else normalized_vat[2:]
# The Tax Agency arbitrarily decided that non-EU VAT are not interesting,
# so this default code is used instead
# Detect the country code from the partner country instead
else:
normalized_vat = 'OO99999999999'
# If it has a codice fiscale (and no country), it's an Italian partner
if not normalized_country and partner.l10n_it_codice_fiscale:
normalized_country = 'IT'
# If customer has not VAT
elif not has_vat and partner.country_id and partner.country_id.code != 'IT':
normalized_vat = '0000000'
return {
'vat': normalized_vat,
'country_code': normalized_country,
}
formato_trasmissione = "FPA12" if self._is_commercial_partner_pa() else "FPR12"
# Flags
in_eu = self.env['account.edi.format']._l10n_it_edi_partner_in_eu
is_self_invoice = self.env['account.edi.format']._l10n_it_edi_is_self_invoice(self)
document_type = self.env['account.edi.format']._l10n_it_get_document_type(self)
if self.env['account.edi.format']._l10n_it_is_simplified_document_type(document_type):
formato_trasmissione = "FSM10"
# Represent if the document is a reverse charge refund in a single variable
reverse_charge = document_type in ['TD17', 'TD18', 'TD19']
is_downpayment = document_type in ['TD02']
reverse_charge_refund = self.move_type == 'in_refund' and reverse_charge
convert_to_euros = self.currency_id.name != 'EUR'
# b64encode returns a bytestring, the template tries to turn it to string,
# but only gets the repr(pdf) --> "b'<base64_data>'"
pdf = self.env['ir.actions.report']._render_qweb_pdf("account.account_invoices", self.id)[0]
pdf = base64.b64encode(pdf).decode()
pdf_name = re.sub(r'\W+', '', self.name) + '.pdf'
tax_details = self._prepare_edi_tax_details(filter_to_apply=self._l10n_it_edi_filter_fatturapa_tax_details)
company = self.company_id
partner = self.commercial_partner_id
buyer = partner if not is_self_invoice else company
seller = company if not is_self_invoice else partner
codice_destinatario = (
(is_self_invoice and company.partner_id.l10n_it_pa_index)
# San Marino is externally integrated with the SdI.
# The country as a whole has a single fixed Destination Code (i.e. "2R4GTO8").
# https://www.agenziaentrate.gov.it/portale/documents/20143/3788702/Modifiche+ProvvedimentonSanMarino+0248717-2021.pdf/429b5571-17b9-0cce-7f62-f79cf53086d7
or (partner.country_code == 'SM' and '2R4GTO8')
or partner.l10n_it_pa_index
or (partner.country_id.code == 'IT' and '0000000')
or 'XXXXXXX')
# Self-invoices are technically -100%/+100% repartitioned
# but functionally need to be exported as 100%
document_total = self.amount_total
if is_self_invoice:
document_total += sum([abs(v['tax_amount_currency']) for k, v in tax_details['tax_details'].items()])
if reverse_charge_refund:
document_total = -abs(document_total)
split_payment_amount = self._get_l10n_it_amount_split_payment()
if split_payment_amount:
document_total += split_payment_amount
# Reference line for finding the conversion rate used in the document
conversion_line = self.invoice_line_ids.sorted(lambda l: abs(l.balance), reverse=True)[0] if self.invoice_line_ids else None
conversion_rate = float_repr(
abs(conversion_line.balance / conversion_line.amount_currency), precision_digits=5,
) if convert_to_euros and conversion_line and conversion_line.amount_currency else None
invoice_lines = self._l10n_it_edi_prepare_fatturapa_line_details(reverse_charge_refund, is_downpayment, convert_to_euros)
tax_lines = self._l10n_it_edi_prepare_fatturapa_tax_details(tax_details, reverse_charge_refund)
# Reduce downpayment views to a single recordset
downpayment_moves = [l.get('downpayment_moves', self.env['account.move']) for l in invoice_lines]
downpayment_moves = self.browse(move.id for moves in downpayment_moves for move in moves)
# Create file content.
template_values = {
'record': self,
'balance_multiplicator': -1 if self.is_inbound() else 1,
'company': company,
'sender': company,
'sender_partner': company.partner_id,
'partner': partner,
'buyer': buyer,
'buyer_partner': partner if not is_self_invoice else company.partner_id,
'buyer_is_company': is_self_invoice or partner.is_company,
'seller': seller,
'seller_partner': company.partner_id if not is_self_invoice else partner,
'origin_document_type': False, # see module l10n_it_edi_origin_document, will be merged in master
'currency': self.currency_id or self.company_currency_id if not convert_to_euros else self.env.ref('base.EUR'),
'document_total': document_total,
'representative': company.l10n_it_tax_representative_partner_id,
'codice_destinatario': codice_destinatario,
'regime_fiscale': company.l10n_it_tax_system if not is_self_invoice else 'RF18',
'is_self_invoice': is_self_invoice,
'partner_bank': self.partner_bank_id,
'format_date': format_date,
'format_monetary': format_monetary,
'format_numbers': format_numbers,
'format_numbers_two': format_numbers_two,
'format_phone': format_phone,
'format_alphanumeric': format_alphanumeric,
'discount_type': discount_type,
'formato_trasmissione': formato_trasmissione,
'document_type': document_type,
'pdf': pdf,
'pdf_name': pdf_name,
'tax_details': tax_details,
'downpayment_moves': downpayment_moves,
'abs': abs,
'normalize_codice_fiscale': partner._l10n_it_normalize_codice_fiscale,
'get_vat_number': get_vat_number,
'get_vat_country': get_vat_country,
'in_eu': in_eu,
'rc_refund': reverse_charge_refund,
'invoice_lines': invoice_lines,
'tax_lines': tax_lines,
'conversion_rate': conversion_rate,
'buyer_info': get_vat_values(buyer),
'seller_info': get_vat_values(seller),
'get_move_invoice_template_date': get_move_invoice_template_date,
}
return template_values
def _post(self, soft=True):
# OVERRIDE
posted = super()._post(soft=soft)
return posted
def _compose_info_message(self, tree, element_tags):
output_str = ""
elements = tree.xpath(element_tags)
for element in elements:
output_str += "<ul>"
for line in element.iter():
if line.text:
text = " ".join(line.text.split())
if text:
output_str += "<li>%s: %s</li>" % (line.tag, text)
output_str += "</ul>"
return output_str
def _compose_multi_info_message(self, tree, element_tags):
output_str = "<ul>"
for element_tag in element_tags:
elements = tree.xpath(element_tag)
if not elements:
continue
for element in elements:
text = " ".join(element.text.split())
if text:
output_str += "<li>%s: %s</li>" % (element.tag, text)
return output_str + "</ul>"
class AccountTax(models.Model):
_name = "account.tax"
_inherit = "account.tax"
l10n_it_vat_due_date = fields.Selection([
("I", "[I] IVA ad esigibilità immediata"),
("D", "[D] IVA ad esigibilità differita"),
("S", "[S] Scissione dei pagamenti")], default="I", string="VAT due date")
l10n_it_has_exoneration = fields.Boolean(string="Has exoneration of tax (Italy)", help="Tax has a tax exoneration.")
l10n_it_kind_exoneration = fields.Selection(selection=[
("N1", "[N1] Escluse ex art. 15"),
("N2", "[N2] Non soggette"),
("N2.1", "[N2.1] Non soggette ad IVA ai sensi degli artt. Da 7 a 7-septies del DPR 633/72"),
("N2.2", "[N2.2] Non soggette altri casi"),
("N3", "[N3] Non imponibili"),
("N3.1", "[N3.1] Non imponibili esportazioni"),
("N3.2", "[N3.2] Non imponibili cessioni intracomunitarie"),
("N3.3", "[N3.3] Non imponibili cessioni verso San Marino"),
("N3.4", "[N3.4] Non imponibili operazioni assimilate alle cessioni allesportazione"),
("N3.5", "[N3.5] Non imponibili a seguito di dichiarazioni dintento"),
("N3.6", "[N3.6] Non imponibili altre operazioni che non concorrono alla formazione del plafond"),
("N4", "[N4] Esenti"),
("N5", "[N5] Regime del margine / IVA non esposta in fattura"),
("N6", "[N6] Inversione contabile (per le operazioni in reverse charge ovvero nei casi di autofatturazione per acquisti extra UE di servizi ovvero per importazioni di beni nei soli casi previsti)"),
("N6.1", "[N6.1] Inversione contabile cessione di rottami e altri materiali di recupero"),
("N6.2", "[N6.2] Inversione contabile cessione di oro e argento puro"),
("N6.3", "[N6.3] Inversione contabile subappalto nel settore edile"),
("N6.4", "[N6.4] Inversione contabile cessione di fabbricati"),
("N6.5", "[N6.5] Inversione contabile cessione di telefoni cellulari"),
("N6.6", "[N6.6] Inversione contabile cessione di prodotti elettronici"),
("N6.7", "[N6.7] Inversione contabile prestazioni comparto edile esettori connessi"),
("N6.8", "[N6.8] Inversione contabile operazioni settore energetico"),
("N6.9", "[N6.9] Inversione contabile altri casi"),
("N7", "[N7] IVA assolta in altro stato UE (prestazione di servizi di telecomunicazioni, tele-radiodiffusione ed elettronici ex art. 7-octies, comma 1 lett. a, b, art. 74-sexies DPR 633/72)")],
string="Exoneration",
help="Exoneration type",
default="N1")
l10n_it_law_reference = fields.Char(string="Law Reference", size=100)
@api.constrains('l10n_it_has_exoneration',
'l10n_it_kind_exoneration',
'l10n_it_law_reference',
'amount',
'invoice_repartition_line_ids',
'refund_repartition_line_ids')
def _check_exoneration_with_no_tax(self):
for tax in self:
if tax.l10n_it_has_exoneration:
if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0:
raise ValidationError(_("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0."))
if tax.l10n_it_kind_exoneration == 'N6' and tax._l10n_it_is_split_payment():
raise UserError(_("Split Payment is not compatible with exoneration of kind 'N6'"))
def _l10n_it_filter_kind(self, kind):
""" This can be overridden by l10n_it_edi_withholding for different kind of taxes (withholding, pension_fund)."""
return self if kind == 'vat' else self.env['account.tax']
def _l10n_it_is_split_payment(self):
""" Split payment means that the Public Administration buyer will pay VAT
to the tax agency instead of the vendor
"""
self.ensure_one()
tax_tags = self.get_tax_tags(is_refund=False, repartition_type='base') | self.get_tax_tags(is_refund=False, repartition_type='tax')
if not tax_tags:
return False
it_tax_report_ve38_lines = self.env['account.report.line'].search([
('report_id.country_id.code', '=', 'IT'),
('code', '=', 'VE38'),
])
if not it_tax_report_ve38_lines:
return False
ve38_lines_tags = it_tax_report_ve38_lines.expression_ids._get_matching_tags()
return bool(tax_tags & ve38_lines_tags)

View file

@ -0,0 +1,18 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
class L10nItDdt(models.Model):
_name = 'l10n_it.ddt'
_description = 'Transport Document'
invoice_id = fields.One2many('account.move', 'l10n_it_ddt_id', string='Invoice Reference')
name = fields.Char(string="Numero DDT", size=20, help="Transport document number", required=True)
date = fields.Date(string="Data DDT", help="Transport document date", required=True)
def name_get(self):
res = []
for ddt in self:
res.append((ddt.id, ("%s (%s)") % (ddt.name, ddt.date)))
return res

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from odoo import models
class MailTemplate(models.Model):
_inherit = "mail.template"
def _get_edi_attachments(self, document):
"""
Will return the information about the attachment of the edi document for adding the attachment in the mail.
Can be overridden where e.g. a zip-file needs to be sent with the individual files instead of the entire zip
:param document: an edi document
:return: list with a tuple with the name and base64 content of the attachment
"""
if document.edi_format_id.code == 'fattura_pa':
return {}
return super()._get_edi_attachments(document)

View file

@ -0,0 +1,115 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
TAX_SYSTEM = [
("RF01", "[RF01] Ordinario"),
("RF02", "[RF02] Contribuenti minimi (art.1, c.96-117, L. 244/07)"),
("RF04", "[RF04] Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)"),
("RF05", "[RF05] Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)"),
("RF06", "[RF06] Commercio fiammiferi (art.74, c.1, DPR 633/72)"),
("RF07", "[RF07] Editoria (art.74, c.1, DPR 633/72)"),
("RF08", "[RF08] Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)"),
("RF09", "[RF09] Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)"),
("RF10", "[RF10] Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)"),
("RF11", "[RF11] Agenzie viaggi e turismo (art.74-ter, DPR 633/72)"),
("RF12", "[RF12] Agriturismo (art.5, c.2, L. 413/91)"),
("RF13", "[RF13] Vendite a domicilio (art.25-bis, c.6, DPR 600/73)"),
("RF14", "[RF14] Rivendita beni usati, oggetti darte, dantiquariato o da collezione (art.36, DL 41/95)"),
("RF15", "[RF15] Agenzie di vendite allasta di oggetti darte, antiquariato o da collezione (art.40-bis, DL 41/95)"),
("RF16", "[RF16] IVA per cassa P.A. (art.6, c.5, DPR 633/72)"),
("RF17", "[RF17] IVA per cassa (art. 32-bis, DL 83/2012)"),
("RF18", "[RF18] Altro"),
("RF19", "[RF19] Regime forfettario (art.1, c.54-89, L. 190/2014)"),
]
class ResCompany(models.Model):
_name = 'res.company'
_inherit = 'res.company'
l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16, related='partner_id.l10n_it_codice_fiscale',
store=True, readonly=False, help="Fiscal code of your company")
l10n_it_tax_system = fields.Selection(selection=TAX_SYSTEM, string="Tax System",
help="Please select the Tax system to which you are subjected.")
# Economic and Administrative Index
l10n_it_has_eco_index = fields.Boolean(
help="The seller/provider is a company listed on the register of companies and as\
such must also indicate the registration data on all documents (art. 2250, Italian\
Civil Code)")
l10n_it_eco_index_office = fields.Many2one('res.country.state', domain="[('country_id','=','IT')]",
string="Province of the register-of-companies office")
l10n_it_eco_index_number = fields.Char(string="Number in register of companies", size=20,
help="This field must contain the number under which the\
seller/provider is listed on the register of companies.")
l10n_it_eco_index_share_capital = fields.Float(string="Share capital actually paid up",
help="Mandatory if the seller/provider is a company with share\
capital (SpA, SApA, Srl), this field must contain the amount\
of share capital actually paid up as resulting from the last\
financial statement")
l10n_it_eco_index_sole_shareholder = fields.Selection(
[
("NO", "Not a limited liability company"),
("SU", "Socio unico"),
("SM", "Più soci")],
string="Shareholder")
l10n_it_eco_index_liquidation_state = fields.Selection(
[
("LS", "The company is in a state of liquidation"),
("LN", "The company is not in a state of liquidation")],
string="Liquidation state")
# Tax representative
l10n_it_has_tax_representative = fields.Boolean(
help="The seller/provider is a non-resident subject which\
carries out transactions in Italy with relevance for VAT\
purposes and which takes avail of a tax representative in\
Italy")
l10n_it_tax_representative_partner_id = fields.Many2one('res.partner', string='Tax representative partner')
@api.constrains('l10n_it_has_eco_index',
'l10n_it_eco_index_office',
'l10n_it_eco_index_number',
'l10n_it_eco_index_liquidation_state')
def _check_eco_admin_index(self):
for record in self:
if (record.l10n_it_has_eco_index
and (not record.l10n_it_eco_index_office
or not record.l10n_it_eco_index_number
or not record.l10n_it_eco_index_liquidation_state)):
raise ValidationError(_("All fields about the Economic and Administrative Index must be completed."))
@api.constrains('l10n_it_has_eco_index',
'l10n_it_eco_index_share_capital',
'l10n_it_eco_index_sole_shareholder')
def _check_eco_incorporated(self):
""" If the business is incorporated, both these fields must be present.
We don't know whether the business is incorporated, but in any case the fields
must be both present or not present. """
for record in self:
if (record.l10n_it_has_eco_index
and bool(record.l10n_it_eco_index_share_capital) ^ bool(record.l10n_it_eco_index_sole_shareholder)):
raise ValidationError(_("If one of Share Capital or Sole Shareholder is present, "
"then they must be both filled out."))
@api.constrains('l10n_it_has_tax_representative',
'l10n_it_tax_representative_partner_id')
def _check_tax_representative(self):
for record in self:
if not record.l10n_it_has_tax_representative:
continue
if not record.l10n_it_tax_representative_partner_id:
raise ValidationError(_("You must select a tax representative."))
if not record.l10n_it_tax_representative_partner_id.vat:
raise ValidationError(_("Your tax representative partner must have a tax number."))
if not record.l10n_it_tax_representative_partner_id.country_id:
raise ValidationError(_("Your tax representative partner must have a country."))
@api.onchange("l10n_it_has_tax_representative")
def _onchange_l10n_it_has_tax_represeentative(self):
for company in self:
if not company.l10n_it_has_tax_representative:
company.l10n_it_tax_representative_partner_id = False

View file

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
from odoo.exceptions import UserError
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
is_edi_proxy_active = fields.Boolean(compute='_compute_is_edi_proxy_active')
l10n_it_edi_proxy_current_state = fields.Char(compute='_compute_l10n_it_edi_proxy_current_state')
l10n_it_edi_sdicoop_register = fields.Boolean(compute='_compute_l10n_it_edi_sdicoop_register', inverse='_set_l10n_it_edi_sdicoop_register_demo_mode')
l10n_it_edi_sdicoop_demo_mode = fields.Selection(
[('demo', 'Demo'),
('test', 'Test (experimental)'),
('prod', 'Official')],
compute='_compute_l10n_it_edi_sdicoop_demo_mode',
inverse='_set_l10n_it_edi_sdicoop_register_demo_mode',
readonly=False)
def _create_proxy_user(self, company_id):
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
edi_identification = fattura_pa._get_proxy_identification(company_id)
self.env['account_edi_proxy_client.user']._register_proxy_user(company_id, fattura_pa, edi_identification)
@api.depends('company_id.account_edi_proxy_client_ids', 'company_id.account_edi_proxy_client_ids.active')
def _compute_l10n_it_edi_sdicoop_demo_mode(self):
for config in self:
config.l10n_it_edi_sdicoop_demo_mode = self.env['account_edi_proxy_client.user']._get_demo_state()
def _set_l10n_it_edi_sdicoop_demo_mode(self):
for config in self:
self.env['ir.config_parameter'].set_param('account_edi_proxy_client.demo', config.l10n_it_edi_sdicoop_demo_mode)
@api.depends('company_id.account_edi_proxy_client_ids', 'company_id.account_edi_proxy_client_ids.active')
def _compute_is_edi_proxy_active(self):
for config in self:
config.is_edi_proxy_active = config.company_id.account_edi_proxy_client_ids
@api.depends('company_id.account_edi_proxy_client_ids', 'company_id.account_edi_proxy_client_ids.active')
def _compute_l10n_it_edi_proxy_current_state(self):
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
for config in self:
proxy_user = config.company_id.account_edi_proxy_client_ids.search([
('company_id', '=', config.company_id.id),
('edi_format_id', '=', fattura_pa.id),
], limit=1)
config.l10n_it_edi_proxy_current_state = 'inactive' if not proxy_user else 'demo' if proxy_user.id_client[:4] == 'demo' else 'active'
@api.depends('company_id')
def _compute_l10n_it_edi_sdicoop_register(self):
"""Needed because it expects a compute"""
self.l10n_it_edi_sdicoop_register = False
def button_create_proxy_user(self):
# For now, only fattura_pa uses the proxy.
# To use it for more, we have to either make the activation of the proxy on a format basis
# or create a user per format here (but also when installing new formats)
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
edi_identification = fattura_pa._get_proxy_identification(self.company_id)
if not edi_identification:
return
self.env['account_edi_proxy_client.user']._register_proxy_user(self.company_id, fattura_pa, edi_identification)
def _set_l10n_it_edi_sdicoop_register_demo_mode(self):
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
for config in self:
proxy_user = self.env['account_edi_proxy_client.user'].search([
('company_id', '=', config.company_id.id),
('edi_format_id', '=', fattura_pa.id)
], limit=1)
real_proxy_users = self.env['account_edi_proxy_client.user'].sudo().search([
('id_client', 'not like', 'demo'),
])
# Update the config as per the selected radio button
previous_demo_state = proxy_user._get_demo_state()
self.env['ir.config_parameter'].set_param('account_edi_proxy_client.demo', config.l10n_it_edi_sdicoop_demo_mode)
# If the user is trying to change from a state in which they have a registered official or testing proxy client
# to another state, we should stop them
if real_proxy_users and previous_demo_state != config.l10n_it_edi_sdicoop_demo_mode:
raise UserError(_("The company has already registered with the service as 'Test' or 'Official', it cannot change."))
if config.l10n_it_edi_sdicoop_register:
# There should only be one user at a time, if there are no users, register one
if not proxy_user:
self._create_proxy_user(config.company_id)
return
# If there is a demo user, and we are transitioning from demo to test or production, we should
# delete all demo users and then create the new user.
elif proxy_user.id_client[:4] == 'demo' and config.l10n_it_edi_sdicoop_demo_mode != 'demo':
self.env['account_edi_proxy_client.user'].search([('id_client', '=like', 'demo%')]).sudo().unlink()
self._create_proxy_user(config.company_id)

View file

@ -0,0 +1,55 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from stdnum.it import codicefiscale, iva
from odoo import api, fields, models, _
from odoo.exceptions import UserError
import re
class ResPartner(models.Model):
_name = 'res.partner'
_inherit = 'res.partner'
l10n_it_pec_email = fields.Char(string="PEC e-mail")
l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16)
l10n_it_pa_index = fields.Char(string="Destination Code",
size=7,
help="Must contain the 6-character (or 7) code, present in the PA\
Index in the information relative to the electronic invoicing service,\
associated with the office which, within the addressee administration, deals\
with receiving (and processing) the invoice.")
_sql_constraints = [
('l10n_it_codice_fiscale',
"CHECK(l10n_it_codice_fiscale IS NULL OR l10n_it_codice_fiscale = '' OR LENGTH(l10n_it_codice_fiscale) >= 11)",
"Codice fiscale must have between 11 and 16 characters."),
('l10n_it_pa_index',
"CHECK(l10n_it_pa_index IS NULL OR l10n_it_pa_index = '' OR LENGTH(l10n_it_pa_index) >= 6)",
"Destination Code must have between 6 and 7 characters."),
]
@api.model
def _l10n_it_normalize_codice_fiscale(self, codice):
if codice and re.match(r'^IT[0-9]{11}$', codice):
return codice[2:13]
return codice
@api.onchange('vat', 'country_id')
def _l10n_it_onchange_vat(self):
if self.vat and (
self.country_code == "IT"
if self.country_code
else self.vat.startswith("IT")
):
self.l10n_it_codice_fiscale = self._l10n_it_normalize_codice_fiscale(self.vat)
else:
self.l10n_it_codice_fiscale = False
@api.constrains('l10n_it_codice_fiscale')
def validate_codice_fiscale(self):
for record in self:
if record.l10n_it_codice_fiscale and (not codicefiscale.is_valid(record.l10n_it_codice_fiscale) and not iva.is_valid(record.l10n_it_codice_fiscale)):
raise UserError(_("Invalid Codice Fiscale '%s': should be like 'MRTMTT91D08F205J' for physical person and '12345670546' or 'IT12345670546' for businesses.", record.l10n_it_codice_fiscale))

View file

@ -0,0 +1,2 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_it_ddt_manager","it_ddt manager","model_l10n_it_ddt","account.group_account_invoice",1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_it_ddt_manager it_ddt manager model_l10n_it_ddt account.group_account_invoice 1 1 1 1

View file

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_edi_export
from . import test_edi_import
from . import test_edi_reverse_charge
from . import test_res_partner

View file

@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
from unittest.mock import patch, MagicMock
from odoo import tools
from odoo.tests import tagged
from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyClientUser
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestItEdi(AccountEdiTestCommon):
@classmethod
def setUpClass(cls, chart_template_ref='l10n_it.l10n_it_chart_template_generic', edi_format_ref="l10n_it_edi.edi_fatturaPA"):
super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
# Company data ------
cls.company = cls.company_data_2['company']
cls.company.l10n_it_codice_fiscale = '01234560157'
cls.company.partner_id.l10n_it_pa_index = "0803HR0"
cls.company.vat = 'IT01234560157'
cls.test_bank = cls.env['res.partner.bank'].with_company(cls.company).create({
'partner_id': cls.company.partner_id.id,
'acc_number': 'IT1212341234123412341234123',
'bank_name': 'BIG BANK',
'bank_bic': 'BIGGBANQ',
})
cls.company.l10n_it_tax_system = "RF01"
cls.company.street = "1234 Test Street"
cls.company.zip = "12345"
cls.company.city = "Prova"
cls.company.country_id = cls.env.ref('base.it')
# Partners
cls.italian_partner_a = cls.env['res.partner'].create({
'name': 'Alessi',
'vat': 'IT00465840031',
'l10n_it_codice_fiscale': '93026890017',
'country_id': cls.env.ref('base.it').id,
'street': 'Via Privata Alessi 6',
'zip': '28887',
'city': 'Milan',
'company_id': cls.company.id,
'is_company': True,
})
cls.italian_partner_b = cls.env['res.partner'].create({
'name': 'pa partner',
'vat': 'IT06655971007',
'l10n_it_codice_fiscale': '06655971007',
'l10n_it_pa_index': '123456',
'country_id': cls.env.ref('base.it').id,
'street': 'Via Test PA',
'zip': '32121',
'city': 'PA Town',
'is_company': True
})
cls.italian_partner_no_address_codice = cls.env['res.partner'].create({
'name': 'Alessi',
'l10n_it_codice_fiscale': '00465840031',
'is_company': True,
})
cls.italian_partner_no_address_VAT = cls.env['res.partner'].create({
'name': 'Alessi',
'vat': 'IT00465840031',
'is_company': True,
})
cls.american_partner = cls.env['res.partner'].create({
'name': 'Alessi',
'vat': '00465840031',
'country_id': cls.env.ref('base.us').id,
'is_company': True,
})
# We create this because we are unable to post without a proxy user existing
cls.proxy_user = cls.env['account_edi_proxy_client.user'].create({
'id_client': 'l10n_it_edi_test',
'company_id': cls.company.id,
'edi_format_id': cls.edi_format.id,
'edi_identification': 'l10n_it_edi_test',
'private_key': 'l10n_it_edi_test',
})
cls.standard_line = {
'name': 'standard_line',
'quantity': 1,
'price_unit': 800.40,
'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])]
}
cls.edi_basis_xml = cls._get_test_file_content('IT00470550013_basis.xml')
cls.edi_simplified_basis_xml = cls._get_test_file_content('IT00470550013_simpl.xml')
@classmethod
def _get_test_file_content(cls, filename):
""" Get the content of a test file inside this module """
path = 'l10n_it_edi/tests/expected_xmls/' + filename
with tools.file_open(path, mode='rb') as test_file:
return test_file.read()
def _cleanup_etree(self, content, xpaths=None):
xpaths = {
**(xpaths or {}),
'//FatturaElettronicaBody/Allegati': 'Allegati',
'//DatiTrasmissione/ProgressivoInvio': 'ProgressivoInvio',
}
return self.with_applied_xpath(
etree.fromstring(content),
"".join([f"<xpath expr='{x}' position='replace'>{y}</xpath>" for x, y in xpaths.items()])
)
def _test_invoice_with_sample_file(self, invoice, filename, xpaths_file=None, xpaths_result=None):
invoice_xml = self.edi_format._l10n_it_edi_export_invoice_as_xml(invoice)
expected_xml = self._get_test_file_content(filename)
result = self._cleanup_etree(invoice_xml, xpaths_result)
expected = self._cleanup_etree(expected_xml, xpaths_file)
self.assertXmlTreeEqual(result, expected)

View file

@ -0,0 +1,74 @@
<p:FatturaElettronica xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" 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" versione="FPR12">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<ProgressivoInvio>___ignore___</ProgressivoInvio>
<FormatoTrasmissione>FPR12</FormatoTrasmissione>
<CodiceDestinatario>0000000</CodiceDestinatario>
<ContattiTrasmittente>
</ContattiTrasmittente>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>01234560157</CodiceFiscale>
<Anagrafica>
<Denominazione>company_2_data</Denominazione>
</Anagrafica>
<RegimeFiscale>RF01</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>1234 Test Street </Indirizzo>
<CAP>12345</CAP>
<Comune>Prova</Comune>
<Nazione>IT</Nazione>
</Sede>
</CedentePrestatore>
<CessionarioCommittente>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>00465840031</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>93026890017</CodiceFiscale>
<Anagrafica>
<Denominazione>Alessi</Denominazione>
</Anagrafica>
</DatiAnagrafici>
<Sede>
<Indirizzo>Via Privata Alessi 6 </Indirizzo>
<CAP>28887</CAP>
<Comune>Milan</Comune>
<Nazione>IT</Nazione>
</Sede>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD01</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-03-24</Data>
<Numero>___ignore___</Numero>
<ImportoTotaleDocumento></ImportoTotaleDocumento>
</DatiGeneraliDocumento>
</DatiGenerali>
<DatiBeniServizi>
</DatiBeniServizi>
<DatiPagamento>
<CondizioniPagamento>TP02</CondizioniPagamento>
<DettaglioPagamento>
<ModalitaPagamento>MP05</ModalitaPagamento>
<DataScadenzaPagamento>2022-03-24</DataScadenzaPagamento>
<ImportoPagamento></ImportoPagamento>
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
</FatturaElettronicaBody>
</p:FatturaElettronica>

View file

@ -0,0 +1,42 @@
<p:FatturaElettronicaSemplificata xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.0" xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.0 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.0/Schema_del_file_xml_FatturaPA_versione_1.0.xsd" versione="FSM10">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<ProgressivoInvio>___ignore___</ProgressivoInvio>
<FormatoTrasmissione>FSM10</FormatoTrasmissione>
<CodiceDestinatario>0000000</CodiceDestinatario>
</DatiTrasmissione>
<CedentePrestatore>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>01234560157</CodiceFiscale>
<Denominazione>company_2_data</Denominazione>
<Sede>
<Indirizzo>1234 Test Street </Indirizzo>
<CAP>12345</CAP>
<Comune>Prova</Comune>
<Nazione>IT</Nazione>
</Sede>
<RegimeFiscale>RF01</RegimeFiscale>
</CedentePrestatore>
<CessionarioCommittente>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD07</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-03-24</Data>
<Numero>___ignore___</Numero>
</DatiGeneraliDocumento>
</DatiGenerali>
<DatiBeniServizi>
</DatiBeniServizi>
</FatturaElettronicaBody>
</p:FatturaElettronicaSemplificata>

View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<p:FatturaElettronica versione="FPR12" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<ProgressivoInvio>00001</ProgressivoInvio>
<FormatoTrasmissione>FPR12</FormatoTrasmissione>
<CodiceDestinatario>ABC1234</CodiceDestinatario>
<ContattiTrasmittente/>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<Anagrafica>
<Denominazione>SOCIETA' ALPHA SRL</Denominazione>
</Anagrafica>
<RegimeFiscale>RF19</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>VIALE ROMA 543</Indirizzo>
<CAP>07100</CAP>
<Comune>SASSARI</Comune>
<Provincia>SS</Provincia>
<Nazione>IT</Nazione>
</Sede>
</CedentePrestatore>
<CessionarioCommittente>
<DatiAnagrafici>
<CodiceFiscale>01234560157</CodiceFiscale>
<Anagrafica>
<Denominazione>DITTA BETA</Denominazione>
</Anagrafica>
</DatiAnagrafici>
<Sede>
<Indirizzo>VIA TORINO 38-B</Indirizzo>
<CAP>00145</CAP>
<Comune>ROMA</Comune>
<Provincia>RM</Provincia>
<Nazione>IT</Nazione>
</Sede>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD01</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2014-12-18</Data>
<Numero>01234567890</Numero>
<Causale>LA FATTURA FA RIFERIMENTO AD UNA OPERAZIONE AAAA BBBBBBBBBBBBBBBBBB CCC DDDDDDDDDDDDDDD E FFFFFFFFFFFFFFFFFFFF GGGGGGGGGG HHHHHHH II LLLLLLLLLLLLLLLLL MMM NNNNN OO PPPPPPPPPPP QQQQ RRRR SSSSSSSSSSSSSS</Causale>
<Causale>SEGUE DESCRIZIONE CAUSALE NEL CASO IN CUI NON SIANO STATI SUFFICIENTI 200 CARATTERI AAAAAAAAAAA BBBBBBBBBBBBBBBBB</Causale>
</DatiGeneraliDocumento>
<DatiOrdineAcquisto>
<RiferimentoNumeroLinea>1</RiferimentoNumeroLinea>
<IdDocumento>66685</IdDocumento>
<NumItem>1</NumItem>
</DatiOrdineAcquisto>
<DatiContratto>
<RiferimentoNumeroLinea>1</RiferimentoNumeroLinea>
<IdDocumento>01234567890</IdDocumento>
<Data>2012-09-01</Data>
<NumItem>5</NumItem>
<CodiceCUP>01234567890abc</CodiceCUP>
<CodiceCIG>456def</CodiceCIG>
</DatiContratto>
<DatiTrasporto>
<DatiAnagraficiVettore>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>24681012141</IdCodice>
</IdFiscaleIVA>
<Anagrafica>
<Denominazione>Trasporto spa</Denominazione>
</Anagrafica>
</DatiAnagraficiVettore>
<DataOraConsegna>2012-10-22T16:46:12.000+02:00</DataOraConsegna>
</DatiTrasporto>
</DatiGenerali>
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>DESCRIZIONE DELLA FORNITURA</Descrizione>
<Quantita>5.00</Quantita>
<PrezzoUnitario>1.00</PrezzoUnitario>
<PrezzoTotale>5.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>5.00</ImponibileImporto>
<Imposta>1.10</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
<DatiPagamento>
<CondizioniPagamento>TP01</CondizioniPagamento>
<DettaglioPagamento>
<ModalitaPagamento>MP01</ModalitaPagamento>
<DataScadenzaPagamento>2015-01-30</DataScadenzaPagamento>
<ImportoPagamento>6.10</ImportoPagamento>
</DettaglioPagamento>
</DatiPagamento>
</FatturaElettronicaBody>
</p:FatturaElettronica>

View file

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="DT_v1.0.xsl"?>
<types:NotificaDecorrenzaTermini xmlns:types="http://www.fatturapa.gov.it/sdi/messaggi/v1.0" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IntermediarioConDupliceRuolo="Si" versione="1.0" xsi:schemaLocation="http://www.fatturapa.gov.it/sdi/messaggi/v1.0 MessaggiTypes_v1.0.xsd http://www.w3.org/2000/09/xmldsig# xmldsig-core-schema.xsd">
<IdentificativoSdI>111</IdentificativoSdI>
<NomeFile>IT01234567890_FPR01.xml</NomeFile>
<Descrizione>Notifica di esempio</Descrizione>
<MessageId>123456</MessageId>
<Note>Esempio</Note>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="Signature1">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference Id="reference-document" URI="">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">
<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="subtract">/descendant::ds:Signature</XPath>
</ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>g6h8KnGd+Y4DCdnGk5oIUbBwjJB3MMGlyizaFyCqH7I=</ds:DigestValue>
</ds:Reference>
<ds:Reference Id="reference-signedpropeties" Type="http://uri.etsi.org/01903#SignedProperties" URI="#SignedProperties_1">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>LkOlfB97QK/evb7mYg+KkxW3BSiZre63y3Qeh/rV28E=</ds:DigestValue>
</ds:Reference>
<ds:Reference Id="reference-keyinfo" URI="#KeyInfoId">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>BaZyFTXyxM8aIJhtiemem1lEwKR75ksXb33lsMqD89w=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue Id="SignatureValue1">
Z8/Kt/ZF/syaHxYr6/qoTz+nTJe3IV1m9Hj3WPOl1CZ/p5intUORW0IinpMum4rvPkLYpKPVbi39
WCJujEqVOVFw5xezZlwmrRghmUeyTyKazK7mKEEMXCad+FGCZj2Gz1nkqi5aNyNX/lN7m9Ix7rZ8
br3Fi3bi3nNMdyUmwog=
</ds:SignatureValue>
<ds:KeyInfo Id="KeyInfoId">
<ds:X509Data>
<ds:X509Certificate>
MIIEYDCCA0igAwIBAgIDEIgbMA0GCSqGSIb3DQEBBQUAMG0xCzAJBgNVBAYTAklUMR4wHAYDVQQK
ExVBZ2VuemlhIGRlbGxlIEVudHJhdGUxGzAZBgNVBAsTElNlcnZpemkgVGVsZW1hdGljaTEhMB8G
A1UEAxMYQ0EgQWdlbnppYSBkZWxsZSBFbnRyYXRlMB4XDTExMDcwNDEzMTkyNFoXDTE0MDcwNDEz
MTkyM1owdDELMAkGA1UEBhMCSVQxHjAcBgNVBAoTFUFnZW56aWEgZGVsbGUgRW50cmF0ZTEbMBkG
A1UECxMSU2Vydml6aSBUZWxlbWF0aWNpMSgwJgYDVQQDEx9TaXN0ZW1hIEludGVyc2NhbWJpbyBG
YXR0dXJhIFBBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMxOQj1dj6xgQBwB/S5naHvVqP
FL25Y3GnAulrcaeO8ZFFK5fWKPgiBwfyJ7qdlzn/RF7y+w92XLgh9zROmNlIjsJcp3rRwsAiKjuW
CkqwVXE0/Qtvxpo2Eovk1SV4+rf+7WKSHtabjmWXbM2FVccyN2AOvfR4WAdpr4hHkoEIiwIDAQAB
o4IBhDCCAYAwDgYDVR0PAQH/BAQDAgZAMIGZBgNVHSMEgZEwgY6AFOpEPx8Z4zc+q6qUgqWf6/wW
un+1oXGkbzBtMQswCQYDVQQGEwJJVDEeMBwGA1UEChMVQWdlbnppYSBkZWxsZSBFbnRyYXRlMRsw
GQYDVQQLExJTZXJ2aXppIFRlbGVtYXRpY2kxITAfBgNVBAMTGENBIEFnZW56aWEgZGVsbGUgRW50
cmF0ZYIDEGJwMIGyBgNVHR8EgaowgacwgaSggaGggZ6GgZtsZGFwOi8vY2Fkcy5lbnRyYXRlLmZp
bmFuemUuaXQvY24lM2RDQSUyMEFnZW56aWElMjBkZWxsZSUyMEVudHJhdGUsb3UlM2RTZXJ2aXpp
JTIwVGVsZW1hdGljaSxvJTNkQWdlbnppYSUyMGRlbGxlJTIwRW50cmF0ZSxjJTNkaXQ/Y2VydGlm
aWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHQ4EFgQUn+JY07NI6xlrCUXERiHoFFN66dkwDQYJKoZI
hvcNAQEFBQADggEBALZ0po2uLhLyZ8uiVfQUCAQd8s5o8ZJw2mcgZc/iaoNmDfcslZnTLWeuT6Gr
UFgG0uc1rY0UwWx/R1UOyc0ZesRo7Z6+kFmVubT1tbjLMuLjjUIyt4zWeNjf4PwNS0+s6Y6eC8tx
fOJmQNGQIbujWhAejoIteG01ciGeeII6AMnGK8KvbCA0UZmWl3Bou49zWajiEjtHFGkq/WNfDwRa
Fd4UWjR+UWS3rLahV7iOfh/+Yy7h1F0RzQuPJk7TCm7iHyc9QtgwxHHCmknRyNXMv6DeTOfK8ciq
uFWd6DasmblXLUm+uqhsWVRIkj2Bz63bpjuJU+8ptRfxHrVnzyCr9M4=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<ds:Object>
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#Signature1">
<xades:SignedProperties Id="SignedProperties_1">
<xades:SignedSignatureProperties>
<xades:SigningTime>2014-06-05T14:27:51Z</xades:SigningTime>
</xades:SignedSignatureProperties>
</xades:SignedProperties>
</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>
</types:NotificaDecorrenzaTermini>

View file

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="RC_v1.0.xsl"?>
<types:RicevutaConsegna xmlns:types="http://www.fatturapa.gov.it/sdi/messaggi/v1.0" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IntermediarioConDupliceRuolo="Si" versione="1.0" xsi:schemaLocation="http://www.fatturapa.gov.it/sdi/messaggi/v1.0 MessaggiTypes_v1.0.xsd ">
<IdentificativoSdI>111</IdentificativoSdI>
<NomeFile>IT01234567890_FPR01.xml</NomeFile>
<DataOraRicezione>2013-06-06T12:00:00Z</DataOraRicezione>
<DataOraConsegna>2013-06-06T12:01:00Z</DataOraConsegna>
<Destinatario>
<Codice>AAA111</Codice>
<Descrizione>Amministrazione di prova</Descrizione>
</Destinatario>
<MessageId>123456</MessageId>
<Note>Esempio</Note>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="Signature1">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference Id="reference-document" URI="">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">
<XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="subtract">/descendant::ds:Signature</XPath>
</ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>c+5ntDV6t4+PxIKEU6rbCUGu3ne9RMxoADu4yK4XIak=</ds:DigestValue>
</ds:Reference>
<ds:Reference Id="reference-signedpropeties" Type="http://uri.etsi.org/01903#SignedProperties" URI="#SignedProperties_1">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>AhiGZ+LPENybg4dQwMwjg0Nxdxzu+3M5i0w+UI6X89E=</ds:DigestValue>
</ds:Reference>
<ds:Reference Id="reference-keyinfo" URI="#KeyInfoId">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>BaZyFTXyxM8aIJhtiemem1lEwKR75ksXb33lsMqD89w=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue Id="SignatureValue1">G0FOBC+E8JKtJ5K2C+LBlvv3oarzkub7w2q5U1UQZnobWmFBbZ4WzgBNTMKUjdi2ZLkUpOSEwedf
VLgl5SyhaKYY6TizDNbxededjUpqKhyIgaWBLc/iDI6H//x+3axnLU4WwFzdr3AwqPQjPuugGX07
gOcjBHtbr7ie2Wr//o8=</ds:SignatureValue>
<ds:KeyInfo Id="KeyInfoId">
<ds:X509Data>
<ds:X509Certificate>MIIEYDCCA0igAwIBAgIDEIgbMA0GCSqGSIb3DQEBBQUAMG0xCzAJBgNVBAYTAklUMR4wHAYDVQQK
ExVBZ2VuemlhIGRlbGxlIEVudHJhdGUxGzAZBgNVBAsTElNlcnZpemkgVGVsZW1hdGljaTEhMB8G
A1UEAxMYQ0EgQWdlbnppYSBkZWxsZSBFbnRyYXRlMB4XDTExMDcwNDEzMTkyNFoXDTE0MDcwNDEz
MTkyM1owdDELMAkGA1UEBhMCSVQxHjAcBgNVBAoTFUFnZW56aWEgZGVsbGUgRW50cmF0ZTEbMBkG
A1UECxMSU2Vydml6aSBUZWxlbWF0aWNpMSgwJgYDVQQDEx9TaXN0ZW1hIEludGVyc2NhbWJpbyBG
YXR0dXJhIFBBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMxOQj1dj6xgQBwB/S5naHvVqP
FL25Y3GnAulrcaeO8ZFFK5fWKPgiBwfyJ7qdlzn/RF7y+w92XLgh9zROmNlIjsJcp3rRwsAiKjuW
CkqwVXE0/Qtvxpo2Eovk1SV4+rf+7WKSHtabjmWXbM2FVccyN2AOvfR4WAdpr4hHkoEIiwIDAQAB
o4IBhDCCAYAwDgYDVR0PAQH/BAQDAgZAMIGZBgNVHSMEgZEwgY6AFOpEPx8Z4zc+q6qUgqWf6/wW
un+1oXGkbzBtMQswCQYDVQQGEwJJVDEeMBwGA1UEChMVQWdlbnppYSBkZWxsZSBFbnRyYXRlMRsw
GQYDVQQLExJTZXJ2aXppIFRlbGVtYXRpY2kxITAfBgNVBAMTGENBIEFnZW56aWEgZGVsbGUgRW50
cmF0ZYIDEGJwMIGyBgNVHR8EgaowgacwgaSggaGggZ6GgZtsZGFwOi8vY2Fkcy5lbnRyYXRlLmZp
bmFuemUuaXQvY24lM2RDQSUyMEFnZW56aWElMjBkZWxsZSUyMEVudHJhdGUsb3UlM2RTZXJ2aXpp
JTIwVGVsZW1hdGljaSxvJTNkQWdlbnppYSUyMGRlbGxlJTIwRW50cmF0ZSxjJTNkaXQ/Y2VydGlm
aWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHQ4EFgQUn+JY07NI6xlrCUXERiHoFFN66dkwDQYJKoZI
hvcNAQEFBQADggEBALZ0po2uLhLyZ8uiVfQUCAQd8s5o8ZJw2mcgZc/iaoNmDfcslZnTLWeuT6Gr
UFgG0uc1rY0UwWx/R1UOyc0ZesRo7Z6+kFmVubT1tbjLMuLjjUIyt4zWeNjf4PwNS0+s6Y6eC8tx
fOJmQNGQIbujWhAejoIteG01ciGeeII6AMnGK8KvbCA0UZmWl3Bou49zWajiEjtHFGkq/WNfDwRa
Fd4UWjR+UWS3rLahV7iOfh/+Yy7h1F0RzQuPJk7TCm7iHyc9QtgwxHHCmknRyNXMv6DeTOfK8ciq
uFWd6DasmblXLUm+uqhsWVRIkj2Bz63bpjuJU+8ptRfxHrVnzyCr9M4=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<ds:Object>
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#Signature1">
<xades:SignedProperties Id="SignedProperties_1">
<xades:SignedSignatureProperties>
<xades:SigningTime>2014-06-05T14:24:28Z</xades:SigningTime>
</xades:SignedSignatureProperties>
</xades:SignedProperties>
</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>
</types:RicevutaConsegna>

View file

@ -0,0 +1,105 @@
<p:FatturaElettronica xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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" versione="FPR12">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<ProgressivoInvio>2022030001</ProgressivoInvio>
<FormatoTrasmissione>FPR12</FormatoTrasmissione>
<CodiceDestinatario>0803HR0</CodiceDestinatario>
<ContattiTrasmittente>
</ContattiTrasmittente>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>FR</IdPaese>
<IdCodice>15437982937</IdCodice>
</IdFiscaleIVA>
<Anagrafica>
<Denominazione>Alessi</Denominazione>
</Anagrafica>
<RegimeFiscale>RF18</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>Avenue Test rue </Indirizzo>
<CAP>00000</CAP>
<Comune>Avignon</Comune>
<Nazione>FR</Nazione>
</Sede>
</CedentePrestatore>
<CessionarioCommittente>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>01234560157</CodiceFiscale>
<Anagrafica>
<Denominazione>company_2_data</Denominazione>
</Anagrafica>
</DatiAnagrafici>
<Sede>
<Indirizzo>1234 Test Street </Indirizzo>
<CAP>12345</CAP>
<Comune>Prova</Comune>
<Nazione>IT</Nazione>
</Sede>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD18</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-04-01</Data>
<Numero>BILL/2022/04/0001</Numero>
<ImportoTotaleDocumento>1808.91</ImportoTotaleDocumento>
</DatiGeneraliDocumento>
</DatiGenerali>
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>
Product A
</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>
Product B, taxed 4%
</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>4.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>176.09</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
<DatiRiepilogo>
<AliquotaIVA>4.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>32.02</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
<DatiPagamento>
<CondizioniPagamento>TP02</CondizioniPagamento>
<DettaglioPagamento>
<ModalitaPagamento>MP05</ModalitaPagamento>
<DataScadenzaPagamento>2022-03-24</DataScadenzaPagamento>
<ImportoPagamento>1600.80</ImportoPagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati></Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>

View file

@ -0,0 +1,100 @@
<p:FatturaElettronica xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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" versione="FPR12">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<ProgressivoInvio>___ignore___</ProgressivoInvio>
<FormatoTrasmissione>FPR12</FormatoTrasmissione>
<CodiceDestinatario>XXXXXXX</CodiceDestinatario>
<ContattiTrasmittente>
</ContattiTrasmittente>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>01234560157</CodiceFiscale>
<Anagrafica>
<Denominazione>company_2_data</Denominazione>
</Anagrafica>
<RegimeFiscale>RF01</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>1234 Test Street </Indirizzo>
<CAP>12345</CAP>
<Comune>Prova</Comune>
<Nazione>IT</Nazione>
</Sede>
</CedentePrestatore>
<CessionarioCommittente>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>FR</IdPaese>
<IdCodice>15437982937</IdCodice>
</IdFiscaleIVA>
<Anagrafica>
<Denominazione>Alessi</Denominazione>
</Anagrafica>
</DatiAnagrafici>
<Sede>
<Indirizzo>Avenue Test rue </Indirizzo>
<CAP>00000</CAP>
<Comune>Avignon</Comune>
<Nazione>FR</Nazione>
</Sede>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD01</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-03-24</Data>
<Numero>___ignore___</Numero>
<ImportoTotaleDocumento>1600.80</ImportoTotaleDocumento>
</DatiGeneraliDocumento>
</DatiGenerali>
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>
Product A
</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>0.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>
Product B
</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>0.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>0.00</AliquotaIVA>
<ImponibileImporto>1600.80</ImponibileImporto>
<Imposta>0.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
<DatiPagamento>
<CondizioniPagamento>TP02</CondizioniPagamento>
<DettaglioPagamento>
<ModalitaPagamento>MP05</ModalitaPagamento>
<DataScadenzaPagamento>2022-03-24</DataScadenzaPagamento>
<ImportoPagamento>1600.80</ImportoPagamento>
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
<Allegati></Allegati>
</FatturaElettronicaBody>
</p:FatturaElettronica>

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<p:FatturaElettronica xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" 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" versione="FPA12">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<FormatoTrasmissione>FPA12</FormatoTrasmissione>
<CodiceDestinatario>123456</CodiceDestinatario>
<ContattiTrasmittente>
</ContattiTrasmittente>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>01234560157</CodiceFiscale>
<Anagrafica>
<Denominazione>company_2_data</Denominazione>
</Anagrafica>
<RegimeFiscale>RF01</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>1234 Test Street </Indirizzo>
<CAP>12345</CAP>
<Comune>Prova</Comune>
<Nazione>IT</Nazione>
</Sede>
</CedentePrestatore>
<CessionarioCommittente>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>06655971007</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>06655971007</CodiceFiscale>
<Anagrafica>
<Denominazione>pa partner</Denominazione>
</Anagrafica>
</DatiAnagrafici>
<Sede>
<Indirizzo>Via Test PA </Indirizzo>
<CAP>32121</CAP>
<Comune>PA Town</Comune>
<Nazione>IT</Nazione>
</Sede>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD01</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-03-24</Data>
<Numero>INV/2022/00001</Numero>
<ImportoTotaleDocumento>976.49</ImportoTotaleDocumento>
</DatiGeneraliDocumento>
<DatiOrdineAcquisto>
<IdDocumento>INV/2022/0001</IdDocumento>
<Data>2022-03-23</Data>
<CodiceCUP>0123456789</CodiceCUP>
<CodiceCIG>0987654321</CodiceCIG>
</DatiOrdineAcquisto>
</DatiGenerali>
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>standard_line</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>176.09</Imposta>
<EsigibilitaIVA>S</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
<DatiPagamento>
<CondizioniPagamento>TP02</CondizioniPagamento>
<DettaglioPagamento>
<ModalitaPagamento>MP05</ModalitaPagamento>
<DataScadenzaPagamento>2022-03-24</DataScadenzaPagamento>
<ImportoPagamento>800.40</ImportoPagamento>
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
</FatturaElettronicaBody>
</p:FatturaElettronica>

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<p:FatturaElettronica xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" 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" versione="FPA12">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<FormatoTrasmissione>FPA12</FormatoTrasmissione>
<CodiceDestinatario>123456</CodiceDestinatario>
<ContattiTrasmittente>
</ContattiTrasmittente>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>01234560157</CodiceFiscale>
<Anagrafica>
<Denominazione>company_2_data</Denominazione>
</Anagrafica>
<RegimeFiscale>RF01</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>1234 Test Street </Indirizzo>
<CAP>12345</CAP>
<Comune>Prova</Comune>
<Nazione>IT</Nazione>
</Sede>
</CedentePrestatore>
<CessionarioCommittente>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>06655971007</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>06655971007</CodiceFiscale>
<Anagrafica>
<Denominazione>pa partner</Denominazione>
</Anagrafica>
</DatiAnagrafici>
<Sede>
<Indirizzo>Via Test PA </Indirizzo>
<CAP>32121</CAP>
<Comune>PA Town</Comune>
<Nazione>IT</Nazione>
</Sede>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD04</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-03-25</Data>
<Numero>RINV/2022/00001</Numero>
<ImportoTotaleDocumento>976.49</ImportoTotaleDocumento>
</DatiGeneraliDocumento>
<DatiFattureCollegate>
<IdDocumento>INV/2022/00001</IdDocumento>
<Data>2022-03-24</Data>
</DatiFattureCollegate>
</DatiGenerali>
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>standard_line</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>176.09</Imposta>
<EsigibilitaIVA>S</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</FatturaElettronicaBody>
</p:FatturaElettronica>

View file

@ -0,0 +1,774 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from lxml import etree
from odoo import Command
from odoo.tests import tagged
from odoo.addons.l10n_it_edi.tests.common import TestItEdi
from odoo.exceptions import UserError
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestItEdiExport(TestItEdi):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.price_included_tax = cls.env['account.tax'].create({
'name': '22% price included tax',
'amount': 22.0,
'amount_type': 'percent',
'price_include': True,
'include_base_amount': True,
'company_id': cls.company.id,
})
cls.tax_10 = cls.env['account.tax'].create({
'name': '10% tax',
'amount': 10.0,
'amount_type': 'percent',
'company_id': cls.company.id,
})
cls.tax_zero_percent_hundred_percent_repartition = cls.env['account.tax'].create({
'name': 'all of nothing',
'amount': 0,
'amount_type': 'percent',
'company_id': cls.company.id,
'invoice_repartition_line_ids': [
(0, 0, {'factor_percent': 100, 'repartition_type': 'base'}),
(0, 0, {'factor_percent': 100, 'repartition_type': 'tax'}),
],
'refund_repartition_line_ids': [
(0, 0, {'factor_percent': 100, 'repartition_type': 'base'}),
(0, 0, {'factor_percent': 100, 'repartition_type': 'tax'}),
],
})
cls.tax_zero_percent_zero_percent_repartition = cls.env['account.tax'].create({
'name': 'none of nothing',
'amount': 0,
'amount_type': 'percent',
'company_id': cls.company.id,
'invoice_repartition_line_ids': [
(0, 0, {'factor_percent': 100, 'repartition_type': 'base'}),
(0, 0, {'factor_percent': 0, 'repartition_type': 'tax'}),
],
'refund_repartition_line_ids': [
(0, 0, {'factor_percent': 100, 'repartition_type': 'base'}),
(0, 0, {'factor_percent': 0, 'repartition_type': 'tax'}),
],
})
cls.italian_partner_b = cls.env['res.partner'].create({
'name': 'pa partner',
'vat': 'IT06655971007',
'l10n_it_codice_fiscale': '06655971007',
'l10n_it_pa_index': '123456',
'country_id': cls.env.ref('base.it').id,
'street': 'Via Test PA',
'zip': '32121',
'city': 'PA Town',
'is_company': True
})
cls.italian_partner_no_address_codice = cls.env['res.partner'].create({
'name': 'Alessi',
'l10n_it_codice_fiscale': '00465840031',
'is_company': True,
})
cls.italian_partner_no_address_VAT = cls.env['res.partner'].create({
'name': 'Alessi',
'vat': 'IT00465840031',
'is_company': True,
})
cls.american_partner = cls.env['res.partner'].create({
'name': 'Alessi',
'vat': '00465840031',
'country_id': cls.env.ref('base.us').id,
'is_company': True,
})
cls.standard_line_below_400 = {
'name': 'cheap_line',
'quantity': 1,
'price_unit': 100.00,
'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])]
}
cls.standard_line_400 = {
'name': '400_line',
'quantity': 1,
'price_unit': 327.87,
'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])]
}
cls.price_included_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'something price included',
'tax_ids': [(6, 0, [cls.price_included_tax.id])]
}),
(0, 0, {
**cls.standard_line,
'name': 'something else price included',
'tax_ids': [(6, 0, [cls.price_included_tax.id])]
}),
(0, 0, {
**cls.standard_line,
'name': 'something not price included',
}),
],
})
cls.partial_discount_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'no discount',
}),
(0, 0, {
**cls.standard_line,
'name': 'special discount',
'discount': 50,
}),
(0, 0, {
**cls.standard_line,
'name': "an offer you can't refuse",
'discount': 100,
}),
],
})
cls.full_discount_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'nothing shady just a gift for my friend',
'discount': 100,
}),
],
})
cls.non_latin_and_latin_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'ʢ◉ᴥ◉ʡ',
}),
(0, 0, {
**cls.standard_line,
'name': '-',
}),
(0, 0, {
**cls.standard_line,
'name': 'this should be the same as it was',
}),
],
})
cls.below_400_codice_simplified_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_no_address_codice.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line_below_400,
}),
(0, 0, {
**cls.standard_line_below_400,
'name': 'cheap_line_2',
'quantity': 2,
'price_unit': 10.0,
}),
],
})
cls.total_400_VAT_simplified_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_no_address_VAT.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line_400,
}),
],
})
cls.more_400_simplified_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_no_address_codice.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
}),
],
})
cls.non_domestic_simplified_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.american_partner.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line_below_400,
}),
],
})
cls.zero_tax_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'line with tax of 0% with repartition line of 100% ',
'tax_ids': [(6, 0, [cls.tax_zero_percent_hundred_percent_repartition.id])],
}),
(0, 0, {
**cls.standard_line,
'name': 'line with tax of 0% with repartition line of 0% ',
'tax_ids': [(6, 0, [cls.tax_zero_percent_zero_percent_repartition.id])],
}),
],
})
cls.negative_price_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'invoice_date_due': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
}),
(0, 0, {
**cls.standard_line,
'name': 'negative_line',
'price_unit': -100.0,
}),
(0, 0, {
**cls.standard_line,
'name': 'negative_line_different_tax',
'price_unit': -50.0,
'tax_ids': [(6, 0, [cls.tax_10.id])]
}),
],
})
cls.negative_price_credit_note = cls.negative_price_invoice.with_company(cls.company)._reverse_moves([{
'invoice_date': datetime.date(2022, 3, 24),
}])
# post the invoices
cls.price_included_invoice._post()
cls.partial_discount_invoice._post()
cls.full_discount_invoice._post()
cls.non_latin_and_latin_invoice._post()
cls.below_400_codice_simplified_invoice._post()
cls.total_400_VAT_simplified_invoice._post()
cls.zero_tax_invoice._post()
cls.negative_price_invoice._post()
cls.negative_price_credit_note._post()
def test_export_zero_amount_move(self):
"""When a move has an amount of 0, a float division by zero error is triggered."""
usd_currency = self.env.ref('base.USD')
usd_currency.active = True
zero_amount_move = self.env['account.move'].with_company(self.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'currency_id': usd_currency.id,
'invoice_line_ids': [Command.create({'amount_currency': 0})]
})
zero_amount_move._prepare_fatturapa_export_values()
self.assertEqual(len(zero_amount_move.invoice_line_ids), 1)
self.assertEqual(zero_amount_move.invoice_line_ids[0].amount_currency, 0)
def test_price_included_taxes(self):
""" When the tax is price included, there should be a rounding value added to the xml, if the sum(subtotals) * tax_rate is not
equal to taxable base * tax rate (there is a constraint in the edi where taxable base * tax rate = tax amount, but also
taxable base = sum(subtotals) + rounding amount)
"""
# In this case, the first two lines use a price_include tax the
# subtotals should be 800.40 / (100 + 22.0) * 100 = 656.065564..,
# where 22.0 is the tax rate.
#
# Since the subtotals are rounded we actually have 656.07
lines = self.price_included_invoice.line_ids
price_included_lines = lines.filtered(lambda line: line.tax_ids == self.price_included_tax)
self.assertEqual([line.price_subtotal for line in price_included_lines], [656.07, 656.07])
# So the taxable a base the edi expects (for this tax) is actually 1312.14
price_included_tax_line = lines.filtered(lambda line: line.tax_line_id == self.price_included_tax)
self.assertEqual(price_included_tax_line.tax_base_amount, 1312.14)
# The tax amount of the price included tax should be:
# per line: 800.40 - (800.40 / (100 + 22) * 100) = 144.33
# tax amount: 144.33 * 2 = 288.66
self.assertEqual(price_included_tax_line.amount_currency, -288.66)
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>something price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>656.070000</PrezzoUnitario>
<PrezzoTotale>656.07</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>something else price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>656.070000</PrezzoUnitario>
<PrezzoTotale>656.07</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>something not price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<Arrotondamento>-0.04909091</Arrotondamento>
<ImponibileImporto>1312.09</ImponibileImporto>
<Imposta>288.66</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>176.09</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
2577.29
</xpath>
<xpath expr="//DatiGeneraliDocumento//ImportoTotaleDocumento" position="inside">
2577.29
</xpath>
''')
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.price_included_invoice))
# Remove the attachment and its details
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_partially_discounted_invoice(self):
# The EDI can account for discounts, but a line with, for example, a 100% discount should still have
# a corresponding tax with a base amount of 0
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.partial_discount_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>no discount</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>special discount</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>50.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>400.20</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>an offer you can't refuse</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>100.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>0.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>1200.60</ImponibileImporto>
<Imposta>264.13</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
1464.73
</xpath>
<xpath expr="//DatiGeneraliDocumento//ImportoTotaleDocumento" position="inside">
1464.73
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_fully_discounted_inovice(self):
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.full_discount_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>nothing shady just a gift for my friend</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>100.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>0.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>0.00</ImponibileImporto>
<Imposta>0.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
0.00
</xpath>
<xpath expr="//DatiGeneraliDocumento//ImportoTotaleDocumento" position="inside">
0.00
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_non_latin_and_latin_invoice(self):
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.non_latin_and_latin_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>?????</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>?-</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>this should be the same as it was</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>2401.20</ImponibileImporto>
<Imposta>528.26</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
2929.46
</xpath>
<xpath expr="//DatiGeneraliDocumento//ImportoTotaleDocumento" position="inside">
2929.46
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_below_400_codice_simplified_invoice(self):
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.below_400_codice_simplified_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_simplified_basis_xml),
'''
<xpath expr="//FatturaElettronicaHeader//CessionarioCommittente" position="inside">
<IdentificativiFiscali>
<CodiceFiscale>00465840031</CodiceFiscale>
</IdentificativiFiscali>
</xpath>
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<Descrizione>cheap_line</Descrizione>
<Importo>122.00</Importo>
<DatiIVA>
<Imposta>22.00</Imposta>
</DatiIVA>
</DatiBeniServizi>
<DatiBeniServizi>
<Descrizione>cheap_line_2</Descrizione>
<Importo>24.40</Importo>
<DatiIVA>
<Imposta>4.40</Imposta>
</DatiIVA>
</DatiBeniServizi>
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_total_400_VAT_simplified_invoice(self):
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.total_400_VAT_simplified_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_simplified_basis_xml),
'''
<xpath expr="//FatturaElettronicaHeader//CessionarioCommittente" position="inside">
<IdentificativiFiscali>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>00465840031</IdCodice>
</IdFiscaleIVA>
</IdentificativiFiscali>
</xpath>
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<Descrizione>400_line</Descrizione>
<Importo>400.00</Importo>
<DatiIVA>
<Imposta>72.13</Imposta>
</DatiIVA>
</DatiBeniServizi>
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_more_400_simplified_invoice(self):
with self.assertRaises(UserError):
self.more_400_simplified_invoice._post()
def test_non_domestic_simplified_invoice(self):
with self.assertRaises(UserError):
self.non_domestic_simplified_invoice._post()
def test_zero_percent_taxes(self):
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.zero_tax_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>line with tax of 0% with repartition line of 100%</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>0.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>line with tax of 0% with repartition line of 0%</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>0.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>0.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>0.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
<DatiRiepilogo>
<AliquotaIVA>0.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>0.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
1600.80
</xpath>
<xpath expr="//DatiGeneraliDocumento//ImportoTotaleDocumento" position="inside">
1600.80
</xpath>
'''
)
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_negative_price_invoice(self):
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.negative_price_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>standard_line</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>negative_line</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>-100.000000</PrezzoUnitario>
<PrezzoTotale>-100.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>negative_line_different_tax</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>-50.000000</PrezzoUnitario>
<PrezzoTotale>-50.00</PrezzoTotale>
<AliquotaIVA>10.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>700.40</ImponibileImporto>
<Imposta>154.09</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
<DatiRiepilogo>
<AliquotaIVA>10.00</AliquotaIVA>
<ImponibileImporto>-50.00</ImponibileImporto>
<Imposta>-5.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
799.49
</xpath>
<xpath expr="//DatiGeneraliDocumento//ImportoTotaleDocumento" position="inside">
799.49
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_negative_price_credit_note(self):
invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.negative_price_credit_note))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
f'''
<xpath expr="//DatiGeneraliDocumento/TipoDocumento" position="replace">
<TipoDocumento>TD04</TipoDocumento>
</xpath>
<xpath expr="//DatiGeneraliDocumento//ImportoTotaleDocumento" position="inside">
799.49
</xpath>
<xpath expr="//DatiGeneraliDocumento" position="after">
<DatiFattureCollegate>
<IdDocumento>{self.negative_price_invoice.name}</IdDocumento>
<Data>{self.negative_price_credit_note.invoice_date}</Data>
</DatiFattureCollegate>
</xpath>
<xpath expr="//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>standard_line</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>negative_line</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>-100.000000</PrezzoUnitario>
<PrezzoTotale>-100.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>negative_line_different_tax</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>-50.000000</PrezzoUnitario>
<PrezzoTotale>-50.00</PrezzoTotale>
<AliquotaIVA>10.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>700.40</ImponibileImporto>
<Imposta>154.09</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
<DatiRiepilogo>
<AliquotaIVA>10.00</AliquotaIVA>
<ImponibileImporto>-50.00</ImponibileImporto>
<Imposta>-5.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DatiPagamento" position="replace"/>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)

View file

@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import logging
from freezegun import freeze_time
from lxml import etree
from unittest.mock import MagicMock, patch
from odoo import Command, fields, sql_db
from odoo.tests import tagged
from odoo.addons.l10n_it_edi.tests.common import TestItEdi
from odoo.addons.l10n_it_edi.tools.remove_signature import remove_signature
_logger = logging.getLogger(__name__)
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestItEdiImport(TestItEdi):
""" Main test class for the l10n_it_edi vendor bills XML import"""
fake_test_content = """<?xml version="1.0" encoding="UTF-8"?>
<p:FatturaElettronica versione="FPR12" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<ProgressivoInvio>TWICE_TEST</ProgressivoInvio>
</DatiTrasmissione>
<CessionarioCommittente>
<DatiAnagrafici>
<CodiceFiscale>01234560157</CodiceFiscale>
</DatiAnagrafici>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD02</TipoDocumento>
</DatiGeneraliDocumento>
</DatiGenerali>
</FatturaElettronicaBody>
</p:FatturaElettronica>"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Build test data.
# invoice_filename1 is used for vendor bill receipts tests
# invoice_filename2 is used for vendor bill tests
cls.invoice_filename1 = 'IT01234567890_FPR01.xml'
cls.invoice_filename2 = 'IT01234567890_FPR02.xml'
cls.signed_invoice_filename = 'IT01234567890_FPR01.xml.p7m'
cls.wrongly_signed_invoice_filename = 'IT09633951000_NpFwF.xml.p7m'
cls.invoice_content = cls._get_test_file_content(cls.invoice_filename1)
cls.signed_invoice_content = cls._get_test_file_content(cls.signed_invoice_filename)
cls.wrongly_signed_invoice_content = cls._get_test_file_content(cls.wrongly_signed_invoice_filename)
cls.invoice = cls.env['account.move'].create({
'move_type': 'in_invoice',
'ref': '01234567890'
})
cls.attachment = cls.env['ir.attachment'].create({
'name': cls.invoice_filename1,
'raw': cls.invoice_content,
'res_id': cls.invoice.id,
'res_model': 'account.move',
})
cls.edi_document = cls.env['account.edi.document'].create({
'edi_format_id': cls.edi_format.id,
'move_id': cls.invoice.id,
'attachment_id': cls.attachment.id,
'state': 'sent'
})
cls.test_invoice_xmls = {k: cls._get_test_file_content(v) for k, v in [
('normal_1', 'IT01234567890_FPR01.xml'),
('signed', 'IT01234567890_FPR01.xml.p7m'),
]}
def mock_commit(self):
pass
# -----------------------------
#
# Vendor bills
#
# -----------------------------
def test_receive_vendor_bill(self):
""" Test a sample e-invoice file from https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2/IT01234567890_FPR01.xml """
content = etree.fromstring(self.invoice_content)
invoices = self.edi_format._create_invoice_from_xml_tree(self.invoice_filename2, content)
self.assertTrue(bool(invoices))
def test_receive_signed_vendor_bill(self):
""" Test a signed (P7M) sample e-invoice file from https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2/IT01234567890_FPR01.xml """
with freeze_time('2020-04-06'):
content = etree.fromstring(remove_signature(self.signed_invoice_content))
invoices = self.edi_format._create_invoice_from_xml_tree(self.signed_invoice_filename, content)
self.assertRecordValues(invoices, [{
'company_id': self.company.id,
'name': 'BILL/2014/12/0001',
'invoice_date': datetime.date(2014, 12, 18),
'ref': '01234567890',
}])
def test_receive_wrongly_signed_vendor_bill(self):
"""
Some of the invoices (i.e. those from Servizio Elettrico Nazionale, the
ex-monopoly-of-energy company) have custom signatures that rely on an old
OpenSSL implementation that breaks the current one that sees them as malformed,
so we cannot read those files. Also, we couldn't find an alternative way to use
OpenSSL to just get the same result without getting the error.
A new fallback method has been added that reads the ASN1 file structure and
takes the encoded pkcs7-data tag content out of it, regardless of the
signature.
Being a non-optimized pure Python implementation, it takes about 2x the time
than the regular method, so it's better used as a fallback. We didn't use an
existing library not to further pollute the dependencies space.
task-3502910
"""
with freeze_time('2019-01-01'):
filename, content = (
self.wrongly_signed_invoice_filename,
self.wrongly_signed_invoice_content,
)
tree = self.edi_format._decode_p7m_to_xml(filename, content)
invoices = self.edi_format._create_invoice_from_xml_tree(filename, tree)
self.assertRecordValues(invoices, [{
'name': 'BILL/2023/09/0001',
'ref': '333333333333333',
'invoice_date': fields.Date.from_string('2023-09-08'),
'amount_untaxed': 57.54,
'amount_tax': 3.95,
}])
def test_cron_receives_bill_from_another_company(self):
""" Ensure that when from one of your company, you bill the other, the
import isn't impeded because of conflicts with the filename """
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
content = self.fake_test_content.encode()
# Our test content is not encrypted
proxy_user = MagicMock()
proxy_user.company_id = self.company
proxy_user._decrypt_data.return_value = content
other_company = self.company_data['company']
filename = 'IT01234567890_FPR02.xml'
invoice = self.env['account.move'].with_company(other_company).create({
'move_type': 'out_invoice',
'invoice_line_ids': [
Command.create({
'name': "something not price included",
'price_unit': 800.40,
'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)],
}),
],
})
self.env['ir.attachment'].with_company(other_company).create({
'name': filename,
'datas': content,
'res_model': 'account.move',
'res_id': invoice.id,
})
with patch.object(sql_db.Cursor, "commit", self.mock_commit):
fattura_pa._save_incoming_attachment_fattura_pa(
proxy_user=proxy_user,
id_transaction='9999999999',
filename=filename,
content=content,
key=None)
attachment = self.env['ir.attachment'].search([
('name', '=', 'IT01234567890_FPR02.xml'),
('res_model', '=', 'account.move'),
('company_id', '=', self.company.id),
])
self.assertTrue(attachment)
self.assertTrue(self.env['account.move'].browse(attachment.res_id))
def test_receive_same_vendor_bill_twice(self):
""" Test that the second time we are receiving an SdiCoop invoice, the second is discarded """
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
content = self.fake_test_content.encode()
# Our test content is not encrypted
proxy_user = MagicMock()
proxy_user.company_id = self.company
proxy_user._decrypt_data.return_value = content
with patch.object(sql_db.Cursor, "commit", self.mock_commit):
for dummy in range(2):
fattura_pa._save_incoming_attachment_fattura_pa(
proxy_user=proxy_user,
id_transaction='9999999999',
filename=self.invoice_filename2,
content=content,
key=None)
# There should be one attachement with this filename
attachments = self.env['ir.attachment'].search([('name', '=', self.invoice_filename2)])
self.assertEqual(len(attachments), 1)
invoices = self.env['account.move'].search([('payment_reference', '=', 'TWICE_TEST')])
self.assertEqual(len(invoices), 1)
def test_receive_bill_with_global_discount(self):
content = self.with_applied_xpath(
etree.fromstring(self.invoice_content),
'''
<xpath expr="//FatturaElettronicaBody/DatiGenerali/DatiGeneraliDocumento" position="inside">
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Importo>2</Importo>
</ScontoMaggiorazione>
</xpath>
''')
invoices = self.edi_format._create_invoice_from_xml_tree(self.invoice_filename2, content)
self.assertRecordValues(invoices, [{
'amount_untaxed': 3.0,
'amount_tax': 1.1,
}])
self.assertRecordValues(invoices.invoice_line_ids, [
{
'quantity': 5.0,
'name': 'DESCRIZIONE DELLA FORNITURA',
'price_unit': 1.0,
},
{
'quantity': 1.0,
'name': 'SCONTO',
'price_unit': -2,
}
])

View file

@ -0,0 +1,292 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import namedtuple
from lxml import etree
from odoo import fields
from odoo.tests import tagged
from odoo.addons.l10n_it_edi.tests.common import TestItEdi
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestItEdiReverseCharge(TestItEdi):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Helper functions -----------
def get_tag_ids(tag_codes):
""" Helper function to define tag ids for taxes """
return cls.env['account.account.tag'].search([
('applicability', '=', 'taxes'),
('country_id.code', '=', 'IT'),
('name', 'in', tag_codes)]).ids
RepartitionLine = namedtuple('Line', 'factor_percent repartition_type tag_ids')
def repartition_lines(*lines):
""" Helper function to define repartition lines in taxes """
return [(5, 0, 0)] + [(0, 0, {**line._asdict(), 'tag_ids': get_tag_ids(line[2])}) for line in lines]
ProductLine = namedtuple('Line', 'data name product_id')
def product_lines(*lines):
""" Helper function to define move lines based on a product """
return [(0, 0, {**line[0], 'name': line[1], 'product_id': line[2]}) for line in lines]
# Company -----------
cls.company.partner_id.l10n_it_pa_index = "0803HR0"
# Partner -----------
cls.french_partner = cls.env['res.partner'].create({
'name': 'Alessi',
'vat': 'FR15437982937',
'country_id': cls.env.ref('base.fr').id,
'street': 'Avenue Test rue',
'zip': '84000',
'city': 'Avignon',
'is_company': True
})
cls.san_marino_partner = cls.env['res.partner'].create({
'name': 'Prospectra',
'vat': 'SM6784',
'country_id': cls.env.ref('base.sm').id,
'street': 'Via Ventotto Luglio 212 Centro Uffici',
'zip': '47893',
'city': 'San Marino',
'company_id': cls.company.id,
'is_company': True,
})
# Taxes -----------
tax_data = {
'name': 'Tax 4% (Goods) Reverse Charge',
'amount': 4.0,
'amount_type': 'percent',
'type_tax_use': 'purchase',
'invoice_repartition_line_ids': repartition_lines(
RepartitionLine(100, 'base', ('+03', '+vj9')),
RepartitionLine(100, 'tax', ('+5v',)),
RepartitionLine(-100, 'tax', ('-4v',))),
'refund_repartition_line_ids': repartition_lines(
RepartitionLine(100, 'base', ('-03', '-vj9')),
RepartitionLine(100, 'tax', False),
RepartitionLine(-100, 'tax', False)),
}
# Purchase tax 4% with Reverse Charge
cls.purchase_tax_4p = cls.env['account.tax'].with_company(cls.company).create(tax_data)
cls.line_tax_4p = cls.standard_line.copy()
cls.line_tax_4p['tax_ids'] = [(6, 0, cls.purchase_tax_4p.ids)]
# Purchase tax 4% with Reverse Charge, targeting the tax grid for import of goods
# already in Italy in a VAT deposit
tax_data_4p_already_in_italy = {
**tax_data,
'name': 'Tax 4% purchase Reverse Charge, in Italy',
'invoice_repartition_line_ids': repartition_lines(
RepartitionLine(100, 'base', ('+03', '+vj3')),
RepartitionLine(100, 'tax', ('+5v',)),
RepartitionLine(-100, 'tax', ('-4v',))),
'refund_repartition_line_ids': repartition_lines(
RepartitionLine(100, 'base', ('-03', '-vj3')),
RepartitionLine(100, 'tax', False),
RepartitionLine(-100, 'tax', False)),
}
cls.purchase_tax_4p_already_in_italy = cls.env['account.tax'].with_company(cls.company).create(tax_data_4p_already_in_italy)
cls.line_tax_4p_already_in_italy = cls.standard_line.copy()
cls.line_tax_4p_already_in_italy['tax_ids'] = [(6, 0, cls.purchase_tax_4p_already_in_italy.ids)]
# Purchase tax 22% with Reverse Charge
tax_data_22p = {**tax_data, 'name': 'Tax 22% purchase Reverse Charge', 'amount': 22.0}
cls.purchase_tax_22p = cls.env['account.tax'].with_company(cls.company).create(tax_data_22p)
cls.line_tax_22p = cls.standard_line.copy()
cls.line_tax_22p['tax_ids'] = [(6, 0, cls.purchase_tax_22p.ids)]
# Export tax 0%
tax_data_0v = {**tax_data, "type_tax_use": "sale", "amount": 0}
cls.sale_tax_0v = cls.env['account.tax'].with_company(cls.company).create(tax_data_0v)
cls.line_tax_sale = cls.standard_line.copy()
cls.line_tax_sale['tax_ids'] = [(6, 0, cls.sale_tax_0v.ids)]
# Products -----------
# Product A with 0% sale export and tax 4% reverse carge purchase tax
product_a = cls.env['product.product'].with_company(cls.company).create({
'name': 'product_a',
'lst_price': 1000.0,
'standard_price': 800.0,
'type': 'consu',
'taxes_id': [(6, 0, cls.sale_tax_0v.ids)],
'supplier_taxes_id': [(6, 0, cls.purchase_tax_4p.ids)],
})
# Product B with 0% sale export and tax 4% reverse charge purchase tax
product_b = cls.env['product.product'].with_company(cls.company).create({
'name': 'product_b',
'lst_price': 1000.0,
'standard_price': 800.0,
'type': 'consu',
'taxes_id': [(6, 0, cls.sale_tax_0v.ids)],
'supplier_taxes_id': [(6, 0, cls.purchase_tax_4p.ids)],
})
# Moves -----------
# Export invoice
cls.reverse_charge_invoice = cls.env['account.move'].with_company(cls.company).create({
'company_id': cls.company.id,
'move_type': 'out_invoice',
'invoice_date': fields.Date.from_string('2022-03-24'),
'invoice_date_due': fields.Date.from_string('2022-03-24'),
'partner_id': cls.french_partner.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': product_lines(
ProductLine(cls.line_tax_sale, 'Product A', product_a.id),
ProductLine(cls.line_tax_sale, 'Product B', product_b.id)
),
})
# Import bill #1
bill_data = {
'company_id': cls.company.id,
'move_type': 'in_invoice',
'invoice_date': fields.Date.from_string('2022-03-24'),
'invoice_date_due': fields.Date.from_string('2022-03-24'),
'date': fields.Date.from_string('2022-04-01'),
'partner_id': cls.french_partner.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': product_lines(
ProductLine(cls.line_tax_22p, 'Product A', product_a.id),
ProductLine(cls.line_tax_4p, 'Product B, taxed 4%', product_b.id)
)
}
cls.reverse_charge_bill = cls.env['account.move'].with_company(cls.company).create(bill_data)
# Import bill #2
bill_data_2 = {
**bill_data,
'invoice_line_ids': product_lines(
ProductLine(cls.line_tax_22p, 'Product A', product_a.id),
ProductLine(cls.line_tax_4p_already_in_italy, 'Product B, taxed 4% Already in Italy', product_b.id),
),
}
cls.reverse_charge_bill_2 = cls.env['account.move'].with_company(cls.company).create(bill_data_2)
cls.reverse_charge_refund = cls.reverse_charge_bill.with_company(cls.company)._reverse_moves([{
'invoice_date': fields.Date.from_string('2022-03-24'),
'date': fields.Date.from_string('2022-04-01'),
}])
# Import bill San Marino
bill_data_san_marino = {
'company_id': cls.company.id,
'move_type': 'in_invoice',
'invoice_date': fields.Date.from_string('2022-03-24'),
'invoice_date_due': fields.Date.from_string('2022-03-24'),
'date': fields.Date.from_string('2022-04-01'),
'partner_id': cls.san_marino_partner.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': product_lines(
ProductLine(cls.line_tax_22p, 'Product A', product_a.id),
ProductLine(cls.line_tax_4p, 'Product B, taxed 4%', product_b.id)
)
}
cls.reverse_charge_bill_san_marino = cls.env['account.move'].with_company(cls.company).create(bill_data_san_marino)
# Posting moves -----------
cls.reverse_charge_invoice._post()
cls.reverse_charge_bill._post()
cls.reverse_charge_bill_2._post()
cls.reverse_charge_bill_san_marino._post()
cls.reverse_charge_refund._post()
def test_reverse_charge_invoice(self):
self._test_invoice_with_sample_file(self.reverse_charge_invoice, "reverse_charge_invoice.xml")
def test_reverse_charge_bill(self):
self._test_invoice_with_sample_file(self.reverse_charge_bill, "reverse_charge_bill.xml")
def test_reverse_charge_bill_2(self):
self._test_invoice_with_sample_file(
self.reverse_charge_bill_2,
"reverse_charge_bill.xml",
xpaths_result={
"//DatiGeneraliDocumento/Numero": "<Numero/>",
"(//DettaglioLinee/Descrizione)[2]": "<Descrizione/>",
},
xpaths_file={
"//DatiGeneraliDocumento/TipoDocumento": "<TipoDocumento>TD19</TipoDocumento>",
"//DatiGeneraliDocumento/Numero": "<Numero/>",
"(//DettaglioLinee/Descrizione)[2]": "<Descrizione/>",
}
)
def test_reverse_charge_bill_san_marino(self):
self._test_invoice_with_sample_file(
self.reverse_charge_bill_san_marino,
"reverse_charge_bill.xml",
xpaths_result={
"//DatiGeneraliDocumento/Numero": "<Numero/>",
"(//DettaglioLinee/Descrizione)[2]": "<Descrizione/>",
},
xpaths_file={
"//CedentePrestatore": """
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>SM</IdPaese>
<IdCodice>6784</IdCodice>
</IdFiscaleIVA>
<Anagrafica>
<Denominazione>Prospectra</Denominazione>
</Anagrafica>
<RegimeFiscale>RF18</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>Via Ventotto Luglio 212 Centro Uffici</Indirizzo>
<CAP>00000</CAP>
<Comune>San Marino</Comune>
<Nazione>SM</Nazione>
</Sede>
</CedentePrestatore>
""",
"//DatiGeneraliDocumento/TipoDocumento": "<TipoDocumento>TD28</TipoDocumento>",
"//DatiGeneraliDocumento/Numero": "<Numero/>",
"(//DettaglioLinee/Descrizione)[2]": "<Descrizione/>",
}
)
def test_reverse_charge_refund(self):
self._test_invoice_with_sample_file(
self.reverse_charge_refund,
"reverse_charge_bill.xml",
xpaths_result={
"//DatiGeneraliDocumento/Numero": "<Numero/>",
"//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento": "<DataScadenzaPagamento/>",
},
xpaths_file={
"//DatiGenerali": f"""
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD18</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-04-01</Data>
<Numero/>
<ImportoTotaleDocumento>-1808.91</ImportoTotaleDocumento>
</DatiGeneraliDocumento>
<DatiFattureCollegate>
<IdDocumento>{self.reverse_charge_bill.name}</IdDocumento>
<Data>{self.reverse_charge_refund.date}</Data>
</DatiFattureCollegate>
</DatiGenerali>
""",
"//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento": "<DataScadenzaPagamento/>",
"(//DettaglioLinee/PrezzoUnitario)[1]": "<PrezzoUnitario>-800.400000</PrezzoUnitario>",
"(//DettaglioLinee/PrezzoUnitario)[2]": "<PrezzoUnitario>-800.400000</PrezzoUnitario>",
"(//DettaglioLinee/PrezzoTotale)[1]": "<PrezzoTotale>-800.40</PrezzoTotale>",
"(//DettaglioLinee/PrezzoTotale)[2]": "<PrezzoTotale>-800.40</PrezzoTotale>",
"(//DatiRiepilogo/ImponibileImporto)[1]": "<ImponibileImporto>-800.40</ImponibileImporto>",
"(//DatiRiepilogo/ImponibileImporto)[2]": "<ImponibileImporto>-800.40</ImponibileImporto>",
"(//DatiRiepilogo/Imposta)[1]": "<Imposta>-176.09</Imposta>",
"(//DatiRiepilogo/Imposta)[2]": "<Imposta>-32.02</Imposta>",
}
)

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError
from odoo.tests import Form
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestResPartner(TransactionCase):
def test_validate_fiscal_code(self):
valid_codes = [
"AORTHV05P30V295L",
"SPDTHB43S93F42VH",
"MDRTUV99H14X2MNU",
"XPTDRX73R64YPLUD",
"LOLXDR40T3MZRTSV",
"GJTIUG55DLQZRTSS",
"CDEOTG5PBLQZRTSE",
"PERTLELPALQZRTSN",
"IT12345678887",
"IT12345670546",
"IT95286931217",
"IT95867361206",
"IT94567689990",
"12345670546",
"95286931217",
"95867361206",
"94567689990",
]
invalid_codes = [
"AORTHV05P34V295U",
"SPDTHB43O93F42VH",
"MDRTUVV9H14X2MNU",
"XPTDRX73RS4YPLUD",
"LOLXDRQ0T3QZRTSJ",
"GJTIUGR5DLQZRTSS",
"CDEOTG5PBLQZRTSS",
"PERTLEZPALQZRTSN",
"IT12345678901",
"IT12345678885",
"IT45689349992",
"IT78239131204",
"IT45692151219",
"12345678901",
"12345678885",
"45689349992",
"78239131204",
"45692151219",
]
partners = self.env['res.partner']
for i, code in enumerate(invalid_codes):
with self.assertRaises(UserError):
partners += self.env['res.partner'].create({'name': f'partner_{i}', 'l10n_it_codice_fiscale': code})
for i, code in enumerate(valid_codes):
partners += self.env['res.partner'].create({'name': f'partner_{i}', 'l10n_it_codice_fiscale': code})
self.assertEqual(len(partners), len(valid_codes))
def test_partner_l10n_it_codice_fiscale(self):
vat_partner = self.env['res.partner'].create({
'name': 'Customer with VAT',
})
partner_form = Form(vat_partner)
partner_form.vat = 'IT12345676017'
self.assertEqual(partner_form.l10n_it_codice_fiscale, '12345676017', "We give the Parnter a VAT, l10n_it_codice_fiscale is given accordingly")
partner_form.country_id = self.env.ref('base.ir')
self.assertFalse(partner_form.l10n_it_codice_fiscale, "Partner is given Iran as country, l10n_it_codice_fiscale is removed")
partner_form.country_id = self.env.ref('base.it')
self.assertEqual(partner_form.l10n_it_codice_fiscale, '12345676017', "The partner was given the wrong country, we correct it to Italy")
partner_form.vat = 'IT12345670017'
self.assertEqual(partner_form.l10n_it_codice_fiscale, '12345670017', "There was a typo in the VAT, changing it should change l10n_it_codice_fiscale as well")

View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import remove_signature

View file

@ -0,0 +1,381 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
Italian E-invoice signed files content extraction.
There are two methods: OpenSSL and Fallback.
Sometimes OpenSSL fail in reading signed invoices for some error in the signature itself.
The Fallback method only has minimal code to extract the invoices' content without verifying the signature itself.
It's only to be used as a no-requirements fallback for OpenSSL.
"""
import logging
import struct
import warnings
from contextlib import suppress
_logger = logging.getLogger(__name__)
def remove_signature(content, target=None):
""" Takes a bytestring supposedly PKCS7 signed and returns its PKCS7-data only """
for removal_strategy in (remove_signature_openssl, remove_signature_fallback):
if target:
target.remove_signature_method = removal_strategy.__name__
with suppress(Exception):
return removal_strategy(content)
# --------------------------------------------------------------------------------
# UTILS
# --------------------------------------------------------------------------------
def byte_to_bit_array(val):
""" Convert a byte to an array of zeros and ones """
return [((val & (1 << pos)) and 1) or 0 for pos in range(7, -1, -1)]
def bit_array_to_byte(val):
""" Convert an array of zeros and ones to byte """
value = 0
max_idx = len(val) - 1
for i in range(max_idx, -1, -1):
value += val[i] << max_idx - i
return value
# --------------------------------------------------------------------------------
# OPENSSL
# --------------------------------------------------------------------------------
try:
from OpenSSL import crypto as ssl_crypto
import OpenSSL._util as ssl_util
except ImportError:
ssl_crypto = None
_logger.warning("Cannot import library 'OpenSSL' for PKCS#7 envelope extraction.")
def remove_signature_openssl(content):
""" Remove the PKCS#7 envelope from given content, making a '.xml.p7m' file content readable as it was '.xml'.
As OpenSSL may not be installed, in that case a warning is issued and None is returned. """
# Prevent using the library if it had import errors
if not ssl_crypto:
_logger.warning("Error reading the content, check if the OpenSSL library is installed for for PKCS#7 envelope extraction.")
return None
# Load some tools from the library
null = ssl_util.ffi.NULL
verify = ssl_util.lib.PKCS7_verify
# By default ignore the validity of the certificates, just validate the structure
flags = ssl_util.lib.PKCS7_NOVERIFY | ssl_util.lib.PKCS7_NOSIGS
# Read the signed data fron the content
out_buffer = ssl_crypto._new_mem_buf()
# This method is deprecated, but there are actually no alternatives
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
loaded_data = ssl_crypto.load_pkcs7_data(ssl_crypto.FILETYPE_ASN1, content)
# Verify the signature
if verify(loaded_data._pkcs7, null, null, null, out_buffer, flags) != 1:
ssl_crypto._raise_current_error()
# Get the content as a byte-string
return ssl_crypto._bio_to_string(out_buffer)
# --------------------------------------------------------------------------------
# FALLBACK REMOVE SIGNATURE (ASN1 parse)
# --------------------------------------------------------------------------------
def remove_signature_fallback(content):
""" The invoice content is inside an ASN1 node identified by PKCS7_DATA_OID (pkcs7-data).
The node is defined as an OctectString, which can be composed of an arbitrary
sequence of octects of string data.
We visit in-order the ASN1 tree nodes until we find the pkcs7-data, then we look for content.
Once we found it, we read all OctectString that get yielded by the in-order visit..
When there are no more OctectStrings, then another object will follow
with its header and identifier, so we stop exploring and just return the content.
See also:
https://datatracker.ietf.org/doc/html/rfc2315
https://www.oss.com/asn1/resources/asn1-made-simple/asn1-quick-reference/octetstring.html
"""
PKCS7_DATA_OID = '1.2.840.113549.1.7.1'
result, header_found, data_found = None, False, False
for node in Reader().build_from_stream(content):
if node.kind == 'ObjectIdentifier' and node.content == PKCS7_DATA_OID:
header_found = True
if header_found and node.kind == 'OctetString':
data_found = True
result = (result or b'') + node.content
elif data_found:
break
if not header_found:
raise Exception("ASN1 Header not found")
if not data_found:
raise Exception("ASN1 Content not found")
return result
# --------------------------------------------------------------------------------
# ASN1 DATA
# --------------------------------------------------------------------------------
universal_tags = {
0: 'Zero',
1: 'Boolean',
2: 'Integer',
3: 'BitString',
4: 'OctetString',
5: 'Null',
6: 'ObjectIdentifier',
7: 'ObjectDescriptor',
8: 'External',
9: 'Real',
10: 'Enumerated',
11: 'EmbeddedPDV',
12: 'UTF8String',
13: 'RelativeOid',
16: 'Sequence',
17: 'Set',
18: 'NumericString',
19: 'PrintableString',
20: 'TeletexString',
21: 'VideotexString',
22: 'IA5String',
23: 'UTCTime',
24: 'GeneralizedTime',
25: 'GraphicString',
26: 'VisibleString',
27: 'GeneralString',
28: 'UniversalString',
29: 'CharacterString',
30: 'BMPString',
}
# --------------------------------------------------------------------------------
# NODES (ASN1 parse)
# --------------------------------------------------------------------------------
class Asn1Node:
""" Base class for Asn1 nodes """
_content = None
def __init__(self, kind, start_offset, node_len, cls, parent=None):
""" Initialization of the Asn1 node """
if not (parent is None or issubclass(Asn1Node, parent.__class__)):
raise TypeError("parent must be an Asn1Node or None")
# Register to parent
self.parent = parent
if parent:
parent.children.append(self)
self.kind = kind
self.start_offset = start_offset
self.children = []
self.cls = cls
self.finalized = False
self.name = self.__class__.__name__.replace('Node', '')
self.length = node_len
def finalize(self, end_offset, content=None):
""" Closes the initialization of the Asn1 node, giving it content and finished length """
self.content = content
self.length = end_offset - self.start_offset
self.end_offset = end_offset
self.finalized = True
def total_length(self):
""" Get the total length of the node if defined. The definition and length bytes must be considered. """
return self.length + 2 if self.length != "?" else "?"
@property
def content(self):
return self._content
@content.setter
def content(self, content):
if content is not None and not isinstance(content, bytes):
raise TypeError("content must be bytes or None")
self._content = content
class PrimitiveNode(Asn1Node):
""" Primitive Asn1 nodes contain pure data """
pass
class OctetStringNode(PrimitiveNode):
""" Octet String Asn1 node """
pass
class ObjectIdentifierNode(PrimitiveNode):
""" Asn1 Object Identifier, i.e. 1.3.6.1.5.5.7.48.1 """
@Asn1Node.content.setter
def content(self, content):
# Run through the content's bytes
calc = 0
result = ''
for idx, octet in enumerate(content):
# The first position is treated differently
if idx == 0:
result += "%s.%s" % (octet // 40, octet % 40)
continue
# Other positions value the less significant 7 bits,
# but the most significant bit is only negative when there's a break
calc = (calc << 7) + (octet % 0x80)
break_it = not bool(octet // 0x80)
if break_it:
result += ".%s" % calc
calc = 0
self._content = result
# --------------------------------------------------------------------------------
# READER (ASN1 parse)
# --------------------------------------------------------------------------------
class Reader:
def __init__(self, *args, **kwargs):
self.clear()
def clear(self):
self.offset = 0
self.root = None
self.current_node = None
self.parent_node = None
self.open_nodes_stack = []
self.last_open_node = None
def finalize_last_open_node(self):
""" Whenever a node is complete, it is finalized, and the references are updated """
self.last_open_node = self.open_nodes_stack.pop()
self.last_open_node.finalize(self.offset, None)
self.parent_node = self.last_open_node.parent
self.current_node = None
finalized_node = self.last_open_node
self.last_open_node = self.open_nodes_stack[-1] if self.open_nodes_stack else None
return finalized_node
def build_from_stream(self, stream):
""" Build an Asn1 tree starting from a byte string from a p7m file """
self.clear()
while self.offset < len(stream):
start_offset = self.offset
self.last_open_node = self.open_nodes_stack[-1] if self.open_nodes_stack else None
# Read the definition and length bytes
definition_byte, self.offset = self.consume('B', stream, self.offset)
node_len, _bytes_read, self.offset = self.read_length(stream, self.offset)
if definition_byte == 0 and node_len == 0 and self.open_nodes_stack:
yield self.finalize_last_open_node()
continue
# Create the current Node
self.current_node = self.create_node(definition_byte, node_len, start_offset, parent=self.parent_node)
if not self.root:
self.root = self.current_node
# If not primitive, add to the stack
if not issubclass(self.current_node.__class__, PrimitiveNode):
self.open_nodes_stack.append(self.current_node)
self.last_open_node = self.current_node
self.parent_node = self.current_node
else:
data, self.offset = self.consume('%ss' % self.current_node.length, stream, self.offset)
self.current_node.finalize(self.offset, data)
yield self.current_node
# Clear the stack of all finished nodes
while (
self.last_open_node
and not self.last_open_node.finalized
and self.last_open_node.length != '?'
and self.last_open_node.start_offset + self.last_open_node.total_length() <= self.offset
):
yield self.finalize_last_open_node()
return self.root
def consume(self, _format, stream, offset):
""" Read from a bytes stream to get data out """
size = struct.calcsize(_format)
value = struct.unpack_from(_format, stream, offset)[0]
offset += size
return value, offset
def read_length(self, stream, offset):
""" Returns: (length of the node, bytes read, updated offset) """
# Read the first byte: if it is zero, it's a special entry.
# Probably it's the second byte of a closing tag of a node (\x00 \x00 <--)
first_byte, offset = self.consume('B', stream, offset)
if first_byte == 0:
return 0, 1, offset
# Convert byte to bits
bits = byte_to_bit_array(first_byte)
# If the first bit of the first length byte is on
if not bits[0]:
return first_byte, 1, offset
# If it's the only bit being set, the length is indefinite,
# and the node will terminate with a double \x00
if not any(bits[1:]):
return '?', 1, offset
# We turn off the first bit, and the rest is the number of bytes we have to read
bytes_read = bit_array_to_byte([0] + bits[1:])
# Each byte we read is less significant, so we increase the significance of the
# value we already read and increment by the current byte
node_len = 0
for dummy in range(1, bytes_read + 1):
current_byte, offset = self.consume('B', stream, offset)
node_len = (node_len << 8) + current_byte
return node_len, bytes_read, offset
def create_node(self, definition_byte, node_len, start_offset, parent=None):
""" Method to create new Asn1 nodes, given the definition bytes and the offset """
target_class = Asn1Node
kind = "Indefinite" if node_len == "?" else "Container"
node_classes = {
(0, 0): 'Universal',
(0, 1): 'Application',
(1, 0): 'Context-specific',
(1, 1): 'Private'
}
bits = byte_to_bit_array(definition_byte)
cls_bits = tuple(bits[0:2])
cls = node_classes[cls_bits]
if cls == 'Universal':
is_primitive = not bool(bits[2])
if is_primitive:
tag = definition_byte % (1 << 5)
kind = universal_tags.get(tag)
if kind:
subclasses = PrimitiveNode.__subclasses__()
target_classes = {x.__name__: x for x in subclasses}
target_class = target_classes.get("%sNode" % kind, PrimitiveNode)
return target_class(kind, start_offset, node_len, cls, parent)

View file

@ -0,0 +1,207 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_tax_form_l10n_it" model="ir.ui.view">
<field name="name">account.tax.form.l10n.it</field>
<field name="model">account.tax</field>
<field name="priority">20</field>
<field name="inherit_id" ref="account.view_tax_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//page[@name='advanced_options']" position="inside">
<group attrs="{'invisible': [('country_code', '!=', 'IT')]}">
<group>
<field name="l10n_it_vat_due_date" invisible="1"/>
<field name="l10n_it_has_exoneration"/>
<field name="l10n_it_kind_exoneration" attrs="{'invisible': [('l10n_it_has_exoneration', '=', False)]}"/>
<field name="l10n_it_law_reference"/>
</group>
</group>
</xpath>
</data>
</field>
</record>
<record id="res_partner_form_l10n_it" model="ir.ui.view">
<field name="name">res.partner.form.l10n.it</field>
<field name="model">res.partner</field>
<field name="priority">20</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='category_id']" position="after">
<field name="l10n_it_pec_email" attrs="{'invisible': [('parent_id', '!=', False)]}"/>
<field name="l10n_it_codice_fiscale" attrs="{'invisible': [('parent_id', '!=', False)]}"/>
<field name="l10n_it_pa_index" attrs="{'invisible': [('parent_id', '!=', False)]}"/>
</xpath>
</data>
</field>
</record>
<record id="res_company_form_l10n_it" model="ir.ui.view">
<field name="name">res.company.form.l10n.it</field>
<field name="model">res.company</field>
<field name="priority">20</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='vat']" position="after">
<field name="l10n_it_codice_fiscale" attrs="{'invisible': [('country_code', '!=', 'IT')]}"/>
<field name="l10n_it_tax_system" attrs="{'invisible': [('country_code', '!=', 'IT')]}"/>
</xpath>
<xpath expr="//page" position="after">
<page string="Electronic Invoicing" name="electronic_invoicing" attrs="{'invisible': [('country_code', '!=', 'IT')]}">
<group>
<separator string="Economic and Administrative Index" colspan="4"/>
<div colspan="4">
The seller/provider is a company listed on the register of companies and as
such must also indicate the registration data on all documents (art. 2250, Italian
Civil Code)
</div>
<group>
<field name="l10n_it_has_eco_index" string="Company listed on the register of companies"/>
<field name="l10n_it_eco_index_office" attrs="{'invisible': [('l10n_it_has_eco_index', '=', False)]}"/>
<field name="l10n_it_eco_index_number" attrs="{'invisible': [('l10n_it_has_eco_index', '=', False)]}"/>
<field name="l10n_it_eco_index_share_capital" attrs="{'invisible': [('l10n_it_has_eco_index', '=', False)]}"/>
<field name="l10n_it_eco_index_sole_shareholder" attrs="{'invisible': [('l10n_it_has_eco_index', '=', False)]}"/>
<field name="l10n_it_eco_index_liquidation_state" attrs="{'invisible': [('l10n_it_has_eco_index', '=', False)]}"/>
</group>
</group>
<group>
<separator string="Tax representative" colspan="4"/>
<div colspan="4">
The seller/provider is a non-resident subject which carries out transactions in Italy
with relevance for VAT purposes and which takes avail of a tax representative in Italy
</div>
<group>
<field name="l10n_it_has_tax_representative" string="Company have a tax representative"/>
<field name="l10n_it_tax_representative_partner_id" attrs="{'invisible': [('l10n_it_has_tax_representative', '=', False)]}"/>
</group>
</group>
</page>
</xpath>
</data>
</field>
</record>
<record id="view_invoice_tree_inherit" model="ir.ui.view">
<field name="name">account.move.tree.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_invoice_tree" />
<field name="arch" type="xml">
<field name="state" position="before">
<field name="l10n_it_edi_transaction" optional="hide"/>
<field name="l10n_it_edi_attachment_id" optional="hide"/>
</field>
</field>
</record>
<record id="view_account_invoice_filter" model="ir.ui.view">
<field name="name">account.invoice.select.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_edi.view_account_invoice_filter"/>
<field name="arch" type="xml">
<xpath expr="//search/field[@name='journal_id']" position="after">
<field name="l10n_it_edi_transaction" groups="base.group_no_one"/>
<field name="l10n_it_edi_attachment_id" groups="base.group_no_one"/>
</xpath>
</field>
</record>
<record id="view_in_bill_tree_inherit" model="ir.ui.view">
<field name="name">account.move.tree.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_in_invoice_bill_tree" />
<field name="arch" type="xml">
<field name="state" position="before">
</field>
</field>
</record>
<record id="view_out_invoice_tree_inherit" model="ir.ui.view">
<field name="name">account.move.tree.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_edi.view_out_invoice_tree_inherit" />
<field name="arch" type="xml">
<field name="state" position="before">
<field name="l10n_it_edi_transaction" optional="hide" invisible="1"/>
<field name="l10n_it_edi_attachment_id" optional="hide" invisible="1"/>
</field>
</field>
</record>
<record id="view_account_invoice_filter" model="ir.ui.view">
<field name="name">account.invoice.select.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_edi.view_account_invoice_filter"/>
<field name="arch" type="xml">
<xpath expr="//search/field[@name='journal_id']" position="after">
<field name="l10n_it_edi_transaction" groups="base.group_no_one" invisible="1"/>
<field name="l10n_it_edi_attachment_id" groups="base.group_no_one" invisible="1"/>
</xpath>
</field>
</record>
<record id="account_invoice_form_l10n_it" model="ir.ui.view">
<field name="name">account.move.form.l10n.it</field>
<field name="model">account.move</field>
<field name="priority">20</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//page[@name='other_info']" position="after">
<page string="Electronic Invoicing"
name="electronic_invoicing"
attrs="{'invisible': ['|', ('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')), ('country_code', '!=', 'IT')]}">
<group>
<group>
<field name="l10n_it_edi_transaction" groups="base.group_no_one" readonly="1"/>
<field name="l10n_it_stamp_duty"/>
<field name="l10n_it_ddt_id"
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
</group>
</group>
</page>
</xpath>
</data>
</field>
</record>
<record id="l10n_it_ddt" model="ir.ui.view">
<field name="name">ddt.form.l10n.it</field>
<field name="model">l10n_it.ddt</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name"/>
<field name="date"/>
</group>
</form>
</field>
</record>
<record id="l10n_it_ddt_list_view" model="ir.ui.view">
<field name="name">l10n_it.ddt.list.view</field>
<field name="model">l10n_it.ddt</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="date"/>
</tree>
</field>
</record>
<record id="action_ddt_account" model="ir.actions.act_window">
<field name="name">Transport Document</field>
<field name="res_model">l10n_it.ddt</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="l10n_it_ddt_list_view"/>
</record>
<menuitem
name="DDT"
parent="account.account_account_menu"
action="action_ddt_account"
id="menu_action_ddt_account"
sequence="15"
groups="base.group_no_one"/>
</odoo>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.proxy.user</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='account_vendor_bills']" position="after">
<div attrs="{'invisible':[('country_code', '!=', 'IT')]}">
<h2>Electronic Document Invoicing</h2>
<div class="row mt16 o_settings_container" id='account_edi'>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<div class="group-content">
<field name="l10n_it_edi_proxy_current_state" invisible="1"/>
<span class="o_form_label">
Fattura Elettronica mode
</span>
<div class="text-muted">
In demo mode Odoo will just simulate the sending of invoices to the government.<br/>
In test mode (experimental) Odoo will send the invoices to a non-production service.
Saving this change will direct all companies on this database to this use this configuration.
Once registered for testing or official, the mode cannot be changed.
</div>
<field name="l10n_it_edi_sdicoop_demo_mode"
widget="radio"
options="{'horizontal': true}"/>
</div>
<div class="mt8 content-group" attrs="{'invisible': ['|',('l10n_it_edi_proxy_current_state','=','active'), '&amp;', ('l10n_it_edi_proxy_current_state','=','demo'), ('l10n_it_edi_sdicoop_demo_mode','=','demo')]}">
<span class="o_form_label">Allow Odoo to process invoices</span>
<div class="text-muted">
By checking this box, I accept that Odoo may process my invoices.
</div>
<div class="content-group">
<field name="l10n_it_edi_sdicoop_register"/>
</div>
</div>
<div class="text-success mt8" attrs="{'invisible': [('l10n_it_edi_proxy_current_state','in', ['inactive', 'demo'])]}">
An Official or Test service has been registered.
</div>
<div class="text-success mt8" attrs="{'invisible': ['|',('l10n_it_edi_proxy_current_state','!=', 'demo'), ('l10n_it_edi_sdicoop_demo_mode', '!=', 'demo')]}">
A Demo service is in use.
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,44 @@
[project]
name = "odoo-bringout-oca-ocb-l10n_it_edi"
version = "16.0.0"
description = "Italy - E-invoicing - Odoo addon"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-l10n_it>=16.0.0",
"odoo-bringout-oca-ocb-account_edi>=16.0.0",
"odoo-bringout-oca-ocb-account_edi_proxy_client>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["l10n_it_edi"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]