From f1e28e0e8dea00f92832022279cad20b5f0392ee Mon Sep 17 00:00:00 2001 From: Gaurav Date: Wed, 13 Feb 2019 16:46:24 +0530 Subject: [PATCH] mod(regional,italy): Updates to Italian Localization fixes: removed cleanup_files from italy/utils removed extract_doc_number from italy/utils added country filter to italian localization patch replaced dict assignment with dot operator in prepare_invoice correcions in e-invoice xml following changes in prepare_invoice added setup_report in patch added section for e-invoicing custom fields in company added Italy in test_company --- .../doctype/sales_invoice/sales_invoice.py | 5 + erpnext/controllers/accounts_controller.py | 14 + erpnext/hooks.py | 6 + erpnext/patches.txt | 3 +- .../v11_0/make_italian_localization_fields.py | 14 + erpnext/regional/italy/__init__.py | 31 +++ erpnext/regional/italy/e-invoice.xml | 208 +++++++++++++++ erpnext/regional/italy/setup.py | 147 +++++++++- erpnext/regional/italy/utils.py | 251 ++++++++++++++++++ .../electronic_invoice_register/__init__.py | 0 .../electronic_invoice_register.js | 53 ++++ .../electronic_invoice_register.json | 21 ++ .../electronic_invoice_register.py | 8 + erpnext/setup/doctype/company/test_company.py | 2 +- 14 files changed, 747 insertions(+), 16 deletions(-) create mode 100644 erpnext/patches/v11_0/make_italian_localization_fields.py create mode 100644 erpnext/regional/italy/e-invoice.xml create mode 100644 erpnext/regional/italy/utils.py create mode 100644 erpnext/regional/report/electronic_invoice_register/__init__.py create mode 100644 erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js create mode 100644 erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.json create mode 100644 erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 895ca07da2..abd201f5c7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -24,6 +24,7 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points from erpnext.accounts.deferred_revenue import validate_service_stop_date +from erpnext.controllers.accounts_controller import on_submit_regional, on_cancel_regional from erpnext.healthcare.utils import manage_invoice_submit_cancel @@ -198,6 +199,8 @@ class SalesInvoice(SellingController): if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_submit") + on_submit_regional(self) + def validate_pos_paid_amount(self): if len(self.payments) == 0 and self.is_pos: frappe.throw(_("At least one mode of payment is required for POS invoice.")) @@ -253,6 +256,8 @@ class SalesInvoice(SellingController): if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_cancel") + on_cancel_regional(self) + def update_status_updater_args(self): if cint(self.update_stock): self.status_updater.extend([{ diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0ba47edfc1..5a765aa273 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -94,6 +94,8 @@ class AccountsController(TransactionBase): if self.is_return: self.validate_qty() + validate_regional(self) + def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() self.set_due_date() @@ -1132,3 +1134,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name): p_doctype.update_blanket_order() p_doctype.update_billing_percentage() p_doctype.set_status() + +@erpnext.allow_regional +def validate_regional(doc): + pass + +@erpnext.allow_regional +def on_submit_regional(doc): + pass + +@erpnext.allow_regional +def on_cancel_regional(doc): + pass diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 7d77f90afa..2a871f0bc6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -301,5 +301,11 @@ regional_overrides = { }, 'Saudi Arabia': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' + }, + 'Italy': { + 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', + 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate', + 'erpnext.controllers.accounts_controller.on_submit_regional': 'erpnext.regional.italy.utils.sales_invoice_on_submit', + 'erpnext.controllers.accounts_controller.on_cancel_regional': 'erpnext.regional.italy.utils.sales_invoice_on_cancel' } } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2199491a71..45ee8dc2d7 100755 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -585,4 +585,5 @@ erpnext.patches.v11_0.renamed_from_to_fields_in_project erpnext.patches.v11_0.add_permissions_in_gst_settings erpnext.patches.v11_1.setup_guardian_role execute:frappe.delete_doc('DocType', 'Notification Control') -erpnext.patches.v10_0.item_barcode_childtable_migrate # 16-02-2019 \ No newline at end of file +erpnext.patches.v10_0.item_barcode_childtable_migrate # 16-02-2019 +erpnext.patches.v11_0.make_italian_localization_fields diff --git a/erpnext/patches/v11_0/make_italian_localization_fields.py b/erpnext/patches/v11_0/make_italian_localization_fields.py new file mode 100644 index 0000000000..b0b5ef159b --- /dev/null +++ b/erpnext/patches/v11_0/make_italian_localization_fields.py @@ -0,0 +1,14 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +from erpnext.regional.italy.setup import make_custom_fields, setup_report +import frappe + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'Italy'}) + if not company: + return + + make_custom_fields() + setup_report() diff --git a/erpnext/regional/italy/__init__.py b/erpnext/regional/italy/__init__.py index d7dbddce95..22bf84e4d0 100644 --- a/erpnext/regional/italy/__init__.py +++ b/erpnext/regional/italy/__init__.py @@ -30,3 +30,34 @@ tax_exemption_reasons = [ "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" +] diff --git a/erpnext/regional/italy/e-invoice.xml b/erpnext/regional/italy/e-invoice.xml new file mode 100644 index 0000000000..84b7fffbbc --- /dev/null +++ b/erpnext/regional/italy/e-invoice.xml @@ -0,0 +1,208 @@ +{%- macro format_float(value) -%} +{{ "%.2f" % value|abs }} +{%- endmacro -%} + +{%- macro render_address(address) %} +{{ address.address_line1 }} +{{ address.pincode }} +{{ address.city }} +{%- if address.state %} +{{ address.state }} +{%- endif %} +{{ address.country_code|upper }} +{%- endmacro %} + +{%- macro render_discount_or_margin(item) -%} +{%- if item.discount_percentage > 0.0 or item.margin_type %} + + {%- if item.discount_percentage > 0.0 %} + SC + {{ format_float(item.discount_percentage) }} + {%- endif %} + {%- if item.margin_rate_or_amount > 0.0 -%} + MG + {%- if item.margin_type == "Percentage" -%} + {{ format_float(item.margin_rate_or_amount) }} + {%- elif item.margin_type == "Amount" -%} + {{ format_float(item.margin_rate_or_amount) }} + {%- endif -%} + {%- endif %} + +{%- endif -%} +{%- endmacro -%} + + + + + + + {{ doc.company_address_data.country_code|upper or "IT" }} + {{ doc.company_fiscal_code or doc.company_tax_id | replace("IT","") }} + + {{ doc.progressive_number }} + {{ doc.transmission_format_code }} + {{ doc.customer_data.recipient_code }} + {% if doc.company_data.phone or doc.company_data.email -%} + + {% if doc.company_data.phone -%}{{ doc.company_data.phone }}{%- endif %} + {% if doc.company_data.email -%}{{ doc.company_data.email }}{%- endif %} + + {% endif -%} + + + + + {{ doc.company_address_data.country_code|upper or "IT"}} + {{ doc.company_tax_id | replace("IT","") }} + + {%- if doc.company_fiscal_code %} + {{ doc.company_fiscal_code }} + {%- endif %} + + {{ doc.company }} + + {{ doc.company_fiscal_regime.split("-")[0] }} + + + {{ render_address(doc.company_address_data) }} + + {%- if doc.company_data.registration_number %} + + {{ doc.company_data.registrar_office_province }} + {{ doc.company_data.registration_number }} + {%- if doc.company_data.share_capital_amount %} + {{ format_float(doc.company_data.share_capital_amount) }} + {%- endif %} + {%- if doc.company_data.no_of_members %} + {{ doc.company_data.no_of_members.split("-")[0] }} + {%- endif %} + {%- if doc.company_data.liquidation_state %} + {{ doc.company_data.liquidation_state.split("-")[0] }} + {%- endif %} + + {%- endif %} + + + + {%- if doc.customer_data.customer_type == "Individual" %} + {{ doc.customer_data.fiscal_code }} + + {{ doc.customer_data.first_name }} + {{ doc.customer_data.last_name }} + + {%- else %} + {%- if doc.customer_data.is_public_administration %} + {{ doc.customer_data.fiscal_code }} + {%- else %} + + {{ doc.customer_address_data.country_code|upper or "IT" }} + {{ doc.tax_id | replace("IT","") }} + + {%- endif %} + + {{ doc.customer_name }} + + {%- endif %} + + {%- if doc.customer_address_data %} + + {{ render_address(doc.customer_address_data) }} + + {%- endif %} + + + + + + {{ doc.type_of_document }} + EUR + {{ doc.posting_date }} + {{ doc.unamended_name }} + {%- if doc.stamp_duty %} + + SI + {{ format_float(doc.stamp_duty) }} + + {%- endif %} + {{ format_float(doc.grand_total) }} + VENDITA + + {%- if doc.po_no %} + + {{ doc.po_no }} + {%- if doc.po_date %} + {{ doc.po_date }} + {%- endif %} + + {%- endif %} + {%- if doc.is_return and doc.return_against_unamended %} + + {{ doc.return_against_unamended }} + + {%- endif %} + {%- if doc.shipping_address_data %} + + + {{ render_address(doc.shipping_address_data) }} + + + {%- endif %} + + + {%- for item in doc.items %} + + {{ item.idx }} + + CODICE + {{ item.item_code }} + + {{ item.description or item.item_name }} + {{ format_float(item.qty) }} + {{ item.stock_uom }} + {{ format_float(item.price_list_rate or item.rate) }} + {{ render_discount_or_margin(item) }} + {{ format_float(item.amount) }} + {{ format_float(item.tax_rate) }} + {%- if item.tax_exemption_reason %} + {{ item.tax_exemption_reason.split("-")[0] }} + {%- endif %} + + {%- endfor %} + {%- for tax, data in doc.tax_data.items() %} + + {{ format_float(tax|flt) }} + {%- if data.tax_exemption_reason %} + {{ data.tax_exemption_reason.split("-")[0] }} + {%- endif %} + {{ format_float(data.taxable_amount) }} + {{ format_float(data.tax_amount) }} + {{ doc.vat_collectability.split("-")[0] }} + {%- if data.tax_exemption_law %} + {{ data.tax_exemption_law }} + {%- endif %} + + {%- endfor %} + + {%- if doc.payment_schedule %} + + {%- if payment_schedule|length > 1 %} + TP01 + {%- else %} + TP02 + {%- endif %} + {%- for payment_term in doc.payment_schedule %} + + {{ payment_term.mode_of_payment_code.split("-")[0] }} + {{ payment_term.due_date }} + {{ format_float(payment_term.payment_amount) }} + {%- if payment_term.bank_account_iban %}{{ payment_term.bank_account_iban }}{%- endif %} + + {%- endfor %} + + {%- endif %} + + diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 1931fdc581..b4ab26f9c7 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -5,33 +5,152 @@ 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 +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() + make_custom_fields() + setup_report() def make_custom_fields(update=True): - fiscal_code_field = dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='tax_id', print_hide=1) + 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': [ - fiscal_code_field, + 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='fiscal_code', print_hide=1, - options="\n".join(map(lambda x: x.decode('utf-8'), fiscal_regimes))) - ], - 'Customer': [ - fiscal_code_field, - 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) + fieldtype='Select', insert_after='sb_e_invoicing', print_hide=1, + options="\n".join(map(lambda x: x.decode('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: x.decode('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: x.decode('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: x.decode('utf-8'), tax_exemption_reasons))) + options="\n".join(map(lambda x: x.decode('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: x.decode('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 Account Name', + fieldtype='Data', insert_after='bank_account', print_hide=1, + fetch_from="bank_account.account_name", 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), + ], + "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: x.decode('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=1, + fetch_from="country.code") ] } 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 0000000000..69f7b464c9 --- /dev/null +++ b/erpnext/regional/italy/utils.py @@ -0,0 +1,251 @@ +import frappe, json, os +from frappe.utils import flt +from erpnext.controllers.taxes_and_totals import get_itemised_tax +from frappe import _ +from frappe.utils.file_manager import save_file, remove_file +from frappe.desk.form.load import get_attachments + + +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" + + tax_data = get_invoice_summary(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.items: + if item.tax_rate == 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 + + #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 = str(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 = str(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]]) + + return summary_data + +#Preflight for successful e-invoice export. +def sales_invoice_validate(doc): + #Validate company + 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") + + 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, "Customer") + + 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")) + + +#Ensure payment details are valid for e-invoice. +def sales_invoice_on_submit(doc): + #Validate payment details + 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")) + + prepare_and_attach_invoice(doc) + +def prepare_and_attach_invoice(doc): + progressive_name, progressive_number = get_progressive_name_and_number(doc) + + invoice = prepare_invoice(doc, progressive_number) + invoice_xml = frappe.render_template('erpnext/regional/italy/e-invoice.xml', context={"doc": invoice}, is_path=True) + + xml_filename = progressive_name + ".xml" + save_file(xml_filename, invoice_xml, dt=doc.doctype, dn=doc.name, is_private=True) + +#Delete e-invoice attachment on cancel. +def sales_invoice_on_cancel(doc): + for attachment in get_e_invoice_attachments(doc): + remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name) + +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.startswith(company_tax_id) and attachment.file_name.endswith(".xml"): + out.append(attachment) + + return out + +def validate_address(address_name, address_context): + pincode, city = frappe.db.get_value("Address", address_name, ["pincode", "city"]) + if not pincode: + frappe.throw(_("Please set pin code on %s Address" % address_context), title=_("E-Invoicing Information Missing")) + if not city: + frappe.throw(_("Please set city on %s Address" % address_context), 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): + 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 \ No newline at end of file diff --git a/erpnext/regional/report/electronic_invoice_register/__init__.py b/erpnext/regional/report/electronic_invoice_register/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js new file mode 100644 index 0000000000..67297f757c --- /dev/null +++ b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.js @@ -0,0 +1,53 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Electronic Invoice Register"] = { + "filters": [ + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "width": "80" + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today() + }, + { + "fieldname":"customer", + "label": __("Customer"), + "fieldtype": "Link", + "options": "Customer" + }, + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company") + }, + ], + "onload": function(reportview) { + reportview.page.add_inner_button(__("Export E-Invoices"), function() { + //TODO: refactor condition to disallow export if report has no data. + if (!reportview.data.length) { + frappe.msgprint(__("No data to export")); + return + } + + var w = window.open( + frappe.urllib.get_full_url( + "/api/method/erpnext.regional.italy.utils.export_invoices?" + + "filters=" + JSON.stringify(reportview.get_filter_values()) + ) + ); + if (!w) { + frappe.msgprint(__("Please enable pop-ups")); return; + } + }) + } +} diff --git a/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.json b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.json new file mode 100644 index 0000000000..5470c49c57 --- /dev/null +++ b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.json @@ -0,0 +1,21 @@ +{ + "add_total_row": 0, + "color": "green", + "creation": "2019-01-13 17:43:21.903589", + "disabled": 1, + "docstatus": 0, + "doctype": "Report", + "icon": "fa fa-file-code-o", + "idx": 0, + "is_standard": "Yes", + "modified": "2019-01-13 19:03:56.187748", + "modified_by": "Administrator", + "module": "Regional", + "name": "Electronic Invoice Register", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Invoice", + "report_name": "Electronic Invoice Register", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.py b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.py new file mode 100644 index 0000000000..376ba3ee47 --- /dev/null +++ b/erpnext/regional/report/electronic_invoice_register/electronic_invoice_register.py @@ -0,0 +1,8 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from erpnext.accounts.report.sales_register.sales_register import _execute + +def execute(filters=None): + return _execute(filters) diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index c3260ab906..1b08a228b6 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -46,7 +46,7 @@ class TestCompany(unittest.TestCase): def test_coa_based_on_country_template(self): countries = ["India", "Brazil", "United Arab Emirates", "Canada", "Germany", "France", - "Guatemala", "Indonesia", "Mexico", "Nicaragua", "Netherlands", "Singapore", + "Guatemala", "Indonesia", "Italy", "Mexico", "Nicaragua", "Netherlands", "Singapore", "Brazil", "Argentina", "Hungary", "Taiwan"] for country in countries: