blob: 85f744774f6b637d3442642afb421bb489807cfc [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
9
10
11def update_itemised_tax_data(doc):
12 if not doc.taxes: return
13
14 itemised_tax = get_itemised_tax(doc.taxes)
15
16 for row in doc.items:
17 tax_rate = 0.0
18 if itemised_tax.get(row.item_code):
19 tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()])
20
21 row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
22 row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
23 row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
24
25@frappe.whitelist()
26def export_invoices(filters=None):
27 saved_xmls = []
28
29 invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"])
30
31 for invoice in invoices:
32 attachments = get_e_invoice_attachments(invoice)
33 saved_xmls += [attachment.file_name for attachment in attachments]
34
35 zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
36
37 download_zip(saved_xmls, zip_filename)
38
39
40@frappe.whitelist()
41def prepare_invoice(invoice, progressive_number):
42 #set company information
43 company = frappe.get_doc("Company", invoice.company)
44
45 invoice.progressive_number = progressive_number
46 invoice.unamended_name = get_unamended_name(invoice)
47 invoice.company_data = company
48 company_address = frappe.get_doc("Address", invoice.company_address)
49 invoice.company_address_data = company_address
50
51 #Set invoice type
52 if invoice.is_return and invoice.return_against:
53 invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
54 invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
55 else:
56 invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
57
58 #set customer information
59 invoice.customer_data = frappe.get_doc("Customer", invoice.customer)
60 customer_address = frappe.get_doc("Address", invoice.customer_address)
61 invoice.customer_address_data = customer_address
62
63 if invoice.shipping_address_name:
64 invoice.shipping_address_data = frappe.get_doc("Address", invoice.shipping_address_name)
65
66 if invoice.customer_data.is_public_administration:
67 invoice.transmission_format_code = "FPA12"
68 else:
69 invoice.transmission_format_code = "FPR12"
70
Gaurav2670ad72019-02-19 10:17:17 +053071 invoice.e_invoice_items = [item for item in invoice.items]
72 tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes)
Gauravf1e28e02019-02-13 16:46:24 +053073 invoice.tax_data = tax_data
74
75 #Check if stamp duty (Bollo) of 2 EUR exists.
76 stamp_duty_charge_row = next((tax for tax in invoice.taxes if tax.charge_type == _("Actual") and tax.tax_amount == 2.0 ), None)
77 if stamp_duty_charge_row:
78 invoice.stamp_duty = stamp_duty_charge_row.tax_amount
79
Gaurav2670ad72019-02-19 10:17:17 +053080 for item in invoice.e_invoice_items:
81 if item.tax_rate == 0.0 and item.tax_amount == 0.0:
Gauravf1e28e02019-02-13 16:46:24 +053082 item.tax_exemption_reason = tax_data["0.0"]["tax_exemption_reason"]
83
84 return invoice
85
86def get_conditions(filters):
87 filters = json.loads(filters)
88
89 conditions = {"docstatus": 1}
90
91 if filters.get("company"): conditions["company"] = filters["company"]
92 if filters.get("customer"): conditions["customer"] = filters["customer"]
93
94 if filters.get("from_date"): conditions["posting_date"] = (">=", filters["from_date"])
95 if filters.get("to_date"): conditions["posting_date"] = ("<=", filters["to_date"])
96
97 if filters.get("from_date") and filters.get("to_date"):
98 conditions["posting_date"] = ("between", [filters.get("from_date"), filters.get("to_date")])
99
100 return conditions
101
102#TODO: Use function from frappe once PR #6853 is merged.
103def download_zip(files, output_filename):
104 from zipfile import ZipFile
105
106 input_files = [frappe.get_site_path('private', 'files', filename) for filename in files]
107 output_path = frappe.get_site_path('private', 'files', output_filename)
108
109 with ZipFile(output_path, 'w') as output_zip:
110 for input_file in input_files:
111 output_zip.write(input_file, arcname=os.path.basename(input_file))
112
113 with open(output_path, 'rb') as fileobj:
114 filedata = fileobj.read()
115
116 frappe.local.response.filename = output_filename
117 frappe.local.response.filecontent = filedata
118 frappe.local.response.type = "download"
119
120def get_invoice_summary(items, taxes):
121 summary_data = frappe._dict()
122 for tax in taxes:
123 #Include only VAT charges.
124 if tax.charge_type == "Actual":
125 continue
126
Gaurav2670ad72019-02-19 10:17:17 +0530127 #Charges to appear as items in the e-invoice.
128 if tax.charge_type in ["On Previous Row Total", "On Previous Row Amount"]:
129 reference_row = next((row for row in taxes if row.idx == int(tax.row_id or 0)), None)
130 if reference_row:
131 items.append(
132 frappe._dict(
133 idx=len(items)+1,
134 item_code=reference_row.description,
135 item_name=reference_row.description,
136 rate=reference_row.tax_amount,
137 qty=1.0,
138 amount=reference_row.tax_amount,
139 stock_uom=frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos"),
140 tax_rate=tax.rate,
141 tax_amount=(reference_row.tax_amount * tax.rate) / 100,
142 net_amount=reference_row.tax_amount,
143 taxable_amount=reference_row.tax_amount,
144 item_tax_rate="{}",
145 charges=True
146 )
147 )
148
Gauravf1e28e02019-02-13 16:46:24 +0530149 #Check item tax rates if tax rate is zero.
150 if tax.rate == 0:
151 for item in items:
152 item_tax_rate = json.loads(item.item_tax_rate)
153 if tax.account_head in item_tax_rate:
Gaurav2670ad72019-02-19 10:17:17 +0530154 key = cstr(item_tax_rate[tax.account_head])
Gauravf1e28e02019-02-13 16:46:24 +0530155 summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0, "tax_exemption_reason": "", "tax_exemption_law": ""})
156 summary_data[key]["tax_amount"] += item.tax_amount
157 summary_data[key]["taxable_amount"] += item.net_amount
158 if key == "0.0":
159 summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason
160 summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law
161
162 if summary_data == {}: #Implies that Zero VAT has not been set on any item.
163 summary_data.setdefault("0.0", {"tax_amount": 0.0, "taxable_amount": tax.total,
164 "tax_exemption_reason": tax.tax_exemption_reason, "tax_exemption_law": tax.tax_exemption_law})
165
166 else:
167 item_wise_tax_detail = json.loads(tax.item_wise_tax_detail)
168 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 +0530169 key = cstr(tax.rate)
Gauravf1e28e02019-02-13 16:46:24 +0530170 if not summary_data.get(key): summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0})
171 summary_data[key]["tax_amount"] += rate_item[1][1]
172 summary_data[key]["taxable_amount"] += sum([item.net_amount for item in items if item.item_code == rate_item[0]])
173
Gaurav2670ad72019-02-19 10:17:17 +0530174 for item in items:
175 key = cstr(tax.rate)
176 if item.get("charges"):
177 if not summary_data.get(key): summary_data.setdefault(key, {"taxable_amount": 0.0})
178 summary_data[key]["taxable_amount"] += item.taxable_amount
179
Gauravf1e28e02019-02-13 16:46:24 +0530180 return summary_data
181
182#Preflight for successful e-invoice export.
183def sales_invoice_validate(doc):
184 #Validate company
185 if not doc.company_address:
186 frappe.throw(_("Please set an Address on the Company '%s'" % doc.company), title=_("E-Invoicing Information Missing"))
187 else:
188 validate_address(doc.company_address, "Company")
189
190 if not doc.company_tax_id and not doc.company_fiscal_code:
191 frappe.throw(_("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), title=_("E-Invoicing Information Missing"))
192
193 #Validate customer details
194 customer_type, is_public_administration = frappe.db.get_value("Customer", doc.customer, ["customer_type", "is_public_administration"])
195 if customer_type == _("Individual"):
196 if not doc.customer_fiscal_code:
197 frappe.throw(_("Please set Fiscal Code for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
198 else:
199 if is_public_administration:
200 if not doc.customer_fiscal_code:
201 frappe.throw(_("Please set Fiscal Code for the public administration '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
202 else:
203 if not doc.tax_id:
204 frappe.throw(_("Please set Tax ID for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing"))
205
206 if not doc.customer_address:
207 frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing"))
208 else:
209 validate_address(doc.customer_address, "Customer")
210
211 if not len(doc.taxes):
212 frappe.throw(_("Please set at least one row in the Taxes and Charges Table"), title=_("E-Invoicing Information Missing"))
213 else:
214 for row in doc.taxes:
215 if row.rate == 0 and row.tax_amount == 0 and not row.tax_exemption_reason:
216 frappe.throw(_("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges".format(row.idx)),
217 title=_("E-Invoicing Information Missing"))
218
219
220#Ensure payment details are valid for e-invoice.
221def sales_invoice_on_submit(doc):
222 #Validate payment details
223 if not len(doc.payment_schedule):
224 frappe.throw(_("Please set the Payment Schedule"), title=_("E-Invoicing Information Missing"))
225 else:
226 for schedule in doc.payment_schedule:
227 if not schedule.mode_of_payment:
228 frappe.throw(_("Row {0}: Please set the Mode of Payment in Payment Schedule".format(schedule.idx)),
229 title=_("E-Invoicing Information Missing"))
Gaurav2670ad72019-02-19 10:17:17 +0530230 elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"):
231 frappe.throw(_("Row {0}: Please set the correct code on Mode of Payment {1}".format(schedule.idx, schedule.mode_of_payment)),
232 title=_("E-Invoicing Information Missing"))
Gauravf1e28e02019-02-13 16:46:24 +0530233
234 prepare_and_attach_invoice(doc)
235
236def prepare_and_attach_invoice(doc):
237 progressive_name, progressive_number = get_progressive_name_and_number(doc)
238
239 invoice = prepare_invoice(doc, progressive_number)
240 invoice_xml = frappe.render_template('erpnext/regional/italy/e-invoice.xml', context={"doc": invoice}, is_path=True)
241
242 xml_filename = progressive_name + ".xml"
243 save_file(xml_filename, invoice_xml, dt=doc.doctype, dn=doc.name, is_private=True)
244
245#Delete e-invoice attachment on cancel.
246def sales_invoice_on_cancel(doc):
247 for attachment in get_e_invoice_attachments(doc):
248 remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name)
249
250def get_e_invoice_attachments(invoice):
251 out = []
252 attachments = get_attachments(invoice.doctype, invoice.name)
253 company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id
254
255 for attachment in attachments:
256 if attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"):
257 out.append(attachment)
258
259 return out
260
261def validate_address(address_name, address_context):
262 pincode, city = frappe.db.get_value("Address", address_name, ["pincode", "city"])
263 if not pincode:
264 frappe.throw(_("Please set pin code on %s Address" % address_context), title=_("E-Invoicing Information Missing"))
265 if not city:
266 frappe.throw(_("Please set city on %s Address" % address_context), title=_("E-Invoicing Information Missing"))
267
268
269def get_unamended_name(doc):
270 attributes = ["naming_series", "amended_from"]
271 for attribute in attributes:
272 if not hasattr(doc, attribute):
273 return doc.name
274
275 if doc.amended_from:
276 return "-".join(doc.name.split("-")[:-1])
277 else:
278 return doc.name
279
280def get_progressive_name_and_number(doc):
281 company_tax_id = doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id
282 progressive_name = frappe.model.naming.make_autoname(company_tax_id + "_.#####")
283 progressive_number = progressive_name.split("_")[1]
284
285 return progressive_name, progressive_number