mirror of
https://github.com/bringout/oca-ocb-l10n_europe.git
synced 2026-04-27 03:02:00 +02:00
Initial commit: L10N_Europe packages
This commit is contained in:
commit
9803722600
2377 changed files with 380711 additions and 0 deletions
50
odoo-bringout-oca-ocb-l10n_it_edi/README.md
Normal file
50
odoo-bringout-oca-ocb-l10n_it_edi/README.md
Normal 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
|
||||
32
odoo-bringout-oca-ocb-l10n_it_edi/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-ocb-l10n_it_edi/doc/ARCHITECTURE.md
Normal 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.
|
||||
3
odoo-bringout-oca-ocb-l10n_it_edi/doc/CONFIGURATION.md
Normal file
3
odoo-bringout-oca-ocb-l10n_it_edi/doc/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for l10n_it_edi. Configure related models, access rights, and options as needed.
|
||||
3
odoo-bringout-oca-ocb-l10n_it_edi/doc/CONTROLLERS.md
Normal file
3
odoo-bringout-oca-ocb-l10n_it_edi/doc/CONTROLLERS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
7
odoo-bringout-oca-ocb-l10n_it_edi/doc/DEPENDENCIES.md
Normal file
7
odoo-bringout-oca-ocb-l10n_it_edi/doc/DEPENDENCIES.md
Normal 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)
|
||||
4
odoo-bringout-oca-ocb-l10n_it_edi/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-l10n_it_edi/doc/FAQ.md
Normal 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.
|
||||
7
odoo-bringout-oca-ocb-l10n_it_edi/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-ocb-l10n_it_edi/doc/INSTALL.md
Normal 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"
|
||||
```
|
||||
23
odoo-bringout-oca-ocb-l10n_it_edi/doc/MODELS.md
Normal file
23
odoo-bringout-oca-ocb-l10n_it_edi/doc/MODELS.md
Normal 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.
|
||||
6
odoo-bringout-oca-ocb-l10n_it_edi/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-ocb-l10n_it_edi/doc/OVERVIEW.md
Normal 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
|
||||
3
odoo-bringout-oca-ocb-l10n_it_edi/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-ocb-l10n_it_edi/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
34
odoo-bringout-oca-ocb-l10n_it_edi/doc/SECURITY.md
Normal file
34
odoo-bringout-oca-ocb-l10n_it_edi/doc/SECURITY.md
Normal 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
|
||||
5
odoo-bringout-oca-ocb-l10n_it_edi/doc/TROUBLESHOOTING.md
Normal file
5
odoo-bringout-oca-ocb-l10n_it_edi/doc/TROUBLESHOOTING.md
Normal 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.
|
||||
7
odoo-bringout-oca-ocb-l10n_it_edi/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-l10n_it_edi/doc/USAGE.md
Normal 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
|
||||
```
|
||||
3
odoo-bringout-oca-ocb-l10n_it_edi/doc/WIZARDS.md
Normal file
3
odoo-bringout-oca-ocb-l10n_it_edi/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
32
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/__init__.py
Normal file
32
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/__init__.py
Normal 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)
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
1177
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/i18n/it.po
Normal file
1177
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/i18n/it.po
Normal file
File diff suppressed because it is too large
Load diff
1144
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/i18n/l10n_it_edi.pot
Normal file
1144
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/i18n/l10n_it_edi.pot
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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 all’esportazione"),
|
||||
("N3.5", "[N3.5] Non imponibili – a seguito di dichiarazioni d’intento"),
|
||||
("N3.6", "[N3.6] Non imponibili – altre operazioni che non concorrono alla formazione del plafond"),
|
||||
("N4", "[N4] Esenti"),
|
||||
("N5", "[N5] Regime del margine / IVA non esposta in fattura"),
|
||||
("N6", "[N6] Inversione contabile (per le operazioni in reverse charge ovvero nei casi di autofatturazione per acquisti extra UE di servizi ovvero per importazioni di beni nei soli casi previsti)"),
|
||||
("N6.1", "[N6.1] Inversione contabile – cessione di rottami e altri materiali di recupero"),
|
||||
("N6.2", "[N6.2] Inversione contabile – cessione di oro e argento puro"),
|
||||
("N6.3", "[N6.3] Inversione contabile – subappalto nel settore edile"),
|
||||
("N6.4", "[N6.4] Inversione contabile – cessione di fabbricati"),
|
||||
("N6.5", "[N6.5] Inversione contabile – cessione di telefoni cellulari"),
|
||||
("N6.6", "[N6.6] Inversione contabile – cessione di prodotti elettronici"),
|
||||
("N6.7", "[N6.7] Inversione contabile – prestazioni comparto edile esettori connessi"),
|
||||
("N6.8", "[N6.8] Inversione contabile – operazioni settore energetico"),
|
||||
("N6.9", "[N6.9] Inversione contabile – altri casi"),
|
||||
("N7", "[N7] IVA assolta in altro stato UE (prestazione di servizi di telecomunicazioni, tele-radiodiffusione ed elettronici ex art. 7-octies, comma 1 lett. a, b, art. 74-sexies DPR 633/72)")],
|
||||
string="Exoneration",
|
||||
help="Exoneration type",
|
||||
default="N1")
|
||||
l10n_it_law_reference = fields.Char(string="Law Reference", size=100)
|
||||
|
||||
@api.constrains('l10n_it_has_exoneration',
|
||||
'l10n_it_kind_exoneration',
|
||||
'l10n_it_law_reference',
|
||||
'amount',
|
||||
'invoice_repartition_line_ids',
|
||||
'refund_repartition_line_ids')
|
||||
def _check_exoneration_with_no_tax(self):
|
||||
for tax in self:
|
||||
if tax.l10n_it_has_exoneration:
|
||||
if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0:
|
||||
raise ValidationError(_("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0."))
|
||||
if tax.l10n_it_kind_exoneration == 'N6' and tax._l10n_it_is_split_payment():
|
||||
raise UserError(_("Split Payment is not compatible with exoneration of kind 'N6'"))
|
||||
|
||||
def _l10n_it_filter_kind(self, kind):
|
||||
""" This can be overridden by l10n_it_edi_withholding for different kind of taxes (withholding, pension_fund)."""
|
||||
return self if kind == 'vat' else self.env['account.tax']
|
||||
|
||||
def _l10n_it_is_split_payment(self):
|
||||
""" Split payment means that the Public Administration buyer will pay VAT
|
||||
to the tax agency instead of the vendor
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
tax_tags = self.get_tax_tags(is_refund=False, repartition_type='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)
|
||||
18
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/models/ddt.py
Normal file
18
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/models/ddt.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 d’arte, d’antiquariato o da collezione (art.36, DL 41/95)"),
|
||||
("RF15", "[RF15] Agenzie di vendite all’asta di oggetti d’arte, 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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
124
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/tests/common.py
Normal file
124
odoo-bringout-oca-ocb-l10n_it_edi/l10n_it_edi/tests/common.py
Normal 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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Binary file not shown.
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Binary file not shown.
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
}
|
||||
])
|
||||
|
|
@ -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>",
|
||||
}
|
||||
)
|
||||
|
|
@ -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")
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import remove_signature
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'), '&', ('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>
|
||||
44
odoo-bringout-oca-ocb-l10n_it_edi/pyproject.toml
Normal file
44
odoo-bringout-oca-ocb-l10n_it_edi/pyproject.toml
Normal 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",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue