From f941e2334bcd40014aa28bfbbae611160f9c381d Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Mon, 7 Aug 2023 10:45:02 +0530 Subject: [PATCH] feat: e-Waybill for purchase invoice (#350) Co-authored-by: Smit Vora Co-authored-by: ljain112 --- .../client_scripts/e_waybill_actions.js | 87 ++++-- .../client_scripts/purchase_invoice.js | 21 +- .../gst_india/constants/custom_fields.py | 60 ++-- .../gst_india/constants/e_waybill.py | 19 ++ .../doctype/gst_settings/gst_settings.json | 8 + .../gst_india/overrides/purchase_invoice.py | 52 +++- .../gst_india/overrides/sales_invoice.py | 7 +- .../overrides/test_transaction_data.py | 2 +- india_compliance/gst_india/utils/e_invoice.py | 2 +- india_compliance/gst_india/utils/e_waybill.py | 280 +++++++++++------- .../gst_india/utils/test_e_waybill.py | 10 +- .../gst_india/utils/transaction_data.py | 21 +- india_compliance/hooks.py | 5 +- india_compliance/patches.txt | 2 +- 14 files changed, 382 insertions(+), 194 deletions(-) diff --git a/india_compliance/gst_india/client_scripts/e_waybill_actions.js b/india_compliance/gst_india/client_scripts/e_waybill_actions.js index dba33b918..ad86d5fba 100644 --- a/india_compliance/gst_india/client_scripts/e_waybill_actions.js +++ b/india_compliance/gst_india/client_scripts/e_waybill_actions.js @@ -1,9 +1,5 @@ function setup_e_waybill_actions(doctype) { - if ( - !gst_settings.enable_e_waybill || - (doctype == "Delivery Note" && !gst_settings.enable_e_waybill_from_dn) - ) - return; + if (!gst_settings.enable_e_waybill) return; frappe.ui.form.on(doctype, { mode_of_transport(frm) { @@ -31,8 +27,7 @@ function setup_e_waybill_actions(doctype) { if ( frm.doc.docstatus != 1 || frm.is_dirty() || - !is_e_waybill_applicable(frm) || - (frm.doctype === "Delivery Note" && !frm.doc.customer_address) + !is_e_waybill_applicable(frm) ) return; @@ -132,11 +127,9 @@ function setup_e_waybill_actions(doctype) { }, async on_submit(frm) { if ( - // threshold is only met for Sales Invoice + frm.doctype != "Sales Invoice" || !has_e_waybill_threshold_met(frm) || frm.doc.ewaybill || - frm.doc.is_return || - frm.doc.is_debit_note || !india_compliance.is_api_enabled() || !gst_settings.auto_generate_e_waybill || is_e_invoice_applicable(frm) || @@ -672,11 +665,10 @@ function show_update_transporter_dialog(frm) { } async function show_extend_validity_dialog(frm) { - const shipping_address = await frappe.db.get_doc( + const destination_address = await frappe.db.get_doc( "Address", - frm.doc.shipping_address_name || frm.doc.customer_address + get_destination_address_name(frm) ); - const is_in_movement = "eval: doc.consignment_status === 'In Movement'"; const is_in_transit = "eval: doc.consignment_status === 'In Transit'"; @@ -749,7 +741,7 @@ async function show_extend_validity_dialog(frm) { label: "Address Line1", fieldname: "address_line1", fieldtype: "Data", - default: shipping_address.address_line1, + default: destination_address.address_line1, depends_on: is_in_transit, mandatory_depends_on: is_in_transit, }, @@ -757,7 +749,7 @@ async function show_extend_validity_dialog(frm) { label: "Address Line2", fieldname: "address_line2", fieldtype: "Data", - default: shipping_address.address_line2, + default: destination_address.address_line2, depends_on: is_in_transit, mandatory_depends_on: is_in_transit, }, @@ -765,7 +757,7 @@ async function show_extend_validity_dialog(frm) { label: "Address Line3", fieldname: "address_line3", fieldtype: "Data", - default: shipping_address.city, + default: destination_address.city, depends_on: is_in_transit, mandatory_depends_on: is_in_transit, }, @@ -777,14 +769,14 @@ async function show_extend_validity_dialog(frm) { fieldname: "current_place", fieldtype: "Data", reqd: 1, - default: shipping_address.city, + default: destination_address.city, }, { label: "Current Pincode", fieldname: "current_pincode", fieldtype: "Data", reqd: 1, - default: shipping_address.pincode, + default: destination_address.pincode, }, { label: "Current State", @@ -792,7 +784,7 @@ async function show_extend_validity_dialog(frm) { fieldtype: "Autocomplete", options: frappe.boot.india_state_options.join("\n"), reqd: 1, - default: shipping_address.state, + default: destination_address.state, }, { fieldtype: "Section Break", @@ -855,19 +847,20 @@ function is_e_waybill_valid(frm) { } function has_e_waybill_threshold_met(frm) { - if ( - frm.doc.doctype == "Sales Invoice" && - Math.abs(frm.doc.base_grand_total) >= gst_settings.e_waybill_threshold - ) + if (Math.abs(frm.doc.base_grand_total) >= gst_settings.e_waybill_threshold) return true; } function is_e_waybill_applicable(frm) { - // means company is Indian and not Unregistered if ( + // means company is Indian and not Unregistered !frm.doc.company_gstin || - (frm.doctype === "Sales Invoice" && - frm.doc.company_gstin === frm.doc.billing_address_gstin) + !gst_settings.enable_e_waybill || + !( + is_e_waybill_applicable_on_sales_invoice(frm) || + is_e_waybill_applicable_on_purchase_invoice(frm) || + is_e_waybill_applicable_on_delivery_note(frm) + ) ) return; @@ -886,9 +879,11 @@ function can_extend_e_waybill(frm) { const valid_upto = frm.doc.__onload?.e_waybill_info?.valid_upto; const extend_after = get_hours(valid_upto, -8); const extend_before = get_hours(valid_upto, 8); + const now = frappe.datetime.now_datetime(); if ( - extend_after < frappe.datetime.now_datetime() < extend_before && + extend_after < now && + now < extend_before && frm.doc.gst_transporter_id != frm.doc.company_gstin ) return true; @@ -907,6 +902,33 @@ function is_e_waybill_cancellable(frm) { ); } +function is_e_waybill_applicable_on_sales_invoice(frm) { + return ( + frm.doctype == "Sales Invoice" && + frm.doc.company_gstin !== frm.doc.billing_address_gstin && + frm.doc.customer_address && + !frm.doc.is_return && + !frm.doc.is_debit_note + ); +} + +function is_e_waybill_applicable_on_delivery_note(frm) { + return ( + frm.doctype == "Delivery Note" && + frm.doc.customer_address && + gst_settings.enable_e_waybill_from_dn + ); +} + +function is_e_waybill_applicable_on_purchase_invoice(frm) { + return ( + frm.doctype == "Purchase Invoice" && + frm.doc.supplier_address && + frm.doc.company_gstin !== frm.doc.supplier_gstin && + gst_settings.enable_e_waybill_from_pi + ); +} + function is_e_waybill_generated_using_api(frm) { const e_waybill_info = frm.doc.__onload && frm.doc.__onload.e_waybill_info; return e_waybill_info && e_waybill_info.created_on; @@ -1021,3 +1043,14 @@ function get_e_waybill_file_name(docname) { function set_primary_action_label(dialog, primary_action_label) { dialog.get_primary_btn().removeClass("hide").html(primary_action_label); } + +function get_destination_address_name(frm) { + if (frm.doc.doctype == "Purchase Invoice") { + if (frm.doc.is_return) return frm.doc.supplier_address; + return frm.doc.shipping_address_name || frm.doc.billing_address; + } else { + if (frm.doc.is_return) + return frm.doc.dispatch_address_name || frm.doc.company_address; + return frm.doc.shipping_address_name || frm.doc.customer_address; + } +} diff --git a/india_compliance/gst_india/client_scripts/purchase_invoice.js b/india_compliance/gst_india/client_scripts/purchase_invoice.js index 3c7d411e7..4033caaf2 100644 --- a/india_compliance/gst_india/client_scripts/purchase_invoice.js +++ b/india_compliance/gst_india/client_scripts/purchase_invoice.js @@ -1,4 +1,23 @@ -frappe.ui.form.on("Purchase Invoice", { +const DOCTYPE = "Purchase Invoice"; +setup_e_waybill_actions(DOCTYPE); + +frappe.ui.form.on(DOCTYPE, { + after_save(frm) { + if ( + frm.doc.docstatus || + frm.doc.supplier_address || + !is_e_waybill_applicable(frm) + ) + return; + + frappe.show_alert( + { + message: __("Supplier Address is required to create e-Waybill"), + indicator: "yellow", + }, + 10 + ); + }, refresh(frm) { if ( frm.doc.docstatus !== 1 || diff --git a/india_compliance/gst_india/constants/custom_fields.py b/india_compliance/gst_india/constants/custom_fields.py index d500741b5..01eadfad5 100644 --- a/india_compliance/gst_india/constants/custom_fields.py +++ b/india_compliance/gst_india/constants/custom_fields.py @@ -370,7 +370,7 @@ "fieldname": "gst_section", "label": "GST Details", "fieldtype": "Section Break", - "insert_after": "language", + "insert_after": "gst_vehicle_type", "print_hide": 1, "collapsible": 1, }, @@ -726,19 +726,9 @@ "print_hide": 1, "translatable": 0, }, - { - "fieldname": "ewaybill", - "label": "e-Waybill No.", - "fieldtype": "Data", - "depends_on": "eval: doc.docstatus === 1 || doc.ewaybill", - "allow_on_submit": 1, - "insert_after": "customer_name", - "translatable": 0, - "no_copy": 1, - }, ] -E_WAYBILL_SI_FIELDS = [ +E_WAYBILL_INV_FIELDS = [ { "fieldname": "transporter_info", "label": "Transporter Info", @@ -814,23 +804,39 @@ "default": "Today", "print_hide": 1, }, - { - "fieldname": "e_waybill_status", - "label": "e-Waybill Status", - "fieldtype": "Select", - "insert_after": "ewaybill", - "options": "\nPending\nGenerated\nCancelled\nNot Applicable", - "print_hide": 1, - "no_copy": 1, - "translatable": 1, - "allow_on_submit": 1, - "depends_on": "eval:doc.docstatus === 1 && (!doc.ewaybill || in_list(['','Pending', 'Not Applicable'], doc.e_waybill_status))", - "read_only_depends_on": "eval:doc.e_waybill_status === 'Generated' && doc.ewaybill", - }, *E_WAYBILL_DN_FIELDS, ] +sales_e_waybill_field = { + "fieldname": "ewaybill", + "label": "e-Waybill No.", + "fieldtype": "Data", + "depends_on": "eval: doc.docstatus === 1 || doc.ewaybill", + "allow_on_submit": 1, + "translatable": 0, + "no_copy": 1, + "insert_after": "customer_name", +} + +e_waybill_status_field = { + "fieldname": "e_waybill_status", + "label": "e-Waybill Status", + "fieldtype": "Select", + "insert_after": "ewaybill", + "options": "\nPending\nGenerated\nCancelled\nNot Applicable", + "print_hide": 1, + "no_copy": 1, + "translatable": 1, + "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus === 1 && (!doc.ewaybill || in_list(['','Pending', 'Not Applicable'], doc.e_waybill_status))", + "read_only_depends_on": "eval:doc.e_waybill_status === 'Generated' && doc.ewaybill", +} + +purchase_e_waybill_field = {**sales_e_waybill_field, "insert_after": "supplier_name"} + E_WAYBILL_FIELDS = { - "Sales Invoice": E_WAYBILL_SI_FIELDS, - "Delivery Note": E_WAYBILL_DN_FIELDS, + "Sales Invoice": E_WAYBILL_INV_FIELDS + + [sales_e_waybill_field, e_waybill_status_field], + "Delivery Note": E_WAYBILL_DN_FIELDS + [sales_e_waybill_field], + "Purchase Invoice": E_WAYBILL_INV_FIELDS + [purchase_e_waybill_field], } diff --git a/india_compliance/gst_india/constants/e_waybill.py b/india_compliance/gst_india/constants/e_waybill.py index 004a3e677..374ed5b81 100644 --- a/india_compliance/gst_india/constants/e_waybill.py +++ b/india_compliance/gst_india/constants/e_waybill.py @@ -10,7 +10,26 @@ # } # # DATETIME_FORMAT = "%d/%m/%Y %I:%M:%S %p" +selling_address = { + "bill_from": "company_address", + "bill_to": "customer_address", + "ship_from": "dispatch_address_name", + "ship_to": "shipping_address_name", +} + +buying_address = { + "bill_from": "supplier_address", + "bill_to": "billing_address", + "ship_from": "supplier_address", + "ship_to": "shipping_address", +} +ADDRESS_FIELDS = { + "Sales Invoice": selling_address, + "Purchase Invoice": buying_address, + "Delivery Note": selling_address, +} +PERMITTED_DOCTYPES = list(ADDRESS_FIELDS.keys()) CANCEL_REASON_CODES = { "Duplicate": "1", diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json index c88762641..17a86bd2c 100644 --- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json +++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json @@ -22,6 +22,7 @@ "e_waybill_section", "enable_e_waybill", "enable_e_waybill_from_dn", + "enable_e_waybill_from_pi", "fetch_e_waybill_data", "attach_e_waybill_print", "column_break_10", @@ -238,6 +239,13 @@ "fieldtype": "Check", "label": "Autofill Party Information based on GSTIN" }, + { + "default": "0", + "depends_on": "eval: doc.enable_e_waybill", + "fieldname": "enable_e_waybill_from_pi", + "fieldtype": "Check", + "label": "Enable e-Waybill Generation from Purchase Invoice" + }, { "collapsible": 1, "fieldname": "uom_mapping_section", diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py index 3de80576a..f056c9e47 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -1,8 +1,41 @@ import frappe from frappe.utils import flt +from india_compliance.gst_india.overrides.sales_invoice import ( + update_dashboard_with_gst_logs, +) from india_compliance.gst_india.overrides.transaction import validate_transaction -from india_compliance.gst_india.utils import get_gst_accounts_by_type +from india_compliance.gst_india.utils import get_gst_accounts_by_type, is_api_enabled +from india_compliance.gst_india.utils.e_waybill import get_e_waybill_info + + +def onload(doc, method=None): + if doc.docstatus != 1: + return + + if doc.gst_category == "Overseas": + doc.set_onload( + "bill_of_entry_exists", + frappe.db.exists( + "Bill of Entry", + {"purchase_invoice": doc.name, "docstatus": 1}, + ), + ) + + if not doc.get("ewaybill"): + return + + gst_settings = frappe.get_cached_doc("GST Settings") + + if not is_api_enabled(gst_settings): + return + + if ( + gst_settings.enable_e_waybill + and gst_settings.enable_e_waybill_from_pi + and doc.ewaybill + ): + doc.set_onload("e_waybill_info", get_e_waybill_info(doc)) def validate(doc, method=None): @@ -35,19 +68,6 @@ def update_itc_totals(doc, method=None): doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount) -def onload(doc, method): - if doc.docstatus != 1 or doc.gst_category != "Overseas": - return - - doc.set_onload( - "bill_of_entry_exists", - frappe.db.exists( - "Bill of Entry", - {"purchase_invoice": doc.name, "docstatus": 1}, - ), - ) - - def get_dashboard_data(data): transactions = data.setdefault("transactions", []) reference_section = next( @@ -60,4 +80,8 @@ def get_dashboard_data(data): reference_section["items"].append("Bill of Entry") + update_dashboard_with_gst_logs( + "Purchase Invoice", data, "e-Waybill Log", "Integration Request" + ) + return data diff --git a/india_compliance/gst_india/overrides/sales_invoice.py b/india_compliance/gst_india/overrides/sales_invoice.py index 702832cd2..be16e50d5 100644 --- a/india_compliance/gst_india/overrides/sales_invoice.py +++ b/india_compliance/gst_india/overrides/sales_invoice.py @@ -33,12 +33,7 @@ def onload(doc, method=None): if not doc.get("irn"): return - gst_settings = frappe.get_cached_value( - "GST Settings", - "GST Settings", - ("enable_api", "enable_e_waybill", "enable_e_invoice", "api_secret"), - as_dict=1, - ) + gst_settings = frappe.get_cached_doc("GST Settings") if not is_api_enabled(gst_settings): return diff --git a/india_compliance/gst_india/overrides/test_transaction_data.py b/india_compliance/gst_india/overrides/test_transaction_data.py index 91e4b7f47..2f8b90555 100644 --- a/india_compliance/gst_india/overrides/test_transaction_data.py +++ b/india_compliance/gst_india/overrides/test_transaction_data.py @@ -149,7 +149,7 @@ def test_set_transaction_details(self): gst_transaction_data.transaction_details, { "company_name": "_Test Indian Registered Company", - "customer_name": "_Test Registered Customer", + "party_name": "_Test Registered Customer", "date": format_date(frappe.utils.today(), "dd/mm/yyyy"), "total": 100.0, "rounding_adjustment": 0.0, diff --git a/india_compliance/gst_india/utils/e_invoice.py b/india_compliance/gst_india/utils/e_invoice.py index 63bae37e2..ef8797635 100644 --- a/india_compliance/gst_india/utils/e_invoice.py +++ b/india_compliance/gst_india/utils/e_invoice.py @@ -510,7 +510,7 @@ def set_party_address_details(self): self.doc.dispatch_address_name ) - self.billing_address.legal_name = self.transaction_details.customer_name + self.billing_address.legal_name = self.transaction_details.party_name self.company_address.legal_name = self.transaction_details.company_name def get_invoice_data(self): diff --git a/india_compliance/gst_india/utils/e_waybill.py b/india_compliance/gst_india/utils/e_waybill.py index 3fe0660af..57d7ebc36 100644 --- a/india_compliance/gst_india/utils/e_waybill.py +++ b/india_compliance/gst_india/utils/e_waybill.py @@ -16,10 +16,12 @@ from india_compliance.gst_india.api_classes.e_waybill import EWaybillAPI from india_compliance.gst_india.constants import STATE_NUMBERS from india_compliance.gst_india.constants.e_waybill import ( + ADDRESS_FIELDS, CANCEL_REASON_CODES, CONSIGNMENT_STATUS, EXTEND_VALIDITY_REASON_CODES, ITEM_LIMIT, + PERMITTED_DOCTYPES, SUB_SUPPLY_TYPES, TRANSIT_TYPES, UPDATE_VEHICLE_REASON_CODES, @@ -33,9 +35,6 @@ ) from india_compliance.gst_india.utils.transaction_data import GSTTransactionData -PERMITTED_DOCTYPES = {"Sales Invoice", "Delivery Note"} - - ####################################################################################### ### Manual JSON Generation for e-Waybill ############################################## ####################################################################################### @@ -129,7 +128,7 @@ def _generate_e_waybill(doc, throw=True): # Via e-Invoice API if not Return or Debit Note # Handles following error when generating e-Waybill using IRN: # 4010: E-way Bill cannot generated for Debit Note, Credit Note and Services - with_irn = doc.get("irn") and not (doc.is_return or doc.is_debit_note) + with_irn = doc.get("irn") and not (doc.is_return or doc.get("is_debit_note")) data = EWaybillData(doc).get_data(with_irn=with_irn) except frappe.ValidationError as e: @@ -232,7 +231,7 @@ def _cancel_e_waybill(doc, values): if ( e_waybill_data.sandbox_mode and doc.get("irn") - and not (doc.is_return or doc.is_debit_note) + and not (doc.is_return or doc.get("is_debit_note")) ) else EWaybillAPI ) @@ -706,18 +705,20 @@ def get_update_vehicle_data(self, values): self.validate_mode_of_transport() self.set_transporter_details() - dispatch_address_name = ( - self.doc.dispatch_address_name - if self.doc.dispatch_address_name - else self.doc.company_address + addresses = ADDRESS_FIELDS.get(self.doc.doctype, {}) + + ship_from_address_name = ( + addresses.get("ship_from") + if self.doc.get(addresses.get("ship_from")) + else addresses.get("bill_from") ) - dispatch_address = self.get_address_details(dispatch_address_name) + ship_from = self.get_address_details(self.doc.get(ship_from_address_name)) return { "ewbNo": self.doc.ewaybill, "vehicleNo": self.transaction_details.vehicle_no, - "fromPlace": dispatch_address.city, - "fromState": dispatch_address.state_number, + "fromPlace": ship_from.city, + "fromState": ship_from.state_number, "reasonCode": UPDATE_VEHICLE_REASON_CODES[values.reason], "reasonRem": self.sanitize_value(values.remark, regex=3), "transDocNo": self.transaction_details.lr_no, @@ -790,6 +791,7 @@ def validate_transaction(self): ) self.validate_applicability() + self.validate_bill_no_for_purchase() def validate_settings(self): if not self.settings.enable_e_waybill: @@ -805,12 +807,11 @@ def validate_applicability(self): - Sales Invoice with same company and billing gstin """ - for fieldname in ("company_address", "customer_address"): - if not self.doc.get(fieldname): + address = ADDRESS_FIELDS.get(self.doc.doctype) + for key in ("bill_from", "bill_to"): + if not self.doc.get(address[key]): frappe.throw( - _("{0} is required to generate e-Waybill").format( - _(self.doc.meta.get_label(fieldname)) - ), + _("{0} is required to generate e-Waybill").format(_(address[key])), exc=frappe.MandatoryError, ) @@ -845,12 +846,23 @@ def validate_applicability(self): title=_("Invalid Data"), ) + def validate_bill_no_for_purchase(self): + if ( + self.doc.doctype == "Purchase Invoice" + and not self.doc.is_return + and not self.doc.bill_no + and self.doc.gst_category != "Unregistered" + ): + frappe.throw( + _("Bill No is mandatory to generate e-Waybill for Purchase Invoice"), + title=_("Invalid Data"), + ) + def validate_doctype_for_e_waybill(self): if self.doc.doctype not in PERMITTED_DOCTYPES: frappe.throw( - _( - "Only Sales Invoice and Delivery Note are supported for e-Waybill" - " actions" + _("Only {0} are supported for e-Waybill actions").format( + ", ".join(PERMITTED_DOCTYPES) ), title=_("Unsupported DocType"), ) @@ -969,93 +981,136 @@ def get_all_item_details(self): def update_transaction_details(self): # first HSN Code for goods + doc = self.doc main_hsn_code = next( row.gst_hsn_code - for row in self.doc.items + for row in doc.items if not row.gst_hsn_code.startswith("99") ) self.transaction_details.update( - { + sub_supply_desc="", + main_hsn_code=main_hsn_code, + ) + + default_supply_types = { + # Key: (doctype, is_return) + ("Sales Invoice", 0): { "supply_type": "O", - "sub_supply_type": 1, + "sub_supply_type": 1, # Supply "document_type": "INV", - "main_hsn_code": main_hsn_code, - } - ) + }, + ("Sales Invoice", 1): { + "supply_type": "I", + "sub_supply_type": 7, # Sales Return + "document_type": "CHL", + }, + ("Delivery Note", 0): { + "supply_type": "O", + "sub_supply_type": doc.get("_sub_supply_type", ""), + "document_type": "CHL", + }, + ("Delivery Note", 1): { + "supply_type": "I", + "sub_supply_type": doc.get("_sub_supply_type", ""), + "document_type": "CHL", + }, + ("Purchase Invoice", 0): { + "supply_type": "I", + "sub_supply_type": 1, # Supply + "document_type": "INV", + }, + ("Purchase Invoice", 1): { + "supply_type": "O", + "sub_supply_type": 8, # Others + "document_type": "OTH", + "sub_supply_desc": "Purchase Return", + }, + } - if self.doc.is_return: - self.transaction_details.update( - { - "supply_type": "I", - "sub_supply_type": 7, - "document_type": "CHL", - } - ) + self.transaction_details.update( + default_supply_types.get((doc.doctype, doc.is_return), {}) + ) - elif is_foreign_doc(self.doc): - self.transaction_details.sub_supply_type = 3 + if is_foreign_doc(self.doc): + self.transaction_details.update(sub_supply_type=3) # Export + if not doc.is_export_with_gst: + self.transaction_details.update(document_type="BIL") - if not self.doc.is_export_with_gst: - self.transaction_details.document_type = "BIL" - - if self.doc.doctype == "Delivery Note": - self.transaction_details.update( - { - "sub_supply_type": self.doc._sub_supply_type, - "document_type": "CHL", - } - ) + if self.doc.doctype == "Purchase Invoice" and not self.doc.is_return: + self.transaction_details.name = self.doc.bill_no or self.doc.name def set_party_address_details(self): transaction_type = 1 - ship_to_address = ( + address = self.get_address_map() + + address.ship_to = ( self.doc.port_address if (is_foreign_doc(self.doc) and self.doc.port_address) - else self.doc.shipping_address_name + else address.ship_to ) - has_different_shipping_address = ( - ship_to_address and self.doc.customer_address != ship_to_address + if self.doc.is_return: + address.bill_from, address.bill_to = address.bill_to, address.bill_from + address.ship_from, address.ship_to = address.ship_to, address.ship_from + + has_different_to_address = ( + address.ship_to and address.ship_to != address.bill_to ) - has_different_dispatch_address = ( - self.doc.dispatch_address_name - and self.doc.company_address != self.doc.dispatch_address_name + has_different_from_address = ( + address.ship_from and address.ship_from != address.bill_from ) - self.to_address = self.get_address_details(self.doc.customer_address) - self.from_address = self.get_address_details(self.doc.company_address) + self.bill_to = self.get_address_details(address.bill_to) + self.bill_from = self.get_address_details(address.bill_from) # Defaults - # calling copy() since we're mutating from_address and to_address below - self.shipping_address = self.to_address.copy() - self.dispatch_address = self.from_address.copy() + # billing state is changed for SEZ, hence copy() + self.ship_to = self.bill_to.copy() + self.ship_from = self.bill_from.copy() - if has_different_shipping_address and has_different_dispatch_address: + if has_different_to_address and has_different_from_address: transaction_type = 4 - self.shipping_address = self.get_address_details(ship_to_address) - self.dispatch_address = self.get_address_details( - self.doc.dispatch_address_name - ) + self.ship_to = self.get_address_details(address.ship_to) + self.ship_from = self.get_address_details(address.ship_from) - elif has_different_dispatch_address: + elif has_different_from_address: transaction_type = 3 - self.dispatch_address = self.get_address_details( - self.doc.dispatch_address_name - ) + self.ship_from = self.get_address_details(address.ship_from) - elif has_different_shipping_address: + elif has_different_to_address: transaction_type = 2 - self.shipping_address = self.get_address_details(ship_to_address) + self.ship_to = self.get_address_details(address.ship_to) self.transaction_details.transaction_type = transaction_type - self.to_address.legal_name = self.transaction_details.customer_name - self.from_address.legal_name = self.transaction_details.company_name + to_party = self.transaction_details.party_name + from_party = self.transaction_details.company_name + + if self.doc.doctype == "Purchase Invoice": + to_party, from_party = from_party, to_party + + if self.doc.is_return: + to_party, from_party = from_party, to_party + + self.bill_to.legal_name = to_party + self.bill_from.legal_name = from_party if self.doc.gst_category == "SEZ": - self.to_address.state_number = 96 + self.bill_to.state_number = 96 + + def get_address_map(self): + """ + Return address names for bill_to, bill_from, ship_to, ship_from + """ + address_fields = ADDRESS_FIELDS.get(self.doc.doctype, {}) + out = frappe._dict() + + for key, field in address_fields.items(): + out[key] = self.doc.get(field) + + return out def get_address_details(self, *args, **kwargs): address_details = super().get_address_details(*args, **kwargs) @@ -1072,7 +1127,7 @@ def validate_distance_for_same_pincode(self): Accuracy of distance is immaterial and used only for e-Waybill validity determination. """ - if self.dispatch_address.pincode != self.shipping_address.pincode: + if self.ship_from.pincode != self.ship_to.pincode: return if self.transaction_details.distance > 100: @@ -1088,52 +1143,63 @@ def validate_distance_for_same_pincode(self): def get_transaction_data(self): if self.sandbox_mode: - self.transaction_details.company_gstin = "05AAACG2115R1ZN" - self.transaction_details.name = ( - random_string(6).lstrip("0") - if not frappe.flags.in_test - else "test_invoice_no" - ) + REGISTERED_GSTIN = "05AAACG2115R1ZN" + OTHER_GSTIN = "05AAACG2140A1ZL" - self.from_address.gstin = "05AAACG2115R1ZN" - self.to_address.gstin = ( - "05AAACG2140A1ZL" - if self.transaction_details.sub_supply_type not in (5, 10, 11, 12) - else "05AAACG2115R1ZN" + self.transaction_details.update( + { + "company_gstin": REGISTERED_GSTIN, + "name": random_string(6).lstrip("0") + if not frappe.flags.in_test + else "test_invoice_no", + } ) - if self.doc.is_return: - self.from_address, self.to_address = self.to_address, self.from_address - self.dispatch_address, self.shipping_address = ( - self.shipping_address, - self.dispatch_address, - ) + # to ensure company_gstin is inline with company address gstin + sandbox_gstin = { + # (doctype, is_return): (bill_from, bill_to) + ("Sales Invoice", 0): (REGISTERED_GSTIN, OTHER_GSTIN), + ("Sales Invoice", 1): (OTHER_GSTIN, REGISTERED_GSTIN), + ("Purchase Invoice", 0): (OTHER_GSTIN, REGISTERED_GSTIN), + ("Purchase Invoice", 1): (REGISTERED_GSTIN, OTHER_GSTIN), + ("Delivery Note", 0): (REGISTERED_GSTIN, OTHER_GSTIN), + ("Delivery Note", 1): (OTHER_GSTIN, REGISTERED_GSTIN), + } + + def _get_sandbox_gstin(address, key): + if address.gst_category == "Unregistered": + return "URP" + + return sandbox_gstin.get((self.doc.doctype, self.doc.is_return))[key] + + self.bill_from.gstin = _get_sandbox_gstin(self.bill_from, 0) + self.bill_to.gstin = _get_sandbox_gstin(self.bill_to, 1) data = { "userGstin": self.transaction_details.company_gstin, "supplyType": self.transaction_details.supply_type, "subSupplyType": self.transaction_details.sub_supply_type, - "subSupplyDesc": "", + "subSupplyDesc": self.transaction_details.sub_supply_desc, "docType": self.transaction_details.document_type, "docNo": self.transaction_details.name, "docDate": self.transaction_details.date, "transactionType": self.transaction_details.transaction_type, - "fromTrdName": self.from_address.legal_name, - "fromGstin": self.from_address.gstin, - "fromAddr1": self.dispatch_address.address_line1, - "fromAddr2": self.dispatch_address.address_line2, - "fromPlace": self.dispatch_address.city, - "fromPincode": self.dispatch_address.pincode, - "fromStateCode": self.from_address.state_number, - "actFromStateCode": self.dispatch_address.state_number, - "toTrdName": self.to_address.legal_name, - "toGstin": self.to_address.gstin, - "toAddr1": self.shipping_address.address_line1, - "toAddr2": self.shipping_address.address_line2, - "toPlace": self.shipping_address.city, - "toPincode": self.shipping_address.pincode, - "toStateCode": self.to_address.state_number, - "actToStateCode": self.shipping_address.state_number, + "fromTrdName": self.bill_from.legal_name, + "fromGstin": self.bill_from.gstin, + "fromAddr1": self.ship_from.address_line1, + "fromAddr2": self.ship_from.address_line2, + "fromPlace": self.ship_from.city, + "fromPincode": self.ship_from.pincode, + "fromStateCode": self.bill_from.state_number, + "actFromStateCode": self.ship_from.state_number, + "toTrdName": self.bill_to.legal_name, + "toGstin": self.bill_to.gstin, + "toAddr1": self.ship_to.address_line1, + "toAddr2": self.ship_to.address_line2, + "toPlace": self.ship_to.city, + "toPincode": self.ship_to.pincode, + "toStateCode": self.bill_to.state_number, + "actToStateCode": self.ship_to.state_number, "totalValue": self.transaction_details.total, "cgstValue": self.transaction_details.total_cgst_amount, "sgstValue": self.transaction_details.total_sgst_amount, diff --git a/india_compliance/gst_india/utils/test_e_waybill.py b/india_compliance/gst_india/utils/test_e_waybill.py index 00e934faa..157c3fe10 100644 --- a/india_compliance/gst_india/utils/test_e_waybill.py +++ b/india_compliance/gst_india/utils/test_e_waybill.py @@ -26,8 +26,8 @@ from india_compliance.gst_india.utils.tests import ( _append_taxes, append_item, - create_purchase_invoice, create_sales_invoice, + create_transaction, ) DATETIME_FORMAT = "%d/%m/%Y %I:%M:%S %p" @@ -626,13 +626,15 @@ def test_get_extend_validity_data(self): def test_validate_doctype_for_e_waybill(self): """Validate if doctype is supported for e-waybill""" - purchase_invoice = create_purchase_invoice() + purchase_order = create_transaction(doctype="Purchase Order") self.assertRaisesRegex( frappe.exceptions.ValidationError, - re.compile(r"^(Only Sales Invoice and Delivery Note are supported.*)$"), + re.compile( + r"^(Only Sales Invoice, Purchase Invoice, Delivery Note are supported.*)$" + ), EWaybillData, - purchase_invoice, + purchase_order, ) @responses.activate diff --git a/india_compliance/gst_india/utils/transaction_data.py b/india_compliance/gst_india/utils/transaction_data.py index 90c381376..16c68ff75 100644 --- a/india_compliance/gst_india/utils/transaction_data.py +++ b/india_compliance/gst_india/utils/transaction_data.py @@ -39,10 +39,22 @@ def __init__(self, doc): self.sandbox_mode = self.settings.sandbox_mode self.transaction_details = frappe._dict() + gst_type = "Output" + self.party_name_field = "customer_name" + + if self.doc.doctype == "Purchase Invoice": + self.party_name_field = "supplier_name" + if self.doc.is_reverse_charge != 1: + # for with reverse charge, gst_type is Output + # this will ensure zero taxes in transaction details + gst_type = "Input" + + self.party_name = self.doc.get(self.party_name_field) + # "CGST Account - TC": "cgst_account" self.gst_accounts = { v: k - for k, v in get_gst_accounts_by_type(self.doc.company, "Output").items() + for k, v in get_gst_accounts_by_type(self.doc.company, gst_type).items() } def set_transaction_details(self): @@ -50,10 +62,10 @@ def set_transaction_details(self): self.transaction_details.update( { "company_name": self.sanitize_value(self.doc.company), - "customer_name": self.sanitize_value( - self.doc.customer_name + "party_name": self.sanitize_value( + self.party_name or frappe.db.get_value( - "Customer", self.doc.customer, "customer_name" + self.doc.doctype, self.party_name, self.party_name_field ) ), "date": format_date(self.doc.posting_date, self.DATE_FORMAT), @@ -266,6 +278,7 @@ def validate_transaction(self): msg=_("Posting Date cannot be greater than Today's Date"), title=_("Invalid Data"), ) + # compare posting date and lr date, only if lr no is set if ( self.doc.lr_no diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index bcbb706b8..d616a467a 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -45,7 +45,10 @@ ], "Journal Entry": "gst_india/client_scripts/journal_entry.js", "Payment Entry": "gst_india/client_scripts/payment_entry.js", - "Purchase Invoice": "gst_india/client_scripts/purchase_invoice.js", + "Purchase Invoice": [ + "gst_india/client_scripts/e_waybill_actions.js", + "gst_india/client_scripts/purchase_invoice.js", + ], "Sales Invoice": [ "gst_india/client_scripts/e_invoice_actions.js", "gst_india/client_scripts/e_waybill_actions.js", diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index 500c3072c..e5f97d0e8 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -3,7 +3,7 @@ execute:import frappe; frappe.delete_doc_if_exists("DocType", "GSTIN") [post_model_sync] india_compliance.patches.v14.set_default_for_overridden_accounts_setting -execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #21 +execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #22 execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #2 india_compliance.patches.post_install.remove_old_fields india_compliance.patches.post_install.update_company_gstin