Skip to content

Instantly share code, notes, and snippets.

@3v1n0
Last active November 14, 2023 21:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 3v1n0/52d98344272077679cb0fcabf5487bf1 to your computer and use it in GitHub Desktop.
Save 3v1n0/52d98344272077679cb0fcabf5487bf1 to your computer and use it in GitHub Desktop.
Fattura Elettronica and Python playground
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 2019-2021 - Marco Trevisan
#
# Fattura Elettronica and python Playground
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import calendar
import datetime
import xml.etree.cElementTree as ET
import xml.dom.minidom
from enum import Enum
class ITElectronicInvoice(object):
'''Spec at https://www.fatturapa.gov.it/export/documenti/Specifiche_tecniche_del_formato_FatturaPA_V1.3.1.pdf'''
BASE_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:FatturaElettronica versione="FPR12" xmlns:ns2="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2">
</ns2:FatturaElettronica>
'''
class Regime(Enum):
ORDINARY = 'RF01' # Ordinario
MINIMUM_CONTRIBUTORS = 'RF02' # Contribuenti minimi (art.1, c.96-117, L. 244/07)
NEW_VENTURES = 'RF03' # Nuove iniziative produttive (art.13, L. 388/00)
AGRICULTURE_FISHING = 'RF04' # Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)
TOBACCO_SELL = 'RF05' # Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)
MATCHSTICK_TRADING = 'RF06' # Commercio fiammiferi (art.74, c.1, DPR 633/72)
EDITORS = 'RF07' # Editoria (art.74, c.1, DPR 633/72)
PUBLIC_TELEPHONY = 'RF08' # Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)
PUBLIC_TRANSPORT = 'RF09' # Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)
GAMING = 'RF10' # Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)
TRAVEL_AGENTS = 'RF11' # Agenzie viaggi e turismo (art.74-ter, DPR 633/72)
AGRITURISM = 'RF12' # Agriturismo (art.5, c.2, L. 413/91)
HOME_DELIVERY = 'RF13' # Vendite a domicilio (art.25-bis, c.6, DPR 600/73)
SECONDHAND_SELL = 'RF14' # Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)
BID_AUCTION = 'RF15' # Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)
PUBLIC_ADMINISTRATION_CASH_ACCOUNTING = 'RF16' # IVA per cassa P.A. (art.6, c.5, DPR 633/72)
CASH_ACCOUNTING = 'RF17' # IVA per cassa (art. 32-bis, DL 83/2012)
OTHER = 'RF18' # Altro
FLAT_REGIME = 'RF19' # Regime forfettario (art.1, c.54-89, L. 190/2014)
class TaxType(Enum):
NORMAL = None
EXCLUDED_ART_15 = 'N1'
# NOT_APPLIED = 'N2'
NOT_APPLIED_DPR_633_72 = 'N2.1'
NOT_APPLIED_OTHERS = 'N2.2'
# NOT_APPLIABLE = 'N3'
NOT_APPLIABLE_EXPORT = 'N3.1'
NOT_APPLIABLE_TRANSFER_EU = 'N3.2'
NOT_APPLIABLE_SAN_MARINO = 'N3.3'
NOT_APPLIABLE_TRANSFER_EXPORT = 'N3.4'
NOT_APPLIABLE_INTENT = 'N3.5'
NOT_APPLIABLE_OTHERS = 'N3.6'
EXEMPT = 'N4'
MARGIN_REGIME = 'N5'
# REVERSE_CHARGE = 'N6'
REVERSE_CHARGE_N6_1 = 'N6.1'
REVERSE_CHARGE_N6_2 = 'N6.2'
REVERSE_CHARGE_N6_3 = 'N6.3'
REVERSE_CHARGE_N6_4 = 'N6.4'
REVERSE_CHARGE_N6_5 = 'N6.5'
REVERSE_CHARGE_N6_6 = 'N6.6'
REVERSE_CHARGE_N6_7 = 'N6.7'
REVERSE_CHARGE_N6_8 = 'N6.8'
REVERSE_CHARGE_N6_9 = 'N6.9'
class PensionType(Enum):
LAWYERS = 'TC01'
ACCOUNTANTS = 'TC02'
SURVEYORS = 'TC03'
ENGINEERS_AND_ARCHITECTS = 'TC04'
NOTARIES = 'TC05'
COMMERCIAL_ACCOUNTANTS = 'TC06'
ENASARCO = 'TC07'
ENPACL = 'TC08'
ENPAM = 'TC09'
ENPAF = 'TC10'
ENPAV = 'TC11'
ENPAIA = 'TC12'
POSTMEN = 'TC13'
INPGI = 'TC14'
ONAOSI = 'TC15'
CASAGIT = 'TC16'
EPPI = 'TC17'
EPAP = 'TC18'
ENPAB = 'TC19'
ENPAPI = 'TC20'
ENPAP = 'TC21'
INPS = 'TC22'
class PaymentCondition(Enum):
INSTALLMENTS = 'TP01'
FULL = 'TP02'
PREPAID = 'TP03'
class PaymentMode(Enum):
CASH = 'MP01'
CHECK = 'MP02'
CASHIER_CHECK = 'MP03'
CONTANTI_PRESSO_TESORERIA = 'MP04'
BANK_TRANSFER = 'MP05'
VAGLIA_CAMBIARIO = 'MP06'
BOLLETTINO_BANCARIO = 'MP07'
CREDIT_CARD = 'MP08'
RID = 'MP09'
RID_UTENZE = 'MP10'
RID_VELOCE = 'MP11'
RIBA = 'MP12'
MAV = 'MP13'
QUIETANZA_ERARIO_STATO = 'MP14'
GIROCONTO_SU_CONTI_DI_CONTABILITÀ_SPECIALE = 'MP15'
DOMICILIAZIONE_BANCARIA = 'MP16'
DOMICILIAZIONE_POSTALE = 'MP17'
class Address(object):
def __init__(self, street, no, zip_code, city, province, country_id):
self.Indirizzo = street
self.NumeroCivico = no
self.CAP = zip_code
self.Comune = city
self.Provincia = province
self.Nazione = country_id
class Contacts(object):
def __init__(self, email, phone=None):
self.Telefono = phone
self.Email = email
class FiscalContacts(object):
def __init__(self, country_id, vat_no):
self.IdPaese = country_id
self.IdCodice = vat_no
class Business(object):
def __init__(self, country_id, vat_no, nin, regime, name, address, contacts=None):
self.DatiAnagrafici = {
'IdFiscaleIVA': ITElectronicInvoice.FiscalContacts(country_id, vat_no),
'CodiceFiscale': nin,
'Anagrafica': {
'Denominazione': name
},
'RegimeFiscale': regime,
}
self.Sede = address
self.Contatti = contacts
class Pension(object):
def __init__(self, pension_type, amount, rate, vat=22, tax_type=None):
self.TipoCassa = pension_type
self.AlCassa = float(rate)
self.ImportoContributoCassa = float(amount)
self.AliquotaIVA = float(vat)
self.Natura = tax_type
class PaymentDetails(object):
def __init__(self, beneficiary, payment_mode, amount, iban, date_limit,
abi=None, cab=None, bic=None, institute=None):
assert payment_mode == ITElectronicInvoice.PaymentMode.BANK_TRANSFER
self.Beneficiario = beneficiary
self.ModalitaPagamento = payment_mode
self.DataScadenzaPagamento = date_limit
self.ImportoPagamento = float(amount)
self.IstitutoFinanziario = institute
self.IBAN = iban
self.ABI = int(abi) if abi else None
self.CAB = int(cab) if cab else None
self.BIC = bic
class Item(object):
def __init__(self, description, amount, vat=22, tax_type=None,
start=None, end=None, number=1):
self.Descrizione = description
self.DataInizioPeriodo = start
self.DataFinePeriodo = end
self.PrezzoUnitario = float(amount)
self.PrezzoTotale = float(amount * number)
self.AliquotaIVA = float(vat)
self.Natura = tax_type
def __init__(self, id, date, sender, emitter, receiver, receiver_code, receiver_pec=None):
self.fe = ET.fromstring(ITElectronicInvoice.BASE_XML)
self.id = id
self.date = date
self.sender = sender
self.emitter = emitter
self.receiver = receiver
self.receiver_code = receiver_code
self.receiver_pec = receiver_pec
self.items = []
self.tax_rules = {}
self.beni = None
self.pension = None
self.payment = None
def add_item(self, item):
if item.PrezzoTotale != 0:
self.items.append(item)
return item
def add_expense(self, amount, reason):
self.add_item(ITElectronicInvoice.Item(reason, amount, vat=0,
tax_type=ITElectronicInvoice.TaxType.EXCLUDED_ART_15))
def set_pension(self, description, pension_type, amount, rate, vat, tax_type):
item = self.add_item(ITElectronicInvoice.Item(description, amount,
vat=vat, tax_type=tax_type))
self.pension = ITElectronicInvoice.Pension(pension_type, amount, rate,
vat, tax_type)
self.pension._item = item
def set_payment_details(self, conditions, details):
self.payment = {
'CondizioniPagamento': conditions,
'DettaglioPagamento': details,
}
def _xmlfy_obj(self, parent, d):
if isinstance(d, Enum):
self._xmlfy_obj(parent, d.value)
elif isinstance(d, dict):
p = None
for k, v in d.items():
if v is None:
continue
p = ET.SubElement(parent, k)
if isinstance(v, tuple) or isinstance(v, list):
for vv in v:
self._xmlfy_obj(p, vv)
else:
self._xmlfy_obj(p, v)
if len(d.items()) == 1 and p != None:
parent = p
elif isinstance(d, tuple) or isinstance(d, list):
p = ET.SubElement(parent, parent.tag)
for v in d:
self._xmlfy_obj(p, v)
elif isinstance(d, object) and hasattr(d, '__dict__'):
self._xmlfy_obj(parent, { k: v for k, v in d.__dict__.items()
if not k.startswith('_') })
elif isinstance(d, float):
parent.text = '{0:.2f}'.format(round(d, 2))
elif isinstance(d, datetime.datetime):
parent.text = '{:%Y-%m-%d}'.format(d)
else:
parent.text = str(d)
return parent
def _build_header(self):
return self._xmlfy_obj(self.fe, {
'FatturaElettronicaHeader': {
'DatiTrasmissione': {
'IdTrasmittente': self.sender,
'ProgressivoInvio': 1,
'FormatoTrasmissione': 'FPR12',
'CodiceDestinatario': self.receiver_code,
'PECDestinatario': self.receiver_pec
},
'CedentePrestatore': self.emitter,
'CessionarioCommittente': self.receiver,
}
})
def _build_body(self):
body = self._xmlfy_obj(self.fe, {
'FatturaElettronicaBody': {
'DatiGenerali': {
'DatiGeneraliDocumento': {
'TipoDocumento': 'TD01',
'Divisa': 'EUR',
'Data': self.date,
'Numero': self.id,
'DatiCassaPrevidenziale': self.pension,
'ImportoTotaleDocumento': self.total_amount(),
}
}
}
})
if len(self.items):
self.beni = self._xmlfy_obj(body, { 'DatiBeniServizi': [] })
self._xmlfy_obj(body, {'DatiPagamento': self.payment})
return body
def total_amount(self):
return sum([x.PrezzoTotale for x in self.items])
def _compute_amount_by_tax(self, tax_type):
return sum([x.PrezzoTotale for x in self.items if x.Natura == tax_type])
def _add_items(self):
idx = 1
for item in self.items:
if self.pension and self.pension._item == item:
continue
detail = self._xmlfy_obj(self.beni, {
'DettaglioLinee': {
'NumeroLinea': idx,
}
})
self._xmlfy_obj(detail, item)
idx += 1
def _add_summary_line(self, kind, amount, vat_percent=0, vat=0):
if amount <= 0:
return None
return self._xmlfy_obj(self.beni, {
'DatiRiepilogo': {
'AliquotaIVA': float(vat_percent),
'Natura': kind,
'ImponibileImporto': float(amount),
'Imposta': float(vat),
}
})
def _compute_tax_summary(self):
for tax_type in ITElectronicInvoice.TaxType:
summary = self._add_summary_line(tax_type,
self._compute_amount_by_tax(tax_type))
if tax_type in self.tax_rules:
self._xmlfy_obj(summary,
{ 'RiferimentoNormativo': self.tax_rules[tax_type] })
def generate(self, path):
self._build_header()
self._build_body()
self._add_items()
self._compute_tax_summary()
with open(path, 'w') as file:
xmlstr = ET.tostring(self.fe, encoding='unicode', method='xml')
pretty = xml.dom.minidom.parseString(xmlstr).toprettyxml(indent=' ')
xmlstr = '\n'.join(list(filter(lambda x: len(x.strip()), pretty.split('\n'))))
file.write(xmlstr)
class CanonicalInvoice(ITElectronicInvoice):
def __init__(self, id, date, amount, sender, emitter):
canonical = ITElectronicInvoice.Business('GB', '003232247', None, None,
'Canonical Services Limited', ITElectronicInvoice.Address(
'991TT, Finch Road', '12-14', '00000', 'Douglas', None, 'IM'))
super().__init__(id, date, sender, emitter, canonical, receiver_code='XXXXXXX')
self.first_day = datetime.date(year=date.year, month=date.month, day=1)
self.last_day = datetime.date(year=date.year, month=date.month,
day=calendar.monthrange(date.year, date.month)[1])
description = 'Development in {} {}'.format('{:%B}'.format(date), date.year)
self.add_item(ITElectronicInvoice.Item(description, amount, vat=0,
start=self.first_day, end=self.last_day,
tax_type=ITElectronicInvoice.TaxType.NOT_APPLIED_DPR_633_72))
self.tax_rules[ITElectronicInvoice.TaxType.NOT_APPLIED_DPR_633_72] = \
'art. 7, DPR 633/72 - Operazione di prestazione di servizi non soggetta'
def add_bonus(self, amount, reason):
self.add_item(ITElectronicInvoice.Item(reason, amount, vat=0,
tax_type=ITElectronicInvoice.TaxType.NOT_APPLIED_DPR_633_72))
def add_expense(self, amount, reason):
super().add_expense(amount, reason)
if amount > 0:
print('Remember to attach the Expensify PDF for `{}` when '
'uploading the invoice'.format(
reason if len(reason) else 'the {} expense'.format(amount)))
if __name__ == "__main__":
vat_no = '012345678901'
me_person = ITElectronicInvoice.FiscalContacts('IT', 'TRVMRC12B345678Z')
me = ITElectronicInvoice.Business('IT', vat_no, me_person.IdCodice,
ITElectronicInvoice.Regime.ORDINARY, 'SiProgramma.it',
ITElectronicInvoice.Address('Via lì', 2, 12345, 'Laggiù', 'FI', 'IT'),
ITElectronicInvoice.Contacts('foo@bar.baz', '3287654321'))
id = 321
date = datetime.datetime.now()
total_amount = 10000000
bonus = 5000
pension_revenge_rate = 4.0
expense = 3000
amount = ((total_amount - expense) / (1.0 + pension_revenge_rate/100)) - bonus
inps = total_amount - amount - bonus - expense
fe = CanonicalInvoice(id, date, amount, me_person, me)
fe.add_expense(expense/2, 'Alcool')
fe.add_expense(expense/2, 'Fun')
fe.add_bonus(bonus, 'Super spotlight prize')
fe.set_pension('Rivalsa INPS 4% (national insurance)',
ITElectronicInvoice.PensionType.INPS, inps,
rate=pension_revenge_rate, vat=0,
tax_type=ITElectronicInvoice.TaxType.NOT_APPLIED_DPR_633_72)
fe.set_payment_details(ITElectronicInvoice.PaymentCondition.FULL,
ITElectronicInvoice.PaymentDetails('Marco Trevisan',
ITElectronicInvoice.PaymentMode.BANK_TRANSFER,
amount=total_amount, date_limit=date,
iban='ES6621000418401234567891', institute='Caixa bank'))
path = '/tmp/IT{}_draft.xml'.format(vat_no)
fe.generate(path)
print('File saved as', path)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment