chore: updated education config
diff --git a/erpnext/regional/italy/__init__.py b/erpnext/regional/italy/__init__.py
new file mode 100644
index 0000000..ef1d582
--- /dev/null
+++ b/erpnext/regional/italy/__init__.py
@@ -0,0 +1,79 @@
+# coding=utf-8
+
+fiscal_regimes = [
+ "RF01-Ordinario",
+ "RF02-Contribuenti minimi (art.1, c.96-117, L. 244/07)",
+ "RF04-Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)",
+ "RF05-Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)",
+ "RF06-Commercio fiammiferi (art.74, c.1, DPR 633/72)",
+ "RF07-Editoria (art.74, c.1, DPR 633/72)",
+ "RF08-Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)",
+ "RF09-Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)",
+ "RF10-Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)",
+ "RF11-Agenzie viaggi e turismo (art.74-ter, DPR 633/72)",
+ "RF12-Agriturismo (art.5, c.2, L. 413/91)",
+ "RF13-Vendite a domicilio (art.25-bis, c.6, DPR 600/73)",
+ "RF14-Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)",
+ "RF15-Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)",
+ "RF16-IVA per cassa P.A. (art.6, c.5, DPR 633/72)",
+ "RF17-IVA per cassa (art. 32-bis, DL 83/2012)",
+ "RF18-Altro",
+ "RF19-Regime forfettario (art.1, c.54-89, L. 190/2014)"
+]
+
+tax_exemption_reasons = [
+ "N1-Escluse ex art. 15",
+ "N2-Non Soggette",
+ "N3-Non Imponibili",
+ "N4-Esenti",
+ "N5-Regime del margine / IVA non esposta in fattura",
+ "N6-Inversione Contabile",
+ "N7-IVA assolta in altro stato UE"
+]
+
+mode_of_payment_codes = [
+ "MP01-Contanti",
+ "MP02-Assegno",
+ "MP03-Assegno circolare",
+ "MP04-Contanti presso Tesoreria",
+ "MP05-Bonifico",
+ "MP06-Vaglia cambiario",
+ "MP07-Bollettino bancario",
+ "MP08-Carta di pagamento",
+ "MP09-RID",
+ "MP10-RID utenze",
+ "MP11-RID veloce",
+ "MP12-RIBA",
+ "MP13-MAV",
+ "MP14-Quietanza erario",
+ "MP15-Giroconto su conti di contabilità speciale",
+ "MP16-Domiciliazione bancaria",
+ "MP17-Domiciliazione postale",
+ "MP18-Bollettino di c/c postale",
+ "MP19-SEPA Direct Debit",
+ "MP20-SEPA Direct Debit CORE",
+ "MP21-SEPA Direct Debit B2B",
+ "MP22-Trattenuta su somme già riscosse"
+]
+
+vat_collectability_options = [
+ "I-Immediata",
+ "D-Differita",
+ "S-Scissione dei Pagamenti"
+]
+
+state_codes = {'Siracusa': 'SR', 'Bologna': 'BO', 'Grosseto': 'GR', 'Caserta': 'CE', 'Alessandria': 'AL', 'Ancona': 'AN', 'Pavia': 'PV',
+ 'Benevento or Beneventum': 'BN', 'Modena': 'MO', 'Lodi': 'LO', 'Novara': 'NO', 'Avellino': 'AV', 'Verona': 'VR', 'Forli-Cesena': 'FC',
+ 'Caltanissetta': 'CL', 'Brescia': 'BS', 'Rieti': 'RI', 'Treviso': 'TV', 'Ogliastra': 'OG', 'Olbia-Tempio': 'OT', 'Bergamo': 'BG',
+ 'Napoli': 'NA', 'Campobasso': 'CB', 'Fermo': 'FM', 'Roma': 'RM', 'Lucca': 'LU', 'Rovigo': 'RO', 'Piacenza': 'PC', 'Monza and Brianza': 'MB',
+ 'La Spezia': 'SP', 'Pescara': 'PE', 'Vercelli': 'VC', 'Enna': 'EN', 'Nuoro': 'NU', 'Medio Campidano': 'MD', 'Trieste': 'TS', 'Aosta': 'AO',
+ 'Firenze': 'FI', 'Trapani': 'TP', 'Messina': 'ME', 'Teramo': 'TE', 'Udine': 'UD', 'Verbano-Cusio-Ossola': 'VB', 'Padua': 'PD',
+ 'Reggio Emilia': 'RE', 'Frosinone': 'FR', 'Taranto': 'TA', 'Catanzaro': 'CZ', 'Belluno': 'BL', 'Pordenone': 'PN', 'Viterbo': 'VT',
+ 'Gorizia': 'GO', 'Vatican City': 'SCV', 'Ferrara': 'FE', 'Chieti': 'CH', 'Crotone': 'KR', 'Foggia': 'FG', 'Perugia': 'PG', 'Bari': 'BA',
+ 'Massa-Carrara': 'MS', 'Pisa': 'PI', 'Latina': 'LT', 'Salerno': 'SA', 'Turin': 'TO', 'Lecco': 'LC', 'Lecce': 'LE', 'Pistoia': 'PT', 'Como': 'CO',
+ 'Barletta-Andria-Trani': 'BT', 'Mantua': 'MN', 'Ragusa': 'RG', 'Macerata': 'MC', 'Imperia': 'IM', 'Palermo': 'PA', 'Matera': 'MT', "L'Aquila": 'AQ',
+ 'Milano': 'MI', 'Catania': 'CT', 'Pesaro e Urbino': 'PU', 'Potenza': 'PZ', 'Republic of San Marino': 'RSM', 'Genoa': 'GE', 'Brindisi': 'BR',
+ 'Cagliari': 'CA', 'Siena': 'SI', 'Vibo Valentia': 'VV', 'Reggio Calabria': 'RC', 'Ascoli Piceno': 'AP', 'Carbonia-Iglesias': 'CI', 'Oristano': 'OR',
+ 'Asti': 'AT', 'Ravenna': 'RA', 'Vicenza': 'VI', 'Savona': 'SV', 'Biella': 'BI', 'Rimini': 'RN', 'Agrigento': 'AG', 'Prato': 'PO', 'Cuneo': 'CN',
+ 'Cosenza': 'CS', 'Livorno or Leghorn': 'LI', 'Sondrio': 'SO', 'Cremona': 'CR', 'Isernia': 'IS', 'Trento': 'TN', 'Terni': 'TR', 'Bolzano/Bozen': 'BZ',
+ 'Parma': 'PR', 'Varese': 'VA', 'Venezia': 'VE', 'Sassari': 'SS', 'Arezzo': 'AR'}
\ No newline at end of file
diff --git a/erpnext/regional/italy/e-invoice.xml b/erpnext/regional/italy/e-invoice.xml
new file mode 100644
index 0000000..c886ee9
--- /dev/null
+++ b/erpnext/regional/italy/e-invoice.xml
@@ -0,0 +1,216 @@
+{%- macro format_float(value) -%}
+{{ "%.2f" % value|abs }}
+{%- endmacro -%}
+
+{%- macro render_address(address) %}
+<Indirizzo>{{ address.address_line1 }}</Indirizzo>
+<CAP>{{ address.pincode }}</CAP>
+<Comune>{{ address.city }}</Comune>
+{%- if address.state_code %}
+<Provincia>{{ address.state_code }}</Provincia>
+{%- endif %}
+<Nazione>{{ address.country_code }}</Nazione>
+{%- endmacro %}
+
+{%- macro render_discount_or_margin(item) -%}
+{%- if item.discount_percentage > 0.0 or item.margin_type %}
+<ScontoMaggiorazione>
+ {%- if item.discount_percentage > 0.0 %}
+ <Tipo>SC</Tipo>
+ <Percentuale>{{ format_float(item.discount_percentage) }}</Percentuale>
+ {%- endif %}
+ {%- if item.margin_rate_or_amount > 0.0 -%}
+ <Tipo>MG</Tipo>
+ {%- if item.margin_type == "Percentage" -%}
+ <Percentuale>{{ format_float(item.margin_rate_or_amount) }}</Percentuale>
+ {%- elif item.margin_type == "Amount" -%}
+ <Importo>{{ format_float(item.margin_rate_or_amount) }}</Importo>
+ {%- endif -%}
+ {%- endif %}
+</ScontoMaggiorazione>
+{%- endif -%}
+{%- endmacro -%}
+
+<?xml version='1.0' encoding='UTF-8'?>
+<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"
+ versione="{{ doc.transmission_format_code }}"
+ 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>{{ doc.company_address_data.country_code }}</IdPaese>
+ <IdCodice>{{ doc.company_fiscal_code or doc.company_tax_id | replace("IT","") }}</IdCodice>
+ </IdTrasmittente>
+ <ProgressivoInvio>{{ doc.progressive_number }}</ProgressivoInvio>
+ <FormatoTrasmissione>{{ doc.transmission_format_code }}</FormatoTrasmissione>
+ <CodiceDestinatario>{{ doc.customer_data.recipient_code }}</CodiceDestinatario>
+ {% if doc.company_data.phone or doc.company_data.email -%}
+ <ContattiTrasmittente>
+ {% if doc.company_data.phone -%}<Telefono>{{ doc.company_data.phone }}</Telefono>{%- endif %}
+ {% if doc.company_data.email -%}<Email>{{ doc.company_data.email }}</Email>{%- endif %}
+ </ContattiTrasmittente>
+ {% endif -%}
+ </DatiTrasmissione>
+ <CedentePrestatore>
+ <DatiAnagrafici>
+ <IdFiscaleIVA>
+ <IdPaese>{{ doc.company_address_data.country_code }}</IdPaese>
+ <IdCodice>{{ doc.company_tax_id | replace("IT","") }}</IdCodice>
+ </IdFiscaleIVA>
+ {%- if doc.company_fiscal_code %}
+ <CodiceFiscale>{{ doc.company_fiscal_code }}</CodiceFiscale>
+ {%- endif %}
+ <Anagrafica>
+ <Denominazione>{{ doc.company }}</Denominazione>
+ </Anagrafica>
+ <RegimeFiscale>{{ doc.company_fiscal_regime.split("-")[0] }}</RegimeFiscale>
+ </DatiAnagrafici>
+ <Sede>
+ {{ render_address(doc.company_address_data) }}
+ </Sede>
+ {%- if doc.company_data.registration_number %}
+ <IscrizioneREA>
+ <Ufficio>{{ doc.company_data.registrar_office_province }}</Ufficio>
+ <NumeroREA>{{ doc.company_data.registration_number }}</NumeroREA>
+ {%- if doc.company_data.share_capital_amount %}
+ <CapitaleSociale>{{ format_float(doc.company_data.share_capital_amount) }}</CapitaleSociale>
+ {%- endif %}
+ {%- if doc.company_data.no_of_members %}
+ <SocioUnico>{{ doc.company_data.no_of_members.split("-")[0] }}</SocioUnico>
+ {%- endif %}
+ {%- if doc.company_data.liquidation_state %}
+ <StatoLiquidazione>{{ doc.company_data.liquidation_state.split("-")[0] }}</StatoLiquidazione>
+ {%- endif %}
+ </IscrizioneREA>
+ {%- endif %}
+ </CedentePrestatore>
+ <CessionarioCommittente>
+ <DatiAnagrafici>
+ {%- if doc.customer_data.customer_type == "Individual" %}
+ <CodiceFiscale>{{ doc.customer_data.fiscal_code }}</CodiceFiscale>
+ <Anagrafica>
+ <Nome>{{ doc.customer_data.first_name }}</Nome>
+ <Cognome>{{ doc.customer_data.last_name }}</Cognome>
+ </Anagrafica>
+ {%- else %}
+ {%- if doc.customer_data.is_public_administration %}
+ <CodiceFiscale>{{ doc.customer_data.fiscal_code }}</CodiceFiscale>
+ {%- else %}
+ <IdFiscaleIVA>
+ <IdPaese>{{ doc.customer_address_data.country_code }}</IdPaese>
+ <IdCodice>{{ doc.tax_id | replace("IT","") }}</IdCodice>
+ </IdFiscaleIVA>
+ {%- endif %}
+ <Anagrafica>
+ <Denominazione>{{ doc.customer_name }}</Denominazione>
+ </Anagrafica>
+ {%- endif %}
+ </DatiAnagrafici>
+ {%- if doc.customer_address_data %}
+ <Sede>
+ {{ render_address(doc.customer_address_data) }}
+ </Sede>
+ {%- endif %}
+ </CessionarioCommittente>
+ </FatturaElettronicaHeader>
+ <FatturaElettronicaBody>
+ <DatiGenerali>
+ <DatiGeneraliDocumento>
+ <TipoDocumento>{{ doc.type_of_document }}</TipoDocumento>
+ <Divisa>EUR</Divisa>
+ <Data>{{ doc.posting_date }}</Data>
+ <Numero>{{ doc.unamended_name }}</Numero>
+ {%- if doc.stamp_duty %}
+ <DatiBollo>
+ <BolloVirtuale>SI</BolloVirtuale>
+ <ImportoBollo>{{ format_float(doc.stamp_duty) }}</ImportoBollo>
+ </DatiBollo>
+ {%- endif %}
+ <ImportoTotaleDocumento>{{ format_float(doc.grand_total) }}</ImportoTotaleDocumento>
+ <Causale>VENDITA</Causale>
+ </DatiGeneraliDocumento>
+ {%- if doc.po_no %}
+ <DatiOrdineAcquisto>
+ <IdDocumento>{{ doc.po_no }}</IdDocumento>
+ {%- if doc.po_date %}
+ <Data>{{ doc.po_date }}</Data>
+ {%- endif %}
+ </DatiOrdineAcquisto>
+ {%- endif %}
+ {%- if doc.is_return and doc.return_against_unamended %}
+ <DatiFattureCollegate>
+ <IdDocumento>{{ doc.return_against_unamended }}</IdDocumento>
+ </DatiFattureCollegate>
+ {%- endif %}
+ {%- if doc.shipping_address_data %}
+ <DatiTrasporto>
+ <IndirizzoResa>
+ {{ render_address(doc.shipping_address_data) }}
+ </IndirizzoResa>
+ </DatiTrasporto>
+ {%- endif %}
+ </DatiGenerali>
+ <DatiBeniServizi>
+ {%- for item in doc.e_invoice_items %}
+ <DettaglioLinee>
+ <NumeroLinea>{{ item.idx }}</NumeroLinea>
+ <CodiceArticolo>
+ <CodiceTipo>CODICE</CodiceTipo>
+ <CodiceValore>{{ item.item_code }}</CodiceValore>
+ </CodiceArticolo>
+ <Descrizione>{{ html2text(item.description or '') or item.item_name }}</Descrizione>
+ <Quantita>{{ format_float(item.qty) }}</Quantita>
+ <UnitaMisura>{{ item.stock_uom }}</UnitaMisura>
+ <PrezzoUnitario>{{ format_float(item.price_list_rate or item.rate) }}</PrezzoUnitario>
+ {{ render_discount_or_margin(item) }}
+ <PrezzoTotale>{{ format_float(item.amount) }}</PrezzoTotale>
+ <AliquotaIVA>{{ format_float(item.tax_rate) }}</AliquotaIVA>
+ {%- if item.tax_exemption_reason %}
+ <Natura>{{ item.tax_exemption_reason.split("-")[0] }}</Natura>
+ {%- endif %}
+ </DettaglioLinee>
+ {%- endfor %}
+ {%- for tax, data in doc.tax_data.items() %}
+ <DatiRiepilogo>
+ <AliquotaIVA>{{ format_float(tax|flt) }}</AliquotaIVA>
+ {%- if data.tax_exemption_reason %}
+ <Natura>{{ data.tax_exemption_reason.split("-")[0] }}</Natura>
+ {%- endif %}
+ <ImponibileImporto>{{ format_float(data.taxable_amount) }}</ImponibileImporto>
+ <Imposta>{{ format_float(data.tax_amount) }}</Imposta>
+ <EsigibilitaIVA>{{ doc.vat_collectability.split("-")[0] }}</EsigibilitaIVA>
+ {%- if data.tax_exemption_law %}
+ <RiferimentoNormativo>{{ data.tax_exemption_law }}</RiferimentoNormativo>
+ {%- endif %}
+ </DatiRiepilogo>
+ {%- endfor %}
+ </DatiBeniServizi>
+ {%- if doc.payment_schedule %}
+ <DatiPagamento>
+ {%- if payment_schedule|length > 1 %}
+ <CondizioniPagamento>TP01</CondizioniPagamento>
+ {%- else %}
+ <CondizioniPagamento>TP02</CondizioniPagamento>
+ {%- endif %}
+ {%- for payment_term in doc.payment_schedule %}
+ <DettaglioPagamento>
+ <ModalitaPagamento>{{ payment_term.mode_of_payment_code.split("-")[0] }}</ModalitaPagamento>
+ <DataScadenzaPagamento>{{ payment_term.due_date }}</DataScadenzaPagamento>
+ <ImportoPagamento>{{ format_float(payment_term.payment_amount) }}</ImportoPagamento>
+ <IstitutoFinanziario>{{ payment_term.bank_account_name }}</IstitutoFinanziario>
+ {%- if payment_term.bank_account_iban %}
+ <IBAN>{{ payment_term.bank_account_iban }}</IBAN>
+ <ABI>{{ payment_term.bank_account_iban[5:10] }}</ABI>
+ <CAB>{{ payment_term.bank_account_iban[10:15] }}</CAB>
+ {%- endif %}
+ {%- if payment_term.bank_account_swift_number %}
+ <BIC>{{ payment_term.bank_account_swift_number }}</BIC>
+ {%- endif %}
+ </DettaglioPagamento>
+ {%- endfor %}
+ </DatiPagamento>
+ {%- endif %}
+ </FatturaElettronicaBody>
+</p:FatturaElettronica>
diff --git a/erpnext/regional/italy/sales_invoice.js b/erpnext/regional/italy/sales_invoice.js
new file mode 100644
index 0000000..3457f71
--- /dev/null
+++ b/erpnext/regional/italy/sales_invoice.js
@@ -0,0 +1,19 @@
+erpnext.setup_e_invoice_button = (doctype) => {
+ frappe.ui.form.on(doctype, {
+ refresh: (frm) => {
+ if(frm.doc.docstatus == 1) {
+ frm.add_custom_button('Generate E-Invoice', () => {
+ var w = window.open(
+ frappe.urllib.get_full_url(
+ "/api/method/erpnext.regional.italy.utils.generate_single_invoice?"
+ + "docname=" + frm.doc.name
+ )
+ )
+ if (!w) {
+ frappe.msgprint(__("Please enable pop-ups")); return;
+ }
+ });
+ }
+ }
+ });
+};
diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py
new file mode 100644
index 0000000..2b6e3af
--- /dev/null
+++ b/erpnext/regional/italy/setup.py
@@ -0,0 +1,161 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+# coding=utf-8
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe import _
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from erpnext.regional.italy import fiscal_regimes, tax_exemption_reasons, mode_of_payment_codes, vat_collectability_options
+
+def setup(company=None, patch=True):
+ make_custom_fields()
+ setup_report()
+
+def make_custom_fields(update=True):
+ invoice_item_fields = [
+ dict(fieldname='tax_rate', label='Tax Rate',
+ fieldtype='Float', insert_after='description',
+ print_hide=1, hidden=1, read_only=1),
+ dict(fieldname='tax_amount', label='Tax Amount',
+ fieldtype='Currency', insert_after='tax_rate',
+ print_hide=1, hidden=1, read_only=1, options="currency"),
+ dict(fieldname='total_amount', label='Total Amount',
+ fieldtype='Currency', insert_after='tax_amount',
+ print_hide=1, hidden=1, read_only=1, options="currency")
+ ]
+
+ custom_fields = {
+ 'Company': [
+ dict(fieldname='sb_e_invoicing', label='E-Invoicing',
+ fieldtype='Section Break', insert_after='date_of_establishment', print_hide=1),
+ dict(fieldname='fiscal_regime', label='Fiscal Regime',
+ fieldtype='Select', insert_after='sb_e_invoicing', print_hide=1,
+ options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), fiscal_regimes))),
+ dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='fiscal_regime', print_hide=1,
+ description=_("Applicable if the company is an Individual or a Proprietorship")),
+ dict(fieldname='vat_collectability', label='VAT Collectability',
+ fieldtype='Select', insert_after='fiscal_code', print_hide=1,
+ options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options))),
+ dict(fieldname='cb_e_invoicing1', fieldtype='Column Break', insert_after='vat_collectability', print_hide=1),
+ dict(fieldname='registrar_office_province', label='Province of the Registrar Office',
+ fieldtype='Data', insert_after='cb_e_invoicing1', print_hide=1, length=2),
+ dict(fieldname='registration_number', label='Registration Number',
+ fieldtype='Data', insert_after='registrar_office_province', print_hide=1, length=20),
+ dict(fieldname='share_capital_amount', label='Share Capital',
+ fieldtype='Currency', insert_after='registration_number', print_hide=1,
+ description=_('Applicable if the company is SpA, SApA or SRL')),
+ dict(fieldname='no_of_members', label='No of Members',
+ fieldtype='Select', insert_after='share_capital_amount', print_hide=1,
+ options="\nSU-Socio Unico\nSM-Piu Soci", description=_("Applicable if the company is a limited liability company")),
+ dict(fieldname='liquidation_state', label='Liquidation State',
+ fieldtype='Select', insert_after='no_of_members', print_hide=1,
+ options="\nLS-In Liquidazione\nLN-Non in Liquidazione")
+ ],
+ 'Sales Taxes and Charges': [
+ dict(fieldname='tax_exemption_reason', label='Tax Exemption Reason',
+ fieldtype='Select', insert_after='included_in_print_rate', print_hide=1,
+ depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0',
+ options="\n" + "\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), tax_exemption_reasons))),
+ dict(fieldname='tax_exemption_law', label='Tax Exempt Under',
+ fieldtype='Text', insert_after='tax_exemption_reason', print_hide=1,
+ depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0')
+ ],
+ 'Customer': [
+ dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='tax_id', print_hide=1),
+ dict(fieldname='recipient_code', label='Recipient Code',
+ fieldtype='Data', insert_after='fiscal_code', print_hide=1, default="0000000"),
+ dict(fieldname='pec', label='Recipient PEC',
+ fieldtype='Data', insert_after='fiscal_code', print_hide=1),
+ dict(fieldname='is_public_administration', label='Is Public Administration',
+ fieldtype='Check', insert_after='is_internal_customer', print_hide=1,
+ description=_("Set this if the customer is a Public Administration company."),
+ depends_on='eval:doc.customer_type=="Company"'),
+ dict(fieldname='first_name', label='First Name', fieldtype='Data',
+ insert_after='salutation', print_hide=1, depends_on='eval:doc.customer_type!="Company"'),
+ dict(fieldname='last_name', label='Last Name', fieldtype='Data',
+ insert_after='first_name', print_hide=1, depends_on='eval:doc.customer_type!="Company"')
+ ],
+ 'Mode of Payment': [
+ dict(fieldname='mode_of_payment_code', label='Code',
+ fieldtype='Select', insert_after='included_in_print_rate', print_hide=1,
+ options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), mode_of_payment_codes)))
+ ],
+ 'Payment Schedule': [
+ dict(fieldname='mode_of_payment_code', label='Code',
+ fieldtype='Select', insert_after='mode_of_payment', print_hide=1,
+ options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), mode_of_payment_codes)),
+ fetch_from="mode_of_payment.mode_of_payment_code", read_only=1),
+ dict(fieldname='bank_account', label='Bank Account',
+ fieldtype='Link', insert_after='mode_of_payment_code', print_hide=1,
+ options="Bank Account"),
+ dict(fieldname='bank_account_name', label='Bank Name',
+ fieldtype='Data', insert_after='bank_account', print_hide=1,
+ fetch_from="bank_account.bank", read_only=1),
+ dict(fieldname='bank_account_no', label='Bank Account No',
+ fieldtype='Data', insert_after='bank_account_name', print_hide=1,
+ fetch_from="bank_account.bank_account_no", read_only=1),
+ dict(fieldname='bank_account_iban', label='IBAN',
+ fieldtype='Data', insert_after='bank_account_name', print_hide=1,
+ fetch_from="bank_account.iban", read_only=1),
+ dict(fieldname='bank_account_swift_number', label='Swift Code (BIC)',
+ fieldtype='Data', insert_after='bank_account_iban', print_hide=1,
+ fetch_from="bank_account.swift_number", read_only=1),
+ ],
+ "Sales Invoice": [
+ dict(fieldname='vat_collectability', label='VAT Collectability',
+ fieldtype='Select', insert_after='taxes_and_charges', print_hide=1,
+ options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options)),
+ fetch_from="company.vat_collectability"),
+ dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing',
+ fieldtype='Section Break', insert_after='pos_total_qty', print_hide=1),
+ dict(fieldname='company_tax_id', label='Company Tax ID',
+ fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1,
+ fetch_from="company.tax_id"),
+ dict(fieldname='company_fiscal_code', label='Company Fiscal Code',
+ fieldtype='Data', insert_after='company_tax_id', print_hide=1, read_only=1,
+ fetch_from="company.fiscal_code"),
+ dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime',
+ fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1,
+ fetch_from="company.fiscal_regime"),
+ dict(fieldname='cb_e_invoicing_reference', fieldtype='Column Break',
+ insert_after='company_fiscal_regime', print_hide=1),
+ dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code',
+ fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1,
+ fetch_from="customer.fiscal_code"),
+ ],
+ 'Purchase Invoice Item': invoice_item_fields,
+ 'Sales Order Item': invoice_item_fields,
+ 'Delivery Note Item': invoice_item_fields,
+ 'Sales Invoice Item': invoice_item_fields,
+ 'Quotation Item': invoice_item_fields,
+ 'Purchase Order Item': invoice_item_fields,
+ 'Purchase Receipt Item': invoice_item_fields,
+ 'Supplier Quotation Item': invoice_item_fields,
+ 'Address': [
+ dict(fieldname='country_code', label='Country Code',
+ fieldtype='Data', insert_after='country', print_hide=1, read_only=0,
+ fetch_from="country.code"),
+ dict(fieldname='state_code', label='State Code',
+ fieldtype='Data', insert_after='state', print_hide=1)
+ ]
+ }
+
+ create_custom_fields(custom_fields, ignore_validate = frappe.flags.in_patch, update=update)
+
+def setup_report():
+ report_name = 'Electronic Invoice Register'
+
+ frappe.db.sql(""" update `tabReport` set disabled = 0 where
+ name = %s """, report_name)
+
+ if not frappe.db.get_value('Custom Role', dict(report=report_name)):
+ frappe.get_doc(dict(
+ doctype='Custom Role',
+ report=report_name,
+ roles= [
+ dict(role='Accounts User'),
+ dict(role='Accounts Manager')
+ ]
+ )).insert()
diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py
new file mode 100644
index 0000000..52a5ddc
--- /dev/null
+++ b/erpnext/regional/italy/utils.py
@@ -0,0 +1,359 @@
+from __future__ import unicode_literals
+
+import frappe, json, os
+from frappe.utils import flt, cstr
+from erpnext.controllers.taxes_and_totals import get_itemised_tax
+from frappe import _
+from frappe.core.doctype.file.file import remove_file
+from frappe.desk.form.load import get_attachments
+from erpnext.regional.italy import state_codes
+
+
+def update_itemised_tax_data(doc):
+ if not doc.taxes: return
+
+ itemised_tax = get_itemised_tax(doc.taxes)
+
+ for row in doc.items:
+ tax_rate = 0.0
+ if itemised_tax.get(row.item_code):
+ tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()])
+
+ row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
+ row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
+ row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
+
+@frappe.whitelist()
+def export_invoices(filters=None):
+ saved_xmls = []
+
+ invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"])
+
+ for invoice in invoices:
+ attachments = get_e_invoice_attachments(invoice)
+ saved_xmls += [attachment.file_name for attachment in attachments]
+
+ zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
+
+ download_zip(saved_xmls, zip_filename)
+
+
+@frappe.whitelist()
+def prepare_invoice(invoice, progressive_number):
+ #set company information
+ company = frappe.get_doc("Company", invoice.company)
+
+ invoice.progressive_number = progressive_number
+ invoice.unamended_name = get_unamended_name(invoice)
+ invoice.company_data = company
+ company_address = frappe.get_doc("Address", invoice.company_address)
+ invoice.company_address_data = company_address
+
+ #Set invoice type
+ if invoice.is_return and invoice.return_against:
+ invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
+ invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
+ else:
+ invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
+
+ #set customer information
+ invoice.customer_data = frappe.get_doc("Customer", invoice.customer)
+ customer_address = frappe.get_doc("Address", invoice.customer_address)
+ invoice.customer_address_data = customer_address
+
+ if invoice.shipping_address_name:
+ invoice.shipping_address_data = frappe.get_doc("Address", invoice.shipping_address_name)
+
+ if invoice.customer_data.is_public_administration:
+ invoice.transmission_format_code = "FPA12"
+ else:
+ invoice.transmission_format_code = "FPR12"
+
+ invoice.e_invoice_items = [item for item in invoice.items]
+ tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes)
+ invoice.tax_data = tax_data
+
+ #Check if stamp duty (Bollo) of 2 EUR exists.
+ stamp_duty_charge_row = next((tax for tax in invoice.taxes if tax.charge_type == _("Actual") and tax.tax_amount == 2.0 ), None)
+ if stamp_duty_charge_row:
+ invoice.stamp_duty = stamp_duty_charge_row.tax_amount
+
+ for item in invoice.e_invoice_items:
+ if item.tax_rate == 0.0 and item.tax_amount == 0.0:
+ item.tax_exemption_reason = tax_data["0.0"]["tax_exemption_reason"]
+
+ return invoice
+
+def get_conditions(filters):
+ filters = json.loads(filters)
+
+ conditions = {"docstatus": 1}
+
+ if filters.get("company"): conditions["company"] = filters["company"]
+ if filters.get("customer"): conditions["customer"] = filters["customer"]
+
+ if filters.get("from_date"): conditions["posting_date"] = (">=", filters["from_date"])
+ if filters.get("to_date"): conditions["posting_date"] = ("<=", filters["to_date"])
+
+ if filters.get("from_date") and filters.get("to_date"):
+ conditions["posting_date"] = ("between", [filters.get("from_date"), filters.get("to_date")])
+
+ return conditions
+
+#TODO: Use function from frappe once PR #6853 is merged.
+def download_zip(files, output_filename):
+ from zipfile import ZipFile
+
+ input_files = [frappe.get_site_path('private', 'files', filename) for filename in files]
+ output_path = frappe.get_site_path('private', 'files', output_filename)
+
+ with ZipFile(output_path, 'w') as output_zip:
+ for input_file in input_files:
+ output_zip.write(input_file, arcname=os.path.basename(input_file))
+
+ with open(output_path, 'rb') as fileobj:
+ filedata = fileobj.read()
+
+ frappe.local.response.filename = output_filename
+ frappe.local.response.filecontent = filedata
+ frappe.local.response.type = "download"
+
+def get_invoice_summary(items, taxes):
+ summary_data = frappe._dict()
+ for tax in taxes:
+ #Include only VAT charges.
+ if tax.charge_type == "Actual":
+ continue
+
+ #Charges to appear as items in the e-invoice.
+ if tax.charge_type in ["On Previous Row Total", "On Previous Row Amount"]:
+ reference_row = next((row for row in taxes if row.idx == int(tax.row_id or 0)), None)
+ if reference_row:
+ items.append(
+ frappe._dict(
+ idx=len(items)+1,
+ item_code=reference_row.description,
+ item_name=reference_row.description,
+ rate=reference_row.tax_amount,
+ qty=1.0,
+ amount=reference_row.tax_amount,
+ stock_uom=frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos"),
+ tax_rate=tax.rate,
+ tax_amount=(reference_row.tax_amount * tax.rate) / 100,
+ net_amount=reference_row.tax_amount,
+ taxable_amount=reference_row.tax_amount,
+ item_tax_rate="{}",
+ charges=True
+ )
+ )
+
+ #Check item tax rates if tax rate is zero.
+ if tax.rate == 0:
+ for item in items:
+ item_tax_rate = json.loads(item.item_tax_rate)
+ if tax.account_head in item_tax_rate:
+ key = cstr(item_tax_rate[tax.account_head])
+ summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0, "tax_exemption_reason": "", "tax_exemption_law": ""})
+ summary_data[key]["tax_amount"] += item.tax_amount
+ summary_data[key]["taxable_amount"] += item.net_amount
+ if key == "0.0":
+ summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason
+ summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law
+
+ if summary_data == {}: #Implies that Zero VAT has not been set on any item.
+ summary_data.setdefault("0.0", {"tax_amount": 0.0, "taxable_amount": tax.total,
+ "tax_exemption_reason": tax.tax_exemption_reason, "tax_exemption_law": tax.tax_exemption_law})
+
+ else:
+ item_wise_tax_detail = json.loads(tax.item_wise_tax_detail)
+ for rate_item in [tax_item for tax_item in item_wise_tax_detail.items() if tax_item[1][0] == tax.rate]:
+ key = cstr(tax.rate)
+ if not summary_data.get(key): summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0})
+ summary_data[key]["tax_amount"] += rate_item[1][1]
+ summary_data[key]["taxable_amount"] += sum([item.net_amount for item in items if item.item_code == rate_item[0]])
+
+ for item in items:
+ key = cstr(tax.rate)
+ if item.get("charges"):
+ if not summary_data.get(key): summary_data.setdefault(key, {"taxable_amount": 0.0})
+ summary_data[key]["taxable_amount"] += item.taxable_amount
+
+ return summary_data
+
+#Preflight for successful e-invoice export.
+def sales_invoice_validate(doc):
+ #Validate company
+ if doc.doctype != 'Sales Invoice':
+ return
+
+ if not doc.company_address:
+ frappe.throw(_("Please set an Address on the Company '%s'" % doc.company), title=_("E-Invoicing Information Missing"))
+ else:
+ validate_address(doc.company_address)
+
+ company_fiscal_regime = frappe.get_cached_value("Company", doc.company, 'fiscal_regime')
+ if not company_fiscal_regime:
+ frappe.throw(_("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}")
+ .format(doc.company))
+ else:
+ doc.company_fiscal_regime = company_fiscal_regime
+
+ if not doc.company_tax_id and not doc.company_fiscal_code:
+ frappe.throw(_("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), title=_("E-Invoicing Information Missing"))
+
+ #Validate customer details
+ customer_type, is_public_administration = frappe.db.get_value("Customer", doc.customer, ["customer_type", "is_public_administration"])
+ if customer_type == _("Individual"):
+ if not doc.customer_fiscal_code:
+ frappe.throw(_("Please set Fiscal Code for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
+ else:
+ if is_public_administration:
+ if not doc.customer_fiscal_code:
+ frappe.throw(_("Please set Fiscal Code for the public administration '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
+ else:
+ if not doc.tax_id:
+ frappe.throw(_("Please set Tax ID for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
+
+ if not doc.customer_address:
+ frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing"))
+ else:
+ validate_address(doc.customer_address)
+
+ if not len(doc.taxes):
+ frappe.throw(_("Please set at least one row in the Taxes and Charges Table"), title=_("E-Invoicing Information Missing"))
+ else:
+ for row in doc.taxes:
+ if row.rate == 0 and row.tax_amount == 0 and not row.tax_exemption_reason:
+ frappe.throw(_("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges".format(row.idx)),
+ title=_("E-Invoicing Information Missing"))
+
+ for schedule in doc.payment_schedule:
+ if schedule.mode_of_payment and not schedule.mode_of_payment_code:
+ schedule.mode_of_payment_code = frappe.get_cached_value('Mode of Payment',
+ schedule.mode_of_payment, 'mode_of_payment_code')
+
+#Ensure payment details are valid for e-invoice.
+def sales_invoice_on_submit(doc, method):
+ #Validate payment details
+ if get_company_country(doc.company) not in ['Italy',
+ 'Italia', 'Italian Republic', 'Repubblica Italiana']:
+ return
+
+ if not len(doc.payment_schedule):
+ frappe.throw(_("Please set the Payment Schedule"), title=_("E-Invoicing Information Missing"))
+ else:
+ for schedule in doc.payment_schedule:
+ if not schedule.mode_of_payment:
+ frappe.throw(_("Row {0}: Please set the Mode of Payment in Payment Schedule".format(schedule.idx)),
+ title=_("E-Invoicing Information Missing"))
+ elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"):
+ frappe.throw(_("Row {0}: Please set the correct code on Mode of Payment {1}".format(schedule.idx, schedule.mode_of_payment)),
+ title=_("E-Invoicing Information Missing"))
+
+ prepare_and_attach_invoice(doc)
+
+def prepare_and_attach_invoice(doc, replace=False):
+ progressive_name, progressive_number = get_progressive_name_and_number(doc, replace)
+
+ invoice = prepare_invoice(doc, progressive_number)
+ invoice_xml = frappe.render_template('erpnext/regional/italy/e-invoice.xml', context={"doc": invoice}, is_path=True)
+ invoice_xml = invoice_xml.replace("&", "&")
+
+ xml_filename = progressive_name + ".xml"
+
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": xml_filename,
+ "attached_to_doctype": doc.doctype,
+ "attached_to_name": doc.name,
+ "is_private": True,
+ "content": invoice_xml
+ })
+ _file.save()
+ return file
+
+@frappe.whitelist()
+def generate_single_invoice(docname):
+ doc = frappe.get_doc("Sales Invoice", docname)
+
+ e_invoice = prepare_and_attach_invoice(doc, True)
+
+ content = None
+ with open(frappe.get_site_path('private', 'files', e_invoice.file_name), "r") as f:
+ content = f.read()
+
+ frappe.local.response.filename = e_invoice.file_name
+ frappe.local.response.filecontent = content
+ frappe.local.response.type = "download"
+
+#Delete e-invoice attachment on cancel.
+def sales_invoice_on_cancel(doc, method):
+ if get_company_country(doc.company) not in ['Italy',
+ 'Italia', 'Italian Republic', 'Repubblica Italiana']:
+ return
+
+ for attachment in get_e_invoice_attachments(doc):
+ remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name)
+
+def get_company_country(company):
+ return frappe.get_cached_value('Company', company, 'country')
+
+def get_e_invoice_attachments(invoice):
+ out = []
+ attachments = get_attachments(invoice.doctype, invoice.name)
+ company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id
+
+ for attachment in attachments:
+ if attachment.file_name and attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"):
+ out.append(attachment)
+
+ return out
+
+def validate_address(address_name):
+ fields = ["pincode", "city", "country_code"]
+ data = frappe.get_cached_value("Address", address_name, fields, as_dict=1) or {}
+
+ for field in fields:
+ if not data.get(field):
+ frappe.throw(_("Please set {0} for address {1}".format(field.replace('-',''), address_name)),
+ title=_("E-Invoicing Information Missing"))
+
+def get_unamended_name(doc):
+ attributes = ["naming_series", "amended_from"]
+ for attribute in attributes:
+ if not hasattr(doc, attribute):
+ return doc.name
+
+ if doc.amended_from:
+ return "-".join(doc.name.split("-")[:-1])
+ else:
+ return doc.name
+
+def get_progressive_name_and_number(doc, replace=False):
+ if replace:
+ for attachment in get_e_invoice_attachments(doc):
+ remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name)
+ filename = attachment.file_name.split(".xml")[0]
+ return filename, filename.split("_")[1]
+
+ company_tax_id = doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id
+ progressive_name = frappe.model.naming.make_autoname(company_tax_id + "_.#####")
+ progressive_number = progressive_name.split("_")[1]
+
+ return progressive_name, progressive_number
+
+def set_state_code(doc, method):
+ if doc.get('country_code'):
+ doc.country_code = doc.country_code.upper()
+
+ if not doc.get('state'):
+ return
+
+ if not (hasattr(doc, "state_code") and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"]):
+ return
+
+ state_codes_lower = {key.lower():value for key,value in state_codes.items()}
+
+ state = doc.get('state','').lower()
+ if state_codes_lower.get(state):
+ doc.state_code = state_codes_lower.get(state)