blob: e56c98b2715a9c52fe813b2891b5d2afcb18a52f [file] [log] [blame]
Gaurav2670ad72019-02-19 10:17:17 +05301from __future__ import unicode_literals
2
Gauravf1e28e02019-02-13 16:46:24 +05303import frappe, json, os
Gaurav2670ad72019-02-19 10:17:17 +05304from frappe.utils import flt, cstr
Gauravf1e28e02019-02-13 16:46:24 +05305from erpnext.controllers.taxes_and_totals import get_itemised_tax
6from frappe import _
7from frappe.utils.file_manager import save_file, remove_file
8from frappe.desk.form.load import get_attachments
Gaurav3f046132019-02-19 16:28:22 +05309from erpnext.regional.italy import state_codes
Gauravf1e28e02019-02-13 16:46:24 +053010
11
12def update_itemised_tax_data(doc):
13 if not doc.taxes: return
14
15 itemised_tax = get_itemised_tax(doc.taxes)
16
17 for row in doc.items:
18 tax_rate = 0.0
19 if itemised_tax.get(row.item_code):
20 tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()])
21
22 row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
23 row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
24 row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
25
26@frappe.whitelist()
27def export_invoices(filters=None):
28 saved_xmls = []
29
30 invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"])
31
32 for invoice in invoices:
33 attachments = get_e_invoice_attachments(invoice)
34 saved_xmls += [attachment.file_name for attachment in attachments]
35
36 zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
37
38 download_zip(saved_xmls, zip_filename)
39
40
41@frappe.whitelist()
42def prepare_invoice(invoice, progressive_number):
43 #set company information
44 company = frappe.get_doc("Company", invoice.company)
45
46 invoice.progressive_number = progressive_number
47 invoice.unamended_name = get_unamended_name(invoice)
48 invoice.company_data = company
49 company_address = frappe.get_doc("Address", invoice.company_address)
50 invoice.company_address_data = company_address
51
52 #Set invoice type
53 if invoice.is_return and invoice.return_against:
54 invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
55 invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
56 else:
57 invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
58
59 #set customer information
60 invoice.customer_data = frappe.get_doc("Customer", invoice.customer)
61 customer_address = frappe.get_doc("Address", invoice.customer_address)
62 invoice.customer_address_data = customer_address
63
64 if invoice.shipping_address_name:
65 invoice.shipping_address_data = frappe.get_doc("Address", invoice.shipping_address_name)
66
67 if invoice.customer_data.is_public_administration:
68 invoice.transmission_format_code = "FPA12"
69 else:
70 invoice.transmission_format_code = "FPR12"
71
Gaurav2670ad72019-02-19 10:17:17 +053072 invoice.e_invoice_items = [item for item in invoice.items]
73 tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes)
Gauravf1e28e02019-02-13 16:46:24 +053074 invoice.tax_data = tax_data
75
76 #Check if stamp duty (Bollo) of 2 EUR exists.
77 stamp_duty_charge_row = next((tax for tax in invoice.taxes if tax.charge_type == _("Actual") and tax.tax_amount == 2.0 ), None)
78 if stamp_duty_charge_row:
79 invoice.stamp_duty = stamp_duty_charge_row.tax_amount
80
Gaurav2670ad72019-02-19 10:17:17 +053081 for item in invoice.e_invoice_items:
82 if item.tax_rate == 0.0 and item.tax_amount == 0.0:
Gauravf1e28e02019-02-13 16:46:24 +053083 item.tax_exemption_reason = tax_data["0.0"]["tax_exemption_reason"]
84
85 return invoice
86
87def get_conditions(filters):
88 filters = json.loads(filters)
89
90 conditions = {"docstatus": 1}
91
92 if filters.get("company"): conditions["company"] = filters["company"]
93 if filters.get("customer"): conditions["customer"] = filters["customer"]
94
95 if filters.get("from_date"): conditions["posting_date"] = (">=", filters["from_date"])
96 if filters.get("to_date"): conditions["posting_date"] = ("<=", filters["to_date"])
97
98 if filters.get("from_date") and filters.get("to_date"):
99 conditions["posting_date"] = ("between", [filters.get("from_date"), filters.get("to_date")])
100
101 return conditions
102
103#TODO: Use function from frappe once PR #6853 is merged.
104def download_zip(files, output_filename):
105 from zipfile import ZipFile
106
107 input_files = [frappe.get_site_path('private', 'files', filename) for filename in files]
108 output_path = frappe.get_site_path('private', 'files', output_filename)
109
110 with ZipFile(output_path, 'w') as output_zip:
111 for input_file in input_files:
112 output_zip.write(input_file, arcname=os.path.basename(input_file))
113
114 with open(output_path, 'rb') as fileobj:
115 filedata = fileobj.read()
116
117 frappe.local.response.filename = output_filename
118 frappe.local.response.filecontent = filedata
119 frappe.local.response.type = "download"
120
121def get_invoice_summary(items, taxes):
122 summary_data = frappe._dict()
123 for tax in taxes:
124 #Include only VAT charges.
125 if tax.charge_type == "Actual":
126 continue
127
Gaurav2670ad72019-02-19 10:17:17 +0530128 #Charges to appear as items in the e-invoice.
129 if tax.charge_type in ["On Previous Row Total", "On Previous Row Amount"]:
130 reference_row = next((row for row in taxes if row.idx == int(tax.row_id or 0)), None)
131 if reference_row:
132 items.append(
133 frappe._dict(
134 idx=len(items)+1,
135 item_code=reference_row.description,
136 item_name=reference_row.description,
rohitwaghchaure9673d0d2019-03-24 12:19:58 +0530137 description=reference_row.description,
Gaurav2670ad72019-02-19 10:17:17 +0530138 rate=reference_row.tax_amount,
139 qty=1.0,
140 amount=reference_row.tax_amount,
141 stock_uom=frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos"),
142 tax_rate=tax.rate,
143 tax_amount=(reference_row.tax_amount * tax.rate) / 100,
144 net_amount=reference_row.tax_amount,
145 taxable_amount=reference_row.tax_amount,
146 item_tax_rate="{}",
147 charges=True
148 )
149 )
150
Gauravf1e28e02019-02-13 16:46:24 +0530151 #Check item tax rates if tax rate is zero.
152 if tax.rate == 0:
153 for item in items:
154 item_tax_rate = json.loads(item.item_tax_rate)
155 if tax.account_head in item_tax_rate:
Gaurav2670ad72019-02-19 10:17:17 +0530156 key = cstr(item_tax_rate[tax.account_head])
Gauravf1e28e02019-02-13 16:46:24 +0530157 summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0, "tax_exemption_reason": "", "tax_exemption_law": ""})
158 summary_data[key]["tax_amount"] += item.tax_amount
159 summary_data[key]["taxable_amount"] += item.net_amount
160 if key == "0.0":
161 summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason
162 summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law
163
164 if summary_data == {}: #Implies that Zero VAT has not been set on any item.
165 summary_data.setdefault("0.0", {"tax_amount": 0.0, "taxable_amount": tax.total,
166 "tax_exemption_reason": tax.tax_exemption_reason, "tax_exemption_law": tax.tax_exemption_law})
167
168 else:
169 item_wise_tax_detail = json.loads(tax.item_wise_tax_detail)
170 for rate_item in [tax_item for tax_item in item_wise_tax_detail.items() if tax_item[1][0] == tax.rate]:
Gaurav2670ad72019-02-19 10:17:17 +0530171 key = cstr(tax.rate)
Gauravf1e28e02019-02-13 16:46:24 +0530172 if not summary_data.get(key): summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0})
173 summary_data[key]["tax_amount"] += rate_item[1][1]
174 summary_data[key]["taxable_amount"] += sum([item.net_amount for item in items if item.item_code == rate_item[0]])
175
Gaurav2670ad72019-02-19 10:17:17 +0530176 for item in items:
177 key = cstr(tax.rate)
178 if item.get("charges"):
179 if not summary_data.get(key): summary_data.setdefault(key, {"taxable_amount": 0.0})
180 summary_data[key]["taxable_amount"] += item.taxable_amount
181
Gauravf1e28e02019-02-13 16:46:24 +0530182 return summary_data
183
184#Preflight for successful e-invoice export.
185def sales_invoice_validate(doc):
186 #Validate company
rohitwaghchaureef3f8642019-02-20 15:47:06 +0530187 if doc.doctype != 'Sales Invoice':
188 return
189
Gauravf1e28e02019-02-13 16:46:24 +0530190 if not doc.company_address:
191 frappe.throw(_("Please set an Address on the Company '%s'" % doc.company), title=_("E-Invoicing Information Missing"))
192 else:
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530193 validate_address(doc.company_address)
Gauravf1e28e02019-02-13 16:46:24 +0530194
Rohit Waghchaure0f98cb82019-02-26 15:01:30 +0530195 company_fiscal_regime = frappe.get_cached_value("Company", doc.company, 'fiscal_regime')
196 if not company_fiscal_regime:
197 frappe.throw(_("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}")
198 .format(doc.company))
199 else:
200 doc.company_fiscal_regime = company_fiscal_regime
201
Gauravf1e28e02019-02-13 16:46:24 +0530202 if not doc.company_tax_id and not doc.company_fiscal_code:
203 frappe.throw(_("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), title=_("E-Invoicing Information Missing"))
204
205 #Validate customer details
206 customer_type, is_public_administration = frappe.db.get_value("Customer", doc.customer, ["customer_type", "is_public_administration"])
207 if customer_type == _("Individual"):
208 if not doc.customer_fiscal_code:
209 frappe.throw(_("Please set Fiscal Code for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
210 else:
211 if is_public_administration:
212 if not doc.customer_fiscal_code:
213 frappe.throw(_("Please set Fiscal Code for the public administration '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
214 else:
215 if not doc.tax_id:
216 frappe.throw(_("Please set Tax ID for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
217
218 if not doc.customer_address:
219 frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing"))
220 else:
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530221 validate_address(doc.customer_address)
Gauravf1e28e02019-02-13 16:46:24 +0530222
223 if not len(doc.taxes):
224 frappe.throw(_("Please set at least one row in the Taxes and Charges Table"), title=_("E-Invoicing Information Missing"))
225 else:
226 for row in doc.taxes:
227 if row.rate == 0 and row.tax_amount == 0 and not row.tax_exemption_reason:
228 frappe.throw(_("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges".format(row.idx)),
229 title=_("E-Invoicing Information Missing"))
230
Rohit Waghchaure0f98cb82019-02-26 15:01:30 +0530231 for schedule in doc.payment_schedule:
232 if schedule.mode_of_payment and not schedule.mode_of_payment_code:
233 schedule.mode_of_payment_code = frappe.get_cached_value('Mode of Payment',
234 schedule.mode_of_payment, 'mode_of_payment_code')
Gauravf1e28e02019-02-13 16:46:24 +0530235
236#Ensure payment details are valid for e-invoice.
rohitwaghchaurec18e9252019-02-20 17:13:15 +0530237def sales_invoice_on_submit(doc, method):
Gauravf1e28e02019-02-13 16:46:24 +0530238 #Validate payment details
rohitwaghchaurec18e9252019-02-20 17:13:15 +0530239 if get_company_country(doc.company) not in ['Italy',
240 'Italia', 'Italian Republic', 'Repubblica Italiana']:
241 return
242
Gauravf1e28e02019-02-13 16:46:24 +0530243 if not len(doc.payment_schedule):
244 frappe.throw(_("Please set the Payment Schedule"), title=_("E-Invoicing Information Missing"))
245 else:
246 for schedule in doc.payment_schedule:
247 if not schedule.mode_of_payment:
248 frappe.throw(_("Row {0}: Please set the Mode of Payment in Payment Schedule".format(schedule.idx)),
249 title=_("E-Invoicing Information Missing"))
Gaurav2670ad72019-02-19 10:17:17 +0530250 elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"):
251 frappe.throw(_("Row {0}: Please set the correct code on Mode of Payment {1}".format(schedule.idx, schedule.mode_of_payment)),
252 title=_("E-Invoicing Information Missing"))
Gauravf1e28e02019-02-13 16:46:24 +0530253
254 prepare_and_attach_invoice(doc)
255
Gauravb30a9b12019-03-01 12:33:19 +0530256def prepare_and_attach_invoice(doc, replace=False):
257 progressive_name, progressive_number = get_progressive_name_and_number(doc, replace)
Gauravf1e28e02019-02-13 16:46:24 +0530258
259 invoice = prepare_invoice(doc, progressive_number)
260 invoice_xml = frappe.render_template('erpnext/regional/italy/e-invoice.xml', context={"doc": invoice}, is_path=True)
Rohit Waghchaure0f98cb82019-02-26 15:01:30 +0530261 invoice_xml = invoice_xml.replace("&", "&amp;")
Gauravf1e28e02019-02-13 16:46:24 +0530262
263 xml_filename = progressive_name + ".xml"
Gauravb30a9b12019-03-01 12:33:19 +0530264 return save_file(xml_filename, invoice_xml, dt=doc.doctype, dn=doc.name, is_private=True)
265
266@frappe.whitelist()
267def generate_single_invoice(docname):
268 doc = frappe.get_doc("Sales Invoice", docname)
269
270 e_invoice = prepare_and_attach_invoice(doc, True)
271
272 content = None
273 with open(frappe.get_site_path('private', 'files', e_invoice.file_name), "r") as f:
274 content = f.read()
275
276 frappe.local.response.filename = e_invoice.file_name
277 frappe.local.response.filecontent = content
278 frappe.local.response.type = "download"
Gauravf1e28e02019-02-13 16:46:24 +0530279
280#Delete e-invoice attachment on cancel.
rohitwaghchaurec18e9252019-02-20 17:13:15 +0530281def sales_invoice_on_cancel(doc, method):
282 if get_company_country(doc.company) not in ['Italy',
283 'Italia', 'Italian Republic', 'Repubblica Italiana']:
284 return
285
Gauravf1e28e02019-02-13 16:46:24 +0530286 for attachment in get_e_invoice_attachments(doc):
287 remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name)
288
rohitwaghchaurec18e9252019-02-20 17:13:15 +0530289def get_company_country(company):
290 return frappe.get_cached_value('Company', company, 'country')
291
Gauravf1e28e02019-02-13 16:46:24 +0530292def get_e_invoice_attachments(invoice):
293 out = []
294 attachments = get_attachments(invoice.doctype, invoice.name)
295 company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id
296
297 for attachment in attachments:
Rohit Waghchaure0f98cb82019-02-26 15:01:30 +0530298 if attachment.file_name and attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"):
Gauravf1e28e02019-02-13 16:46:24 +0530299 out.append(attachment)
300
301 return out
302
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530303def validate_address(address_name):
304 fields = ["pincode", "city", "country_code"]
305 data = frappe.get_cached_value("Address", address_name, fields, as_dict=1) or {}
Gauravf1e28e02019-02-13 16:46:24 +0530306
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530307 for field in fields:
308 if not data.get(field):
309 frappe.throw(_("Please set {0} for address {1}".format(field.replace('-',''), address_name)),
310 title=_("E-Invoicing Information Missing"))
Gauravf1e28e02019-02-13 16:46:24 +0530311
312def get_unamended_name(doc):
313 attributes = ["naming_series", "amended_from"]
314 for attribute in attributes:
315 if not hasattr(doc, attribute):
316 return doc.name
317
318 if doc.amended_from:
319 return "-".join(doc.name.split("-")[:-1])
320 else:
321 return doc.name
322
Gauravb30a9b12019-03-01 12:33:19 +0530323def get_progressive_name_and_number(doc, replace=False):
324 if replace:
325 for attachment in get_e_invoice_attachments(doc):
326 remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name)
327 filename = attachment.file_name.split(".xml")[0]
328 return filename, filename.split("_")[1]
329
Gauravf1e28e02019-02-13 16:46:24 +0530330 company_tax_id = doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id
331 progressive_name = frappe.model.naming.make_autoname(company_tax_id + "_.#####")
332 progressive_number = progressive_name.split("_")[1]
333
Gaurav3f046132019-02-19 16:28:22 +0530334 return progressive_name, progressive_number
335
Gaurav Naik3bf0acb2019-02-20 12:08:53 +0530336def set_state_code(doc, method):
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530337 if doc.get('country_code'):
338 doc.country_code = doc.country_code.upper()
339
deepeshgarg0071915e152019-02-21 17:55:57 +0530340 if not doc.get('state'):
341 return
342
Gaurav3f046132019-02-19 16:28:22 +0530343 if not (hasattr(doc, "state_code") and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"]):
344 return
345
346 state_codes_lower = {key.lower():value for key,value in state_codes.items()}
Rohit Waghchaure4ef924d2019-03-01 16:24:54 +0530347
348 state = doc.get('state','').lower()
349 if state_codes_lower.get(state):
350 doc.state_code = state_codes_lower.get(state)