blob: 12f5762f441a99fcefacd663c06906b1d626e9de [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 _
Saurabhacf83792019-02-24 08:57:16 +05307from frappe.core.doctype.file.file import remove_file
Rohit Waghchaure58f489f2019-04-01 17:50:31 +05308from six import string_types
Gauravf1e28e02019-02-13 16:46:24 +05309from frappe.desk.form.load import get_attachments
Gaurav3f046132019-02-19 16:28:22 +053010from erpnext.regional.italy import state_codes
Gauravf1e28e02019-02-13 16:46:24 +053011
12
13def update_itemised_tax_data(doc):
14 if not doc.taxes: return
15
16 itemised_tax = get_itemised_tax(doc.taxes)
17
18 for row in doc.items:
19 tax_rate = 0.0
20 if itemised_tax.get(row.item_code):
21 tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()])
22
23 row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
24 row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
25 row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
26
27@frappe.whitelist()
28def export_invoices(filters=None):
29 saved_xmls = []
30
31 invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"])
32
33 for invoice in invoices:
34 attachments = get_e_invoice_attachments(invoice)
35 saved_xmls += [attachment.file_name for attachment in attachments]
36
37 zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
38
39 download_zip(saved_xmls, zip_filename)
40
41
42@frappe.whitelist()
43def prepare_invoice(invoice, progressive_number):
44 #set company information
45 company = frappe.get_doc("Company", invoice.company)
46
47 invoice.progressive_number = progressive_number
48 invoice.unamended_name = get_unamended_name(invoice)
49 invoice.company_data = company
50 company_address = frappe.get_doc("Address", invoice.company_address)
51 invoice.company_address_data = company_address
52
53 #Set invoice type
54 if invoice.is_return and invoice.return_against:
55 invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
56 invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
57 else:
58 invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
59
60 #set customer information
61 invoice.customer_data = frappe.get_doc("Customer", invoice.customer)
62 customer_address = frappe.get_doc("Address", invoice.customer_address)
63 invoice.customer_address_data = customer_address
64
65 if invoice.shipping_address_name:
66 invoice.shipping_address_data = frappe.get_doc("Address", invoice.shipping_address_name)
67
68 if invoice.customer_data.is_public_administration:
69 invoice.transmission_format_code = "FPA12"
70 else:
71 invoice.transmission_format_code = "FPR12"
72
Gaurav2670ad72019-02-19 10:17:17 +053073 invoice.e_invoice_items = [item for item in invoice.items]
74 tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes)
Gauravf1e28e02019-02-13 16:46:24 +053075 invoice.tax_data = tax_data
76
77 #Check if stamp duty (Bollo) of 2 EUR exists.
Rohit Waghchaure68a14562019-05-22 13:17:46 +053078 stamp_duty_charge_row = next((tax for tax in invoice.taxes if tax.charge_type == "Actual" and tax.tax_amount == 2.0 ), None)
Gauravf1e28e02019-02-13 16:46:24 +053079 if stamp_duty_charge_row:
80 invoice.stamp_duty = stamp_duty_charge_row.tax_amount
81
Gaurav2670ad72019-02-19 10:17:17 +053082 for item in invoice.e_invoice_items:
Nabin Hait34c551d2019-07-03 10:34:31 +053083 if item.tax_rate == 0.0 and item.tax_amount == 0.0 and tax_data.get("0.0"):
Gauravf1e28e02019-02-13 16:46:24 +053084 item.tax_exemption_reason = tax_data["0.0"]["tax_exemption_reason"]
85
Rohit Waghchaure1b7059b2019-03-12 17:44:29 +053086 customer_po_data = {}
87 for d in invoice.e_invoice_items:
88 if (d.customer_po_no and d.customer_po_date
89 and d.customer_po_no not in customer_po_data):
90 customer_po_data[d.customer_po_no] = d.customer_po_date
91
92 invoice.customer_po_data = customer_po_data
93
Gauravf1e28e02019-02-13 16:46:24 +053094 return invoice
95
96def get_conditions(filters):
97 filters = json.loads(filters)
98
99 conditions = {"docstatus": 1}
100
101 if filters.get("company"): conditions["company"] = filters["company"]
102 if filters.get("customer"): conditions["customer"] = filters["customer"]
103
104 if filters.get("from_date"): conditions["posting_date"] = (">=", filters["from_date"])
105 if filters.get("to_date"): conditions["posting_date"] = ("<=", filters["to_date"])
106
107 if filters.get("from_date") and filters.get("to_date"):
108 conditions["posting_date"] = ("between", [filters.get("from_date"), filters.get("to_date")])
109
110 return conditions
111
112#TODO: Use function from frappe once PR #6853 is merged.
113def download_zip(files, output_filename):
114 from zipfile import ZipFile
115
116 input_files = [frappe.get_site_path('private', 'files', filename) for filename in files]
117 output_path = frappe.get_site_path('private', 'files', output_filename)
118
119 with ZipFile(output_path, 'w') as output_zip:
120 for input_file in input_files:
121 output_zip.write(input_file, arcname=os.path.basename(input_file))
122
123 with open(output_path, 'rb') as fileobj:
124 filedata = fileobj.read()
125
126 frappe.local.response.filename = output_filename
127 frappe.local.response.filecontent = filedata
128 frappe.local.response.type = "download"
129
130def get_invoice_summary(items, taxes):
131 summary_data = frappe._dict()
132 for tax in taxes:
133 #Include only VAT charges.
134 if tax.charge_type == "Actual":
135 continue
136
Gaurav2670ad72019-02-19 10:17:17 +0530137 #Charges to appear as items in the e-invoice.
138 if tax.charge_type in ["On Previous Row Total", "On Previous Row Amount"]:
139 reference_row = next((row for row in taxes if row.idx == int(tax.row_id or 0)), None)
140 if reference_row:
141 items.append(
142 frappe._dict(
143 idx=len(items)+1,
144 item_code=reference_row.description,
145 item_name=reference_row.description,
rohitwaghchaure9673d0d2019-03-24 12:19:58 +0530146 description=reference_row.description,
Gaurav2670ad72019-02-19 10:17:17 +0530147 rate=reference_row.tax_amount,
148 qty=1.0,
149 amount=reference_row.tax_amount,
150 stock_uom=frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos"),
151 tax_rate=tax.rate,
152 tax_amount=(reference_row.tax_amount * tax.rate) / 100,
153 net_amount=reference_row.tax_amount,
rohitwaghchaurecdcff6c2019-09-09 10:15:01 +0530154 taxable_amount=(reference_row.tax_amount if tax.charge_type == 'On Previous Row Amount'
155 else reference_row.total),
Rohit Waghchaure58f489f2019-04-01 17:50:31 +0530156 item_tax_rate={tax.account_head: tax.rate},
Gaurav2670ad72019-02-19 10:17:17 +0530157 charges=True
158 )
159 )
160
Gauravf1e28e02019-02-13 16:46:24 +0530161 #Check item tax rates if tax rate is zero.
162 if tax.rate == 0:
163 for item in items:
Rohit Waghchaure58f489f2019-04-01 17:50:31 +0530164 item_tax_rate = item.item_tax_rate
165 if isinstance(item.item_tax_rate, string_types):
166 item_tax_rate = json.loads(item.item_tax_rate)
167
168 if item_tax_rate and tax.account_head in item_tax_rate:
Gaurav2670ad72019-02-19 10:17:17 +0530169 key = cstr(item_tax_rate[tax.account_head])
Rohit Waghchaure58f489f2019-04-01 17:50:31 +0530170 if key not in summary_data:
171 summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0,
172 "tax_exemption_reason": "", "tax_exemption_law": ""})
173
Gauravf1e28e02019-02-13 16:46:24 +0530174 summary_data[key]["tax_amount"] += item.tax_amount
175 summary_data[key]["taxable_amount"] += item.net_amount
176 if key == "0.0":
177 summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason
178 summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law
179
180 if summary_data == {}: #Implies that Zero VAT has not been set on any item.
181 summary_data.setdefault("0.0", {"tax_amount": 0.0, "taxable_amount": tax.total,
182 "tax_exemption_reason": tax.tax_exemption_reason, "tax_exemption_law": tax.tax_exemption_law})
183
184 else:
185 item_wise_tax_detail = json.loads(tax.item_wise_tax_detail)
186 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 +0530187 key = cstr(tax.rate)
Gauravf1e28e02019-02-13 16:46:24 +0530188 if not summary_data.get(key): summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0})
189 summary_data[key]["tax_amount"] += rate_item[1][1]
190 summary_data[key]["taxable_amount"] += sum([item.net_amount for item in items if item.item_code == rate_item[0]])
191
Gaurav2670ad72019-02-19 10:17:17 +0530192 for item in items:
193 key = cstr(tax.rate)
194 if item.get("charges"):
195 if not summary_data.get(key): summary_data.setdefault(key, {"taxable_amount": 0.0})
196 summary_data[key]["taxable_amount"] += item.taxable_amount
197
Gauravf1e28e02019-02-13 16:46:24 +0530198 return summary_data
199
200#Preflight for successful e-invoice export.
201def sales_invoice_validate(doc):
202 #Validate company
rohitwaghchaureef3f8642019-02-20 15:47:06 +0530203 if doc.doctype != 'Sales Invoice':
204 return
205
Gauravf1e28e02019-02-13 16:46:24 +0530206 if not doc.company_address:
207 frappe.throw(_("Please set an Address on the Company '%s'" % doc.company), title=_("E-Invoicing Information Missing"))
208 else:
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530209 validate_address(doc.company_address)
Gauravf1e28e02019-02-13 16:46:24 +0530210
Rohit Waghchaure0f98cb82019-02-26 15:01:30 +0530211 company_fiscal_regime = frappe.get_cached_value("Company", doc.company, 'fiscal_regime')
212 if not company_fiscal_regime:
213 frappe.throw(_("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}")
214 .format(doc.company))
215 else:
216 doc.company_fiscal_regime = company_fiscal_regime
217
Gauravbd80fd12019-03-28 12:18:03 +0530218 doc.company_tax_id = frappe.get_cached_value("Company", doc.company, 'tax_id')
219 doc.company_fiscal_code = frappe.get_cached_value("Company", doc.company, 'fiscal_code')
Gauravf1e28e02019-02-13 16:46:24 +0530220 if not doc.company_tax_id and not doc.company_fiscal_code:
221 frappe.throw(_("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), title=_("E-Invoicing Information Missing"))
222
223 #Validate customer details
Gaurav010acf72019-03-28 10:42:23 +0530224 customer = frappe.get_doc("Customer", doc.customer)
225
Rohit Waghchaure68a14562019-05-22 13:17:46 +0530226 if customer.customer_type == "Individual":
Gaurav010acf72019-03-28 10:42:23 +0530227 doc.customer_fiscal_code = customer.fiscal_code
Gauravf1e28e02019-02-13 16:46:24 +0530228 if not doc.customer_fiscal_code:
229 frappe.throw(_("Please set Fiscal Code for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
230 else:
Gaurav010acf72019-03-28 10:42:23 +0530231 if customer.is_public_administration:
232 doc.customer_fiscal_code = customer.fiscal_code
Gauravf1e28e02019-02-13 16:46:24 +0530233 if not doc.customer_fiscal_code:
234 frappe.throw(_("Please set Fiscal Code for the public administration '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
235 else:
Gaurav010acf72019-03-28 10:42:23 +0530236 doc.tax_id = customer.tax_id
Gauravf1e28e02019-02-13 16:46:24 +0530237 if not doc.tax_id:
238 frappe.throw(_("Please set Tax ID for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
239
240 if not doc.customer_address:
241 frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing"))
242 else:
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530243 validate_address(doc.customer_address)
Gauravf1e28e02019-02-13 16:46:24 +0530244
245 if not len(doc.taxes):
246 frappe.throw(_("Please set at least one row in the Taxes and Charges Table"), title=_("E-Invoicing Information Missing"))
247 else:
248 for row in doc.taxes:
249 if row.rate == 0 and row.tax_amount == 0 and not row.tax_exemption_reason:
250 frappe.throw(_("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges".format(row.idx)),
251 title=_("E-Invoicing Information Missing"))
252
Rohit Waghchaure0f98cb82019-02-26 15:01:30 +0530253 for schedule in doc.payment_schedule:
254 if schedule.mode_of_payment and not schedule.mode_of_payment_code:
255 schedule.mode_of_payment_code = frappe.get_cached_value('Mode of Payment',
256 schedule.mode_of_payment, 'mode_of_payment_code')
Gauravf1e28e02019-02-13 16:46:24 +0530257
258#Ensure payment details are valid for e-invoice.
rohitwaghchaurec18e9252019-02-20 17:13:15 +0530259def sales_invoice_on_submit(doc, method):
Gauravf1e28e02019-02-13 16:46:24 +0530260 #Validate payment details
rohitwaghchaurec18e9252019-02-20 17:13:15 +0530261 if get_company_country(doc.company) not in ['Italy',
262 'Italia', 'Italian Republic', 'Repubblica Italiana']:
263 return
264
Gauravf1e28e02019-02-13 16:46:24 +0530265 if not len(doc.payment_schedule):
266 frappe.throw(_("Please set the Payment Schedule"), title=_("E-Invoicing Information Missing"))
267 else:
268 for schedule in doc.payment_schedule:
269 if not schedule.mode_of_payment:
270 frappe.throw(_("Row {0}: Please set the Mode of Payment in Payment Schedule".format(schedule.idx)),
271 title=_("E-Invoicing Information Missing"))
Gaurav2670ad72019-02-19 10:17:17 +0530272 elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"):
273 frappe.throw(_("Row {0}: Please set the correct code on Mode of Payment {1}".format(schedule.idx, schedule.mode_of_payment)),
274 title=_("E-Invoicing Information Missing"))
Gauravf1e28e02019-02-13 16:46:24 +0530275
276 prepare_and_attach_invoice(doc)
277
Gauravb30a9b12019-03-01 12:33:19 +0530278def prepare_and_attach_invoice(doc, replace=False):
279 progressive_name, progressive_number = get_progressive_name_and_number(doc, replace)
Gauravf1e28e02019-02-13 16:46:24 +0530280
281 invoice = prepare_invoice(doc, progressive_number)
rohitwaghchaurecdcff6c2019-09-09 10:15:01 +0530282 item_meta = frappe.get_meta("Sales Invoice Item")
283
284 invoice_xml = frappe.render_template('erpnext/regional/italy/e-invoice.xml',
285 context={"doc": invoice, "item_meta": item_meta}, is_path=True)
286
Rohit Waghchaure0f98cb82019-02-26 15:01:30 +0530287 invoice_xml = invoice_xml.replace("&", "&amp;")
Gauravf1e28e02019-02-13 16:46:24 +0530288
289 xml_filename = progressive_name + ".xml"
Saurabhacf83792019-02-24 08:57:16 +0530290
291 _file = frappe.get_doc({
292 "doctype": "File",
293 "file_name": xml_filename,
294 "attached_to_doctype": doc.doctype,
295 "attached_to_name": doc.name,
296 "is_private": True,
297 "content": invoice_xml
298 })
299 _file.save()
Aditya Hase234d3572019-05-01 11:48:10 +0530300 return _file
Gauravb30a9b12019-03-01 12:33:19 +0530301
302@frappe.whitelist()
303def generate_single_invoice(docname):
304 doc = frappe.get_doc("Sales Invoice", docname)
305
Rohit Waghchaure1b7059b2019-03-12 17:44:29 +0530306
Gauravb30a9b12019-03-01 12:33:19 +0530307 e_invoice = prepare_and_attach_invoice(doc, True)
308
Rohit Waghchaure1b7059b2019-03-12 17:44:29 +0530309 return e_invoice.file_name
310
311@frappe.whitelist()
312def download_e_invoice_file(file_name):
Gauravb30a9b12019-03-01 12:33:19 +0530313 content = None
Rohit Waghchaure1b7059b2019-03-12 17:44:29 +0530314 with open(frappe.get_site_path('private', 'files', file_name), "r") as f:
Gauravb30a9b12019-03-01 12:33:19 +0530315 content = f.read()
316
Rohit Waghchaure1b7059b2019-03-12 17:44:29 +0530317 frappe.local.response.filename = file_name
Gauravb30a9b12019-03-01 12:33:19 +0530318 frappe.local.response.filecontent = content
319 frappe.local.response.type = "download"
Gauravf1e28e02019-02-13 16:46:24 +0530320
321#Delete e-invoice attachment on cancel.
rohitwaghchaurec18e9252019-02-20 17:13:15 +0530322def sales_invoice_on_cancel(doc, method):
323 if get_company_country(doc.company) not in ['Italy',
324 'Italia', 'Italian Republic', 'Repubblica Italiana']:
325 return
326
Gauravf1e28e02019-02-13 16:46:24 +0530327 for attachment in get_e_invoice_attachments(doc):
328 remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name)
329
rohitwaghchaurec18e9252019-02-20 17:13:15 +0530330def get_company_country(company):
331 return frappe.get_cached_value('Company', company, 'country')
332
Gauravf1e28e02019-02-13 16:46:24 +0530333def get_e_invoice_attachments(invoice):
Mangesh-Khairnar359a73e2019-07-04 11:37:20 +0530334 if not invoice.company_tax_id:
335 return []
336
Gauravf1e28e02019-02-13 16:46:24 +0530337 out = []
338 attachments = get_attachments(invoice.doctype, invoice.name)
339 company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id
340
341 for attachment in attachments:
Rohit Waghchaure0f98cb82019-02-26 15:01:30 +0530342 if attachment.file_name and attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"):
Gauravf1e28e02019-02-13 16:46:24 +0530343 out.append(attachment)
344
345 return out
346
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530347def validate_address(address_name):
348 fields = ["pincode", "city", "country_code"]
349 data = frappe.get_cached_value("Address", address_name, fields, as_dict=1) or {}
Gauravf1e28e02019-02-13 16:46:24 +0530350
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530351 for field in fields:
352 if not data.get(field):
353 frappe.throw(_("Please set {0} for address {1}".format(field.replace('-',''), address_name)),
354 title=_("E-Invoicing Information Missing"))
Gauravf1e28e02019-02-13 16:46:24 +0530355
356def get_unamended_name(doc):
357 attributes = ["naming_series", "amended_from"]
358 for attribute in attributes:
359 if not hasattr(doc, attribute):
360 return doc.name
361
362 if doc.amended_from:
363 return "-".join(doc.name.split("-")[:-1])
364 else:
365 return doc.name
366
Gauravb30a9b12019-03-01 12:33:19 +0530367def get_progressive_name_and_number(doc, replace=False):
368 if replace:
369 for attachment in get_e_invoice_attachments(doc):
370 remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name)
371 filename = attachment.file_name.split(".xml")[0]
372 return filename, filename.split("_")[1]
373
Gauravf1e28e02019-02-13 16:46:24 +0530374 company_tax_id = doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id
375 progressive_name = frappe.model.naming.make_autoname(company_tax_id + "_.#####")
376 progressive_number = progressive_name.split("_")[1]
377
Gaurav3f046132019-02-19 16:28:22 +0530378 return progressive_name, progressive_number
379
Gaurav Naik3bf0acb2019-02-20 12:08:53 +0530380def set_state_code(doc, method):
Rohit Waghchaure74cfe572019-02-26 20:08:26 +0530381 if doc.get('country_code'):
382 doc.country_code = doc.country_code.upper()
383
deepeshgarg0071915e152019-02-21 17:55:57 +0530384 if not doc.get('state'):
385 return
386
Gaurav3f046132019-02-19 16:28:22 +0530387 if not (hasattr(doc, "state_code") and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"]):
388 return
389
390 state_codes_lower = {key.lower():value for key,value in state_codes.items()}
Rohit Waghchaure4ef924d2019-03-01 16:24:54 +0530391
392 state = doc.get('state','').lower()
393 if state_codes_lower.get(state):
394 doc.state_code = state_codes_lower.get(state)