diff --git a/india_compliance/gst_india/client_scripts/company.js b/india_compliance/gst_india/client_scripts/company.js index 14a1fa7fb2..56cee2dfb0 100644 --- a/india_compliance/gst_india/client_scripts/company.js +++ b/india_compliance/gst_india/client_scripts/company.js @@ -10,13 +10,23 @@ set_gstin_query(DOCTYPE); frappe.ui.form.off(DOCTYPE, "make_default_tax_template"); frappe.ui.form.on(DOCTYPE, { + setup(frm) { + erpnext.company.set_custom_query(frm, [ + "default_customs_expense_account", + { root_type: "Expense" }, + ]); + erpnext.company.set_custom_query(frm, [ + "default_customs_payable_account", + { root_type: "Liability" }, + ]); + }, + make_default_tax_template: function (frm) { + if (frm.doc.country !== "India") return; + frappe.call({ method: "india_compliance.gst_india.overrides.company.make_default_tax_templates", - args: { - company: frm.doc.name, - country: frm.doc.country, - }, + args: { company: frm.doc.name }, callback: function () { frappe.msgprint(__("Default Tax Templates created")); }, diff --git a/india_compliance/gst_india/client_scripts/purchase_invoice.js b/india_compliance/gst_india/client_scripts/purchase_invoice.js new file mode 100644 index 0000000000..3c7d411e77 --- /dev/null +++ b/india_compliance/gst_india/client_scripts/purchase_invoice.js @@ -0,0 +1,21 @@ +frappe.ui.form.on("Purchase Invoice", { + refresh(frm) { + if ( + frm.doc.docstatus !== 1 || + frm.doc.gst_category !== "Overseas" || + frm.doc.__onload?.bill_of_entry_exists + ) + return; + + frm.add_custom_button( + __("Bill of Entry"), + () => { + frappe.model.open_mapped_doc({ + method: "india_compliance.gst_india.doctype.bill_of_entry.bill_of_entry.make_bill_of_entry", + frm: frm, + }); + }, + __("Create") + ); + }, +}); diff --git a/india_compliance/gst_india/constants/custom_fields.py b/india_compliance/gst_india/constants/custom_fields.py index aaa589e213..cc8f59844b 100644 --- a/india_compliance/gst_india/constants/custom_fields.py +++ b/india_compliance/gst_india/constants/custom_fields.py @@ -1,5 +1,3 @@ -from copy import deepcopy - import frappe from india_compliance.gst_india.constants import GST_CATEGORIES, STATE_NUMBERS @@ -50,11 +48,28 @@ def get_place_of_supply_options(): }, ] -company_fields = deepcopy(party_fields) -company_fields[0]["insert_after"] = "parent_company" - CUSTOM_FIELDS = { - "Company": company_fields, + "Company": [ + { + **party_fields[0], + "insert_after": "parent_company", + }, + *party_fields[1:], + { + "fieldname": "default_customs_expense_account", + "label": "Default Customs Duty Expense Account", + "fieldtype": "Link", + "options": "Account", + "insert_after": "unrealized_profit_loss_account", + }, + { + "fieldname": "default_customs_payable_account", + "label": "Default Customs Duty Payable Account", + "fieldtype": "Link", + "options": "Account", + "insert_after": "default_finance_book", + }, + ], ("Customer", "Supplier"): party_fields, # Purchase Fields ("Purchase Order", "Purchase Receipt", "Purchase Invoice"): [ diff --git a/india_compliance/gst_india/doctype/bill_of_entry/__init__.py b/india_compliance/gst_india/doctype/bill_of_entry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js new file mode 100644 index 0000000000..f915f114da --- /dev/null +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js @@ -0,0 +1,337 @@ +// Copyright (c) 2023, Resilient Tech and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Bill of Entry", { + onload(frm) { + frm.fields_dict.items.grid.cannot_add_rows = true; + frm.bill_of_entry_controller = new BillOfEntryController(frm); + }, + + refresh(frm) { + if (frm.doc.docstatus === 0) return; + + // check if Journal Entry exists; + if (frm.doc.docstatus === 1 && !frm.doc.__onload?.journal_entry_exists) { + frm.add_custom_button( + __("Journal Entry for Payment"), + () => { + frappe.model.open_mapped_doc({ + method: "india_compliance.gst_india.doctype.bill_of_entry.bill_of_entry.make_journal_entry_for_payment", + frm: frm, + }); + }, + __("Create") + ); + } + + if (frm.doc.docstatus === 1 && frm.doc.total_customs_duty > 0) { + frm.add_custom_button( + __("Landed Cost Voucher"), + () => { + frappe.model.open_mapped_doc({ + method: "india_compliance.gst_india.doctype.bill_of_entry.bill_of_entry.make_landed_cost_voucher", + frm: frm, + }); + }, + __("Create") + ); + } + + frm.add_custom_button( + __("Accounting Ledger"), + () => { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.posting_date, + to_date: frm.doc.posting_date, + company: frm.doc.company, + group_by: "Group by Voucher (Consolidated)", + show_cancelled_entries: frm.doc.docstatus === 2, + }; + frappe.set_route("query-report", "General Ledger"); + }, + __("View") + ); + }, + + total_taxable_value(frm) { + frm.taxes_controller.update_tax_amount(); + }, + + total_customs_duty(frm) { + frm.bill_of_entry_controller.update_total_amount_payable(); + }, + + total_taxes(frm) { + frm.bill_of_entry_controller.update_total_amount_payable(); + }, +}); + +frappe.ui.form.on("Bill of Entry Item", { + assessable_value(frm, cdt, cdn) { + frm.bill_of_entry_controller.update_item_taxable_value(cdt, cdn); + }, + + customs_duty(frm, cdt, cdn) { + frm.bill_of_entry_controller.update_item_taxable_value(cdt, cdn); + frm.bill_of_entry_controller.update_total_customs_duty(); + }, + + async item_tax_template(frm, cdt, cdn) { + const row = locals[cdt][cdn]; + if (!row.item_tax_template) frm.taxes_controller.update_item_wise_tax_rates(); + else await frm.taxes_controller.set_item_wise_tax_rates(cdn); + + frm.taxes_controller.update_tax_amount(); + }, + + items_remove(frm) { + frm.bill_of_entry_controller.update_total_taxable_value(); + }, +}); + +frappe.ui.form.on("Bill of Entry Taxes", { + rate(frm, cdt, cdn) { + frm.taxes_controller.update_tax_rate(cdt, cdn); + }, + + tax_amount(frm, cdt, cdn) { + frm.taxes_controller.update_tax_amount(cdt, cdn); + }, + + async account_head(frm, cdt, cdn) { + await frm.taxes_controller.set_item_wise_tax_rates(null, cdn); + frm.taxes_controller.update_tax_amount(cdt, cdn); + }, + + async charge_type(frm, cdt, cdn) { + const row = locals[cdt][cdn]; + if (row.charge_type === "On Net Total") { + await frm.taxes_controller.set_item_wise_tax_rates(null, cdn); + frm.taxes_controller.update_tax_amount(cdt, cdn); + } else { + row.rate = 0; + row.item_wise_tax_rates = "{}"; + frm.refresh_field("taxes"); + } + }, + + taxes_remove(frm) { + frm.bill_of_entry_controller.update_total_taxes(); + }, +}); + +class BillOfEntryController { + constructor(frm) { + this.frm = frm; + this.frm.taxes_controller = new TaxesController(frm); + this.setup(); + } + + setup() { + this.set_account_query(); + } + + set_account_query() { + [ + { + name: "customs_payable_account", + filters: { root_type: "Liability", account_type: ["!=", "Payable"] }, + }, + { name: "customs_expense_account", filters: { root_type: "Expense" } }, + { name: "cost_center" }, + ].forEach(row => { + this.frm.set_query(row.name, () => { + return { + filters: { + ...row.filters, + company: this.frm.doc.company, + is_group: 0, + }, + }; + }); + }); + } + + async update_item_taxable_value(cdt, cdn) { + const row = locals[cdt][cdn]; + await frappe.model.set_value( + cdt, + cdn, + "taxable_value", + row.assessable_value + row.customs_duty + ); + this.update_total_taxable_value(); + } + + update_total_taxable_value() { + this.frm.set_value( + "total_taxable_value", + this.frm.doc.items.reduce((total, row) => { + return total + row.taxable_value; + }, 0) + ); + } + + update_total_customs_duty() { + this.frm.set_value( + "total_customs_duty", + this.frm.doc.items.reduce((total, row) => { + return total + row.customs_duty; + }, 0) + ); + } + + update_total_taxes() { + const total_taxes = this.frm.doc.taxes.reduce( + (total, row) => total + row.tax_amount, + 0 + ); + this.frm.set_value("total_taxes", total_taxes); + } + + update_total_amount_payable() { + this.frm.set_value( + "total_amount_payable", + this.frm.doc.total_customs_duty + this.frm.doc.total_taxes + ); + } +} + +class TaxesController { + constructor(frm) { + this.frm = frm; + this.setup(); + } + + setup() { + this.set_item_tax_template_query(); + this.set_account_head_query(); + } + + set_item_tax_template_query() { + this.frm.set_query("item_tax_template", "items", () => { + return { + filters: { + company: this.frm.doc.company, + }, + }; + }); + } + + set_account_head_query() { + this.frm.set_query("account_head", "taxes", () => { + return { + filters: { + company: this.frm.doc.company, + is_group: 0, + }, + }; + }); + } + + async set_item_wise_tax_rates(item_name, tax_name) { + /** + * This method is used to set item wise tax rates from the server + * and update the item_wise_tax_rates field in the taxes table. + * + * @param {string} item_name - Item row name for which the tax rates are to be fetched. + * @param {string} tax_name - Tax row name for which the tax rates are to be fetched. + */ + + if (!this.frm.taxes || !this.frm.taxes.length) return; + + await this.frm.call("set_item_wise_tax_rates", { + item_name: item_name, + tax_name: tax_name, + }); + } + + update_item_wise_tax_rates(tax_row) { + /** + * This method is used to update the item_wise_tax_rates field in the taxes table when + * - Item tax template is removed from the item row. + * - Tax rate is changed in the tax row. + * + * It will update item rate with default tax rate. + * + * @param {object} tax_row - Tax row object. + */ + + let taxes; + if (tax_row) taxes = [tax_row]; + else taxes = this.frm.doc.taxes; + + taxes.forEach(tax => { + const item_wise_tax_rates = JSON.parse(tax.item_wise_tax_rates || "{}"); + this.frm.doc.items.forEach(item => { + if (item.item_tax_template) return; + item_wise_tax_rates[item.name] = tax.rate; + }); + tax.item_wise_tax_rates = JSON.stringify(item_wise_tax_rates); + }); + } + + async update_tax_rate(cdt, cdn) { + const row = locals[cdt][cdn]; + if (row.charge_type === "Actual") row.rate = 0; + else if (row.charge_type === "On Net Total") { + this.update_item_wise_tax_rates(row); + await this.update_tax_amount(cdt, cdn); + } + } + + async update_tax_amount(cdt, cdn) { + /** + * This method is used to update the tax amount in the tax row + * - Update for all tax rows when cdt is null. + * - Update for a single tax row when cdt and cdn are passed. + * + * @param {string} cdt - Doctype of the tax row. + * @param {string} cdn - Name of the tax row. + */ + + let taxes; + if (cdt) taxes = [locals[cdt][cdn]]; + else taxes = this.frm.doc.taxes; + + taxes.forEach(async row => { + if (row.charge_type === "On Net Total") { + const tax_amount = this.get_tax_on_net_total(row); + + // update if tax amount is changed manually + if (tax_amount !== row.tax_amount) { + row.tax_amount = tax_amount; + } + } + }); + + this.update_total_amount(); + this.frm.bill_of_entry_controller.update_total_taxes(); + } + + update_total_amount() { + this.frm.doc.taxes.reduce((total, row) => { + const total_amount = total + row.tax_amount; + row.total = total_amount; + + return total_amount; + }, this.frm.doc.total_taxable_value); + + this.frm.refresh_field("taxes"); + } + + get_tax_on_net_total(tax_row) { + /** + * This method is used to calculate the tax amount on net total + * based on the item wise tax rates. + * + * @param {object} tax_row - Tax row object. + */ + + const item_wise_tax_rates = JSON.parse(tax_row.item_wise_tax_rates || "{}"); + return this.frm.doc.items.reduce((total, item) => { + return total + (item.taxable_value * item_wise_tax_rates[item.name]) / 100; + }, 0); + } +} diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.json b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.json new file mode 100644 index 0000000000..3111ed4de2 --- /dev/null +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.json @@ -0,0 +1,328 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2023-01-27 14:50:06.483818", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "column_break_2wjs", + "amended_from", + "column_break_hbfk", + "section_break_fqdh", + "purchase_invoice", + "column_break_epap", + "company", + "column_break_4zp5", + "company_gstin", + "section_break_ott8", + "bill_of_entry_no", + "bill_of_entry_date", + "column_break_v3jw", + "port_code", + "column_break_srom", + "posting_date", + "accounting_details_section", + "customs_payable_account", + "column_break_xjvk", + "customs_expense_account", + "column_break_zsed", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "section_break_nlqh", + "items", + "section_break_ujtl", + "column_break_cnrv", + "column_break_wmum", + "column_break_wxlx", + "total_taxable_value", + "section_break_biay", + "taxes", + "section_break_zcnz", + "total_customs_duty", + "column_break_nsns", + "total_taxes", + "column_break_kgnb", + "total_amount_payable" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "no_copy": 1, + "options": "BOE-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "bill_of_entry_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Bill of Entry No", + "reqd": 1 + }, + { + "fieldname": "column_break_srom", + "fieldtype": "Column Break" + }, + { + "fieldname": "bill_of_entry_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Bill of Entry Date", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Bill of Entry", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_ott8", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_2wjs", + "fieldtype": "Column Break" + }, + { + "fieldname": "port_code", + "fieldtype": "Data", + "label": "Port Code" + }, + { + "fieldname": "column_break_v3jw", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_fqdh", + "fieldtype": "Section Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "column_break_4zp5", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_gstin", + "fieldtype": "Data", + "label": "Company GSTIN", + "read_only": 1 + }, + { + "fieldname": "column_break_hbfk", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_invoice", + "fieldtype": "Link", + "label": "Purchase Invoice", + "options": "Purchase Invoice", + "reqd": 1 + }, + { + "fieldname": "column_break_epap", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_nlqh", + "fieldtype": "Section Break", + "label": "Items" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "options": "Bill of Entry Item", + "reqd": 1 + }, + { + "fieldname": "section_break_biay", + "fieldtype": "Section Break", + "label": "Taxes" + }, + { + "fieldname": "taxes", + "fieldtype": "Table", + "options": "Bill of Entry Taxes", + "reqd": 1 + }, + { + "fieldname": "section_break_ujtl", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_wxlx", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_taxable_value", + "fieldtype": "Currency", + "label": "Total Taxable Value (Net Total)", + "read_only": 1 + }, + { + "fieldname": "section_break_zcnz", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_cnrv", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_wmum", + "fieldtype": "Column Break" + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_xjvk", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_zsed", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_customs_duty", + "fieldtype": "Currency", + "label": "Total Customs Duty", + "read_only": 1 + }, + { + "fieldname": "column_break_nsns", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_kgnb", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_taxes", + "fieldtype": "Currency", + "label": "Total Taxes", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "total_amount_payable", + "fieldtype": "Currency", + "label": "Total Amount Payable", + "read_only": 1 + }, + { + "fieldname": "customs_payable_account", + "fieldtype": "Link", + "label": "Customs Duty Payable Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "customs_expense_account", + "fieldtype": "Link", + "label": "Customs Duty Expense Account", + "options": "Account", + "reqd": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-03-24 09:46:13.847177", + "modified_by": "Administrator", + "module": "GST India", + "name": "Bill of Entry", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User" + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Auditor" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py new file mode 100644 index 0000000000..196225ee89 --- /dev/null +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py @@ -0,0 +1,524 @@ +# Copyright (c) 2023, Resilient Tech and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.utils import today +import erpnext +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.controllers.accounts_controller import AccountsController + +from india_compliance.gst_india.utils import get_gst_accounts_by_type + + +class BillofEntry(Document): + get_gl_dict = AccountsController.get_gl_dict + + def onload(self): + if self.docstatus != 1: + return + + self.set_onload( + "journal_entry_exists", + frappe.db.exists( + "Journal Entry Account", + { + "reference_type": "Bill of Entry", + "reference_name": self.name, + "docstatus": 1, + }, + ), + ) + + def before_validate(self): + self.set_taxes_and_totals() + + def validate(self): + self.validate_purchase_invoice() + self.validate_taxes() + + def on_submit(self): + make_gl_entries(self.get_gl_entries()) + + def on_cancel(self): + self.ignore_linked_doctypes = ("GL Entry",) + make_gl_entries(self.get_gl_entries(), cancel=True) + + # Code adapted from AccountsController.on_trash + def on_trash(self): + if not frappe.db.get_single_value( + "Accounts Settings", "delete_linked_ledger_entries" + ): + return + + frappe.db.delete( + "GL Entry", {"voucher_type": self.doctype, "voucher_no": self.name} + ) + + def set_defaults(self): + self.set_item_defaults() + self.set_default_accounts() + + def set_item_defaults(self): + """These defaults are needed for taxes and totals to get calculated""" + for item in self.items: + item.name = frappe.generate_hash(length=10) + item.customs_duty = 0 + + def set_default_accounts(self): + company = frappe.get_cached_doc("Company", self.company) + self.customs_expense_account = company.default_customs_expense_account + self.customs_payable_account = company.default_customs_payable_account + + def set_taxes_and_totals(self): + self.set_item_wise_tax_rates() + self.calculate_totals() + + def calculate_totals(self): + self.set_total_customs_and_taxable_values() + self.set_total_taxes() + self.total_amount_payable = self.total_customs_duty + self.total_taxes + + def set_total_customs_and_taxable_values(self): + total_customs_duty = 0 + total_taxable_value = 0 + + for item in self.items: + item.taxable_value = item.assessable_value + item.customs_duty + total_customs_duty += item.customs_duty + total_taxable_value += item.taxable_value + + self.total_customs_duty = total_customs_duty + self.total_taxable_value = total_taxable_value + + def set_total_taxes(self): + total_taxes = 0 + + for tax in self.taxes: + if tax.charge_type == "On Net Total": + tax.tax_amount = self.get_tax_amount(tax.item_wise_tax_rates) + + total_taxes += tax.tax_amount + tax.total = self.total_taxable_value + total_taxes + + self.total_taxes = total_taxes + + def get_tax_amount(self, item_wise_tax_rates): + if isinstance(item_wise_tax_rates, str): + item_wise_tax_rates = json.loads(item_wise_tax_rates) + + tax_amount = 0 + for item in self.items: + tax_amount += ( + item_wise_tax_rates.get(item.name, 0) * item.taxable_value / 100 + ) + + return tax_amount + + def validate_purchase_invoice(self): + purchase = frappe.get_doc("Purchase Invoice", self.purchase_invoice) + if purchase.docstatus != 1: + frappe.throw( + _("Purchase Invoice must be submitted when creating a Bill of Entry") + ) + + if purchase.gst_category != "Overseas": + frappe.throw( + _( + "GST Category must be set to Overseas in Purchase Invoice to create" + " a Bill of Entry" + ) + ) + + pi_items = {item.name for item in purchase.items} + for item in self.items: + if not item.pi_detail: + frappe.throw( + _("Row #{0}: Purchase Invoice Item is required").format(item.idx) + ) + + if item.pi_detail not in pi_items: + frappe.throw( + _( + "Row #{0}: Purchase Invoice Item {1} not found in Purchase" + " Invoice {2}" + ).format( + item.idx, + frappe.bold(item.pi_detail), + frappe.bold(self.purchase_invoice), + ) + ) + + def validate_taxes(self): + input_accounts = get_gst_accounts_by_type(self.company, "Input", throw=True) + for tax in self.taxes: + if ( + tax.account_head + in (input_accounts.igst_account, input_accounts.cess_account) + or not tax.tax_amount + ): + continue + + frappe.throw( + _( + "Row #{0}: Only Input IGST and CESS accounts are allowed in" + " Bill of Entry" + ).format(tax.idx) + ) + + def get_gl_entries(self): + # company_currency is required by get_gl_dict + self.company_currency = erpnext.get_company_currency(self.company) + + gl_entries = [] + remarks = "No Remarks" + + for item in self.items: + gl_entries.append( + self.get_gl_dict( + { + "account": self.customs_expense_account, + "debit": item.customs_duty, + "credit": 0, + "cost_center": item.cost_center, + "remarks": remarks, + }, + ) + ) + + for tax in self.taxes: + gl_entries.append( + self.get_gl_dict( + { + "account": tax.account_head, + "debit": tax.tax_amount, + "credit": 0, + "cost_center": self.cost_center, + "remarks": remarks, + }, + ) + ) + + gl_entries.append( + self.get_gl_dict( + { + "account": self.customs_payable_account, + "debit": 0, + "credit": self.total_amount_payable, + "cost_center": self.cost_center, + "remarks": remarks, + }, + ) + ) + + return gl_entries + + # Overriding AccountsController method + def validate_account_currency(self, account, account_currency=None): + if account_currency == "INR": + return + + frappe.throw( + _("Row #{0}: Account {1} must be of INR currency").format( + self.idx, frappe.bold(account) + ) + ) + + @frappe.whitelist() + def set_item_wise_tax_rates(self, item_name=None, tax_name=None): + items, taxes = self.get_rows_to_update(item_name, tax_name) + tax_accounts = {tax.account_head for tax in taxes} + + if not tax_accounts: + return + + tax_templates = {item.item_tax_template for item in items} + item_tax_map = self.get_item_tax_map(tax_templates, tax_accounts) + + for tax in taxes: + if tax.charge_type != "On Net Total": + tax.item_wise_tax_rates = "{}" + continue + + item_wise_tax_rates = ( + json.loads(tax.item_wise_tax_rates) if tax.item_wise_tax_rates else {} + ) + + for item in items: + key = (item.item_tax_template, tax.account_head) + item_wise_tax_rates[item.name] = item_tax_map.get(key, tax.rate) + + tax.item_wise_tax_rates = json.dumps(item_wise_tax_rates) + + def get_item_tax_map(self, tax_templates, tax_accounts): + """ + Parameters: + tax_templates (list): List of item tax templates used in the items + tax_accounts (list): List of tax accounts used in the taxes + + Returns: + dict: A map of item_tax_template, tax_account and tax_rate + + Sample Output: + { + ('GST 18%', 'IGST - TC'): 18.0 + ('GST 28%', 'IGST - TC'): 28.0 + } + """ + + if not tax_templates: + return {} + + tax_rates = frappe.get_all( + "Item Tax Template Detail", + fields=("parent", "tax_type", "tax_rate"), + filters={ + "parent": ("in", tax_templates), + "tax_type": ("in", tax_accounts), + }, + ) + + return {(d.parent, d.tax_type): d.tax_rate for d in tax_rates} + + def get_rows_to_update(self, item_name=None, tax_name=None): + """ + Returns items and taxes to update based on item_name and tax_name passed. + If item_name and tax_name are not passed, all items and taxes are returned. + """ + + items = self.get("items", {"name": item_name}) if item_name else self.items + taxes = self.get("taxes", {"name": tax_name}) if tax_name else self.taxes + + return items, taxes + + +@frappe.whitelist() +def make_bill_of_entry(source_name, target_doc=None): + def set_missing_values(source, target): + target.set_defaults() + + # Add default tax + input_igst_account = get_gst_accounts_by_type( + source.company, "Input" + ).igst_account + + if not input_igst_account: + return + + rate, description = frappe.db.get_value( + "Purchase Taxes and Charges", + { + "parenttype": "Purchase Taxes and Charges Template", + "account_head": input_igst_account, + }, + ("rate", "description"), + ) or (0, input_igst_account) + + target.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": input_igst_account, + "rate": rate, + "description": description, + }, + ) + + target.set_taxes_and_totals() + + doc = get_mapped_doc( + "Purchase Invoice", + source_name, + { + "Purchase Invoice": { + "doctype": "Bill of Entry", + "field_no_map": ["posting_date"], + "validation": { + "docstatus": ["=", 1], + "gst_category": ["=", "Overseas"], + }, + }, + "Purchase Invoice Item": { + "doctype": "Bill of Entry Item", + "field_map": { + "name": "pi_detail", + "taxable_value": "assessable_value", + }, + }, + }, + target_doc, + postprocess=set_missing_values, + ) + + return doc + + +@frappe.whitelist() +def make_journal_entry_for_payment(source_name, target_doc=None): + def set_missing_values(source, target): + target.voucher_type = "Bank Entry" + target.posting_date = target.cheque_date = today() + target.user_remark = "Payment against Bill of Entry {0}".format(source.name) + + company = frappe.get_cached_doc("Company", source.company) + target.append( + "accounts", + { + "account": source.customs_payable_account, + "debit_in_account_currency": source.total_amount_payable, + "reference_type": "Bill of Entry", + "reference_name": source.name, + "cost_center": company.cost_center, + }, + ) + + target.append( + "accounts", + { + "account": company.default_bank_account or company.default_cash_account, + "credit_in_account_currency": source.total_amount_payable, + "cost_center": company.cost_center, + }, + ) + + doc = get_mapped_doc( + "Bill of Entry", + source_name, + { + "Bill of Entry": { + "doctype": "Journal Entry", + "validation": { + "docstatus": ["=", 1], + }, + }, + }, + target_doc, + postprocess=set_missing_values, + ) + + return doc + + +@frappe.whitelist() +def make_landed_cost_voucher(source_name, target_doc=None): + def set_missing_values(source, target): + items = get_items_for_landed_cost_voucher(source) + if not items: + frappe.throw(_("No items found for Landed Cost Voucher")) + + target.posting_date = today() + target.distribute_charges_based_on = "Distribute Manually" + + # add references + reference_docs = {item.parent: item.parenttype for item in items.values()} + for parent, parenttype in reference_docs.items(): + target.append( + "purchase_receipts", + { + "receipt_document_type": parenttype, + "receipt_document": parent, + }, + ) + + # add items + target.get_items_from_purchase_receipts() + + # update applicable charges + total_customs_duty = 0 + for item in target.items: + item.applicable_charges = items[item.purchase_receipt_item].customs_duty + total_customs_duty += item.applicable_charges + + # add taxes + target.append( + "taxes", + { + "expense_account": source.customs_expense_account, + "description": "Customs Duty", + "amount": total_customs_duty, + }, + ) + + if total_customs_duty != source.total_customs_duty: + frappe.msgprint( + _( + "Could not find purchase receipts for all items. Please check" + " manually." + ) + ) + + doc = get_mapped_doc( + "Bill of Entry", + source_name, + { + "Bill of Entry": { + "doctype": "Landed Cost Voucher", + }, + }, + target_doc, + postprocess=set_missing_values, + ) + + return doc + + +def get_items_for_landed_cost_voucher(boe): + """ + For creating landed cost voucher, it needs to be linked with transaction where stock was updated. + This function will return items based on following conditions: + 1. Where stock was updated in Purchase Invoice + 2. Where stock was updated in Purchase Receipt + a. Purchase Invoice was created from Purchase Receipt + b. Purchase Receipt was created from Purchase Invoice + + Also, it will apportion customs duty for PI items. + + NOTE: Assuming business has consistent practice of creating PR and PI + """ + pi = frappe.get_doc("Purchase Invoice", boe.purchase_invoice) + item_customs_map = {item.pi_detail: item.customs_duty for item in boe.items} + + def _item_dict(items): + return frappe._dict({item.name: item for item in items}) + + # No PR + if pi.update_stock: + pi_items = [pi_item.as_dict() for pi_item in pi.items] + for pi_item in pi_items: + pi_item.customs_duty = item_customs_map.get(pi_item.name) + + return _item_dict(pi_items) + + # Creating PI from PR + if pi.items[0].purchase_receipt: + pr_pi_map = {pi_item.pr_detail: pi_item.name for pi_item in pi.items} + pr_items = frappe.get_all( + "Purchase Receipt Item", + fields="*", + filters={"name": ["in", pr_pi_map.keys()], "docstatus": 1}, + ) + + for pr_item in pr_items: + pr_item.customs_duty = item_customs_map.get(pr_pi_map.get(pr_item.name)) + + return _item_dict(pr_items) + + # Creating PR from PI (Qty split possible in PR) + pr_items = frappe.get_all( + "Purchase Receipt Item", + fields="*", + filters={"purchase_invoice": pi.name, "docstatus": 1}, + ) + + item_qty_map = {item.name: item.qty for item in pi.items} + + for pr_item in pr_items: + customs_duty_for_item = item_customs_map.get(pr_item.purchase_invoice_item) + total_qty = item_qty_map.get(pr_item.purchase_invoice_item) + pr_item.customs_duty = customs_duty_for_item * pr_item.qty / total_qty + + return _item_dict(pr_items) diff --git a/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py b/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py new file mode 100644 index 0000000000..2fb013dfb4 --- /dev/null +++ b/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py @@ -0,0 +1,107 @@ +# Copyright (c) 2023, Resilient Tech and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from india_compliance.gst_india.doctype.bill_of_entry.bill_of_entry import ( + make_bill_of_entry, + make_journal_entry_for_payment, + make_landed_cost_voucher, +) +from india_compliance.gst_india.utils.tests import create_transaction + + +class TestBillofEntry(FrappeTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + frappe.db.set_single_value("GST Settings", "enable_overseas_transactions", 1) + + def test_create_bill_of_entry(self): + pi = create_transaction( + doctype="Purchase Invoice", + supplier="_Test Foreign Supplier", + update_stock=1, + ) + + # Create BOE + boe = make_bill_of_entry(pi.name) + boe.items[0].customs_duty = 100 + boe.bill_of_entry_no = "123" + boe.bill_of_entry_date = today() + boe.save() + boe.submit() + + # Verify BOE + self.assertDocumentEqual( + { + "total_customs_duty": 100, + "total_taxes": 36, # 18% IGST on (100 + 100) + "total_amount_payable": 136, + }, + boe, + ) + + # Verify GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Bill of Entry", "voucher_no": boe.name}, + fields=["account", "debit", "credit"], + ) + + for gle in gl_entries: + if gle.account == boe.customs_expense_account: + self.assertEqual(gle.debit, boe.total_customs_duty) + elif "IGST" in gle.account: + self.assertEqual(gle.debit, boe.total_taxes) + elif gle.account == boe.customs_payable_account: + self.assertEqual(gle.credit, boe.total_amount_payable) + + # Create Journal Entry + je = make_journal_entry_for_payment(boe.name) + je.cheque_no = "123" + je.save() + je.submit() + + self.assertDocumentEqual( + { + "account": boe.customs_payable_account, + "debit": boe.total_amount_payable, + }, + je.accounts[0], + ) + self.assertEqual(je.total_debit, boe.total_amount_payable) + + # Create Landed Cost Voucher + lcv = make_landed_cost_voucher(boe.name) + lcv.save() + lcv.submit() + + item = pi.items[0] + self.assertDocumentEqual( + { + "purchase_receipts": [ + { + "receipt_document_type": "Purchase Invoice", + "receipt_document": pi.name, + } + ], + "items": [ + { + "item_code": item.item_code, + "purchase_receipt_item": item.name, + "applicable_charges": boe.total_customs_duty, + } + ], + "taxes": [ + { + "expense_account": boe.customs_expense_account, + "amount": boe.total_customs_duty, + } + ], + "distribute_charges_based_on": "Distribute Manually", + }, + lcv, + ) diff --git a/india_compliance/gst_india/doctype/bill_of_entry_item/__init__.py b/india_compliance/gst_india/doctype/bill_of_entry_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.json b/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.json new file mode 100644 index 0000000000..acba4cd5d5 --- /dev/null +++ b/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2023-01-27 15:11:21.223791", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_gcgg", + "item_name", + "section_break_jqkj", + "assessable_value", + "customs_duty", + "pi_detail", + "column_break_ifxl", + "taxable_value", + "item_tax_template", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "fieldname": "assessable_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Assessable Value", + "options": "INR", + "reqd": 1 + }, + { + "fieldname": "customs_duty", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Customs and Additional Charges", + "options": "INR" + }, + { + "fieldname": "taxable_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Taxable Value", + "options": "INR", + "read_only": 1 + }, + { + "fieldname": "item_tax_template", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Tax Template", + "options": "Item Tax Template" + }, + { + "fieldname": "column_break_gcgg", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_jqkj", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_ifxl", + "fieldtype": "Column Break" + }, + { + "fieldname": "pi_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "Purchase Invoice Item", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-03-13 14:26:53.580618", + "modified_by": "Administrator", + "module": "GST India", + "name": "Bill of Entry Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.py b/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.py new file mode 100644 index 0000000000..af158388ad --- /dev/null +++ b/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Resilient Tech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BillofEntryItem(Document): + pass diff --git a/india_compliance/gst_india/doctype/bill_of_entry_taxes/__init__.py b/india_compliance/gst_india/doctype/bill_of_entry_taxes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/india_compliance/gst_india/doctype/bill_of_entry_taxes/bill_of_entry_taxes.json b/india_compliance/gst_india/doctype/bill_of_entry_taxes/bill_of_entry_taxes.json new file mode 100644 index 0000000000..5b89b380e6 --- /dev/null +++ b/india_compliance/gst_india/doctype/bill_of_entry_taxes/bill_of_entry_taxes.json @@ -0,0 +1,107 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2023-01-30 16:08:05.181561", + "default_view": "List", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "charge_type", + "col_break1", + "account_head", + "section_break_10", + "rate", + "column_break_pipk", + "tax_amount", + "total", + "item_wise_tax_rates" + ], + "fields": [ + { + "columns": 2, + "default": "On Net Total", + "fieldname": "charge_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "oldfieldname": "charge_type", + "oldfieldtype": "Select", + "options": "\nActual\nOn Net Total", + "reqd": 1 + }, + { + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "account_head", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account Head", + "oldfieldname": "account_head", + "oldfieldtype": "Link", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "columns": 2, + "fieldname": "rate", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Rate", + "oldfieldname": "rate", + "oldfieldtype": "Currency" + }, + { + "fieldname": "column_break_pipk", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "tax_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "oldfieldname": "tax_amount", + "oldfieldtype": "Currency", + "options": "INR" + }, + { + "columns": 2, + "fieldname": "total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total", + "oldfieldname": "total", + "oldfieldtype": "Currency", + "options": "INR", + "read_only": 1 + }, + { + "fieldname": "item_wise_tax_rates", + "fieldtype": "Code", + "hidden": 1, + "label": "Item Wise Tax Rates" + } + ], + "istable": 1, + "links": [], + "modified": "2023-03-11 17:31:11.315202", + "modified_by": "Administrator", + "module": "GST India", + "name": "Bill of Entry Taxes", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/bill_of_entry_taxes/bill_of_entry_taxes.py b/india_compliance/gst_india/doctype/bill_of_entry_taxes/bill_of_entry_taxes.py new file mode 100644 index 0000000000..0a2ec01bbc --- /dev/null +++ b/india_compliance/gst_india/doctype/bill_of_entry_taxes/bill_of_entry_taxes.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Resilient Tech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BillofEntryTaxes(Document): + pass diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py index 80c146af63..54f76e48af 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py +++ b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py @@ -9,10 +9,11 @@ from frappe import _ from frappe.model.document import Document from frappe.query_builder import DatePart -from frappe.query_builder.functions import Extract -from frappe.utils import cstr, flt +from frappe.query_builder.functions import Extract, Sum +from frappe.utils import cstr, flt, get_date_str, get_first_day, get_last_day from india_compliance.gst_india.constants import INVOICE_DOCTYPES +from india_compliance.gst_india.utils import get_gst_accounts_by_type class GSTR3BReport(Document): @@ -147,8 +148,36 @@ def get_itc_details(self): }, ) + self.update_imports_from_bill_of_entry(itc_details) + return itc_details + def update_imports_from_bill_of_entry(self, itc_details): + boe = frappe.qb.DocType("Bill of Entry") + boe_taxes = frappe.qb.DocType("Bill of Entry Taxes") + gst_accounts = get_gst_accounts_by_type(self.company, "Input") + + def _get_tax_amount(account_type): + return ( + frappe.qb.from_(boe) + .select(Sum(boe_taxes.tax_amount)) + .join(boe_taxes) + .on(boe_taxes.parent == boe.name) + .where( + boe.posting_date.between( + get_date_str(get_first_day(f"{self.year}-{self.month_no}-01")), + get_date_str(get_last_day(f"{self.year}-{self.month_no}-01")), + ) + & boe.company_gstin.eq(self.gst_details.get("gstin")) + & boe.docstatus.eq(1) + & boe_taxes.account_head.eq(gst_accounts[account_type]) + ) + .run() + )[0][0] or 0 + + igst, cess = _get_tax_amount("igst_account"), _get_tax_amount("cess_account") + itc_details.setdefault("Import Of Capital Goods", {"iamt": igst, "csamt": cess}) + def get_inward_nil_exempt(self, state): inward_nil_exempt = frappe.db.sql( """ diff --git a/india_compliance/gst_india/overrides/company.py b/india_compliance/gst_india/overrides/company.py index baf4a46310..519c0491cc 100644 --- a/india_compliance/gst_india/overrides/company.py +++ b/india_compliance/gst_india/overrides/company.py @@ -19,18 +19,36 @@ def delete_gst_settings_for_company(doc, method=None): gst_settings.save() -def create_default_tax_templates(doc, method=None): - if not frappe.flags.country_change: +def make_company_fixtures(doc, method=None): + if not frappe.flags.country_change or doc.country != "India": return - make_default_tax_templates(doc.name, doc.country) + create_company_fixtures(doc.name) -@frappe.whitelist() -def make_default_tax_templates(company: str, country: str): - if country != "India": - return +def create_company_fixtures(company): + make_default_tax_templates(company) + make_default_customs_accounts(company) + + +def make_default_customs_accounts(company): + create_default_company_account( + company, + account_name="Customs Duty Payable", + parent="Duties and Taxes", + default_fieldname="default_customs_payable_account", + ) + + create_default_company_account( + company, + account_name="Customs Duty Expense", + parent="Stock Expenses", + default_fieldname="default_customs_expense_account", + ) + +@frappe.whitelist() +def make_default_tax_templates(company: str): if not frappe.db.exists("Company", company): frappe.throw( _("Company {0} does not exist yet. Taxes setup aborted.").format(company) @@ -131,3 +149,35 @@ def add_accounts_in_gst_settings( "account_type": account_type, }, ) + + +def create_default_company_account( + company, + account_name, + parent, + default_fieldname=None, +): + parent_account = frappe.db.get_value( + "Account", filters={"account_name": parent, "company": company} + ) + + if not parent_account: + return + + account = frappe.get_doc( + { + "doctype": "Account", + "account_name": account_name, + "parent_account": parent_account, + "company": company, + "is_group": 0, + "account_type": "Tax", + } + ) + account.flags.ignore_permissions = True + account.insert(ignore_if_duplicate=True) + + if default_fieldname: + frappe.db.set_value( + "Company", company, default_fieldname, account.name, update_modified=False + ) diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py index 8ec832d9d6..aa1b910642 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -43,3 +43,16 @@ def validate_supplier_gstin(doc): _("Supplier GSTIN and Company GSTIN cannot be the same"), title=_("Invalid Supplier GSTIN"), ) + + +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}, + ), + ) diff --git a/india_compliance/gst_india/overrides/test_company.py b/india_compliance/gst_india/overrides/test_company.py index e61fff5207..92e6ae9248 100644 --- a/india_compliance/gst_india/overrides/test_company.py +++ b/india_compliance/gst_india/overrides/test_company.py @@ -38,7 +38,6 @@ def test_company_exist(self): re.compile(r"^(.*does not exist yet.*)$"), make_default_tax_templates, "Random Company Name", - "India", ) def test_tax_defaults_setup(self): diff --git a/india_compliance/gst_india/setup/__init__.py b/india_compliance/gst_india/setup/__init__.py index 4d339d626f..ec0f20210d 100644 --- a/india_compliance/gst_india/setup/__init__.py +++ b/india_compliance/gst_india/setup/__init__.py @@ -5,6 +5,9 @@ create_custom_fields as _create_custom_fields, ) from frappe.utils import now_datetime, nowdate +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + make_dimension_in_accounting_doctypes, +) from india_compliance.gst_india.constants.custom_fields import ( CUSTOM_FIELDS, @@ -21,6 +24,7 @@ def after_install(): create_custom_fields() + create_accounting_dimension_fields() create_property_setters() create_address_template() set_default_gst_settings() @@ -36,6 +40,18 @@ def create_custom_fields(): _create_custom_fields(get_all_custom_fields(), ignore_validate=True) +def create_accounting_dimension_fields(): + doctypes = frappe.get_hooks( + "accounting_dimension_doctypes", + app_name="india_compliance", + ) + + dimensions = frappe.get_all("Accounting Dimension", pluck="name") + for dimension in dimensions: + doc = frappe.get_doc("Accounting Dimension", dimension) + make_dimension_in_accounting_doctypes(doc, doctypes) + + def create_property_setters(): for property_setter in get_property_setters(): frappe.make_property_setter(property_setter) diff --git a/india_compliance/gst_india/setup/property_setters.py b/india_compliance/gst_india/setup/property_setters.py index e227c2df2f..3887ecd729 100644 --- a/india_compliance/gst_india/setup/property_setters.py +++ b/india_compliance/gst_india/setup/property_setters.py @@ -24,6 +24,12 @@ def get_property_setters(): "naming_series", ["PINV-.YY.-", "PRET-.YY.-", ""], ), + get_options_property_setter( + "Journal Entry Account", + "reference_type", + ["Bill of Entry"], + prepend=False, + ), { "doctype": "Address", "fieldname": "state", diff --git a/india_compliance/gst_india/uninstall.py b/india_compliance/gst_india/uninstall.py index f24ab940e1..a4807dca39 100644 --- a/india_compliance/gst_india/uninstall.py +++ b/india_compliance/gst_india/uninstall.py @@ -11,6 +11,7 @@ def before_uninstall(): delete_custom_fields(get_all_custom_fields()) delete_property_setters() + delete_accounting_dimension_fields() remove_fields_from_item_variant_settings() @@ -28,6 +29,16 @@ def delete_property_setters(): frappe.db.delete("Property Setter", property_setter) +def delete_accounting_dimension_fields(): + doctypes = frappe.get_hooks( + "accounting_dimension_doctypes", + app_name="india_compliance", + ) + + fieldnames = frappe.get_all("Accounting Dimension", fields=["fieldname"]) + delete_custom_fields({doctype: fieldnames for doctype in doctypes}) + + def remove_fields_from_item_variant_settings(): settings = frappe.get_doc("Item Variant Settings") settings.fields = [ diff --git a/india_compliance/gst_india/utils/__init__.py b/india_compliance/gst_india/utils/__init__.py index 14bb0e1d53..331a630d1f 100644 --- a/india_compliance/gst_india/utils/__init__.py +++ b/india_compliance/gst_india/utils/__init__.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.desk.form.load import get_docinfo, run_onload -from frappe.utils import cstr, get_datetime, get_time_zone +from frappe.utils import cstr, get_datetime, get_system_timezone from erpnext.controllers.taxes_and_totals import ( get_itemised_tax, get_itemised_taxable_amount, @@ -325,7 +325,7 @@ def parse_datetime(value, day_first=False): return parsed = parser.parse(value, dayfirst=day_first) - system_tz = get_time_zone() + system_tz = get_system_timezone() if system_tz == TIMEZONE: return parsed.replace(tzinfo=None) @@ -343,7 +343,7 @@ def as_ist(value=None): """Convert system time to offset-naive IST time""" parsed = get_datetime(value) - system_tz = get_time_zone() + system_tz = get_system_timezone() if system_tz == TIMEZONE: return parsed diff --git a/india_compliance/gst_india/utils/test_e_invoice.py b/india_compliance/gst_india/utils/test_e_invoice.py index d9e662bf3f..780cca8341 100644 --- a/india_compliance/gst_india/utils/test_e_invoice.py +++ b/india_compliance/gst_india/utils/test_e_invoice.py @@ -61,6 +61,7 @@ def test_get_data(self): }, ) si.save() + si.submit() self.assertRaisesRegex( frappe.exceptions.ValidationError, diff --git a/india_compliance/gst_india/utils/transaction_data.py b/india_compliance/gst_india/utils/transaction_data.py index ed6de7a1c6..568864c184 100644 --- a/india_compliance/gst_india/utils/transaction_data.py +++ b/india_compliance/gst_india/utils/transaction_data.py @@ -184,6 +184,14 @@ def set_transporter_details(self): ) def validate_transaction(self): + if self.doc.docstatus > 1: + frappe.throw( + msg=_( + "Cannot generate e-Waybill or e-Invoice for a cancelled transaction" + ), + title=_("Invalid Document State"), + ) + posting_date = getdate(self.doc.posting_date) if posting_date > getdate(): diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 071acf88ac..998f2f9421 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -29,6 +29,7 @@ "Item": "gst_india/client_scripts/item.js", "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", "Sales Invoice": [ "gst_india/client_scripts/e_invoice_actions.js", "gst_india/client_scripts/e_waybill_actions.js", @@ -55,7 +56,7 @@ "on_trash": "india_compliance.gst_india.overrides.company.delete_gst_settings_for_company", "on_update": [ "india_compliance.income_tax_india.overrides.company.make_company_fixtures", - "india_compliance.gst_india.overrides.company.create_default_tax_templates", + "india_compliance.gst_india.overrides.company.make_company_fixtures", ], "validate": "india_compliance.gst_india.overrides.party.validate_party", }, @@ -81,6 +82,7 @@ ) }, "Purchase Invoice": { + "onload": "india_compliance.gst_india.overrides.purchase_invoice.onload", "validate": "india_compliance.gst_india.overrides.purchase_invoice.validate", }, "Purchase Order": { @@ -180,6 +182,8 @@ # Links to these doctypes will be ignored when deleting a document ignore_links_on_delete = ["e-Waybill Log", "e-Invoice Log"] +accounting_dimension_doctypes = ["Bill of Entry", "Bill of Entry Item"] + # Includes in # ------------------ diff --git a/india_compliance/income_tax_india/overrides/company.py b/india_compliance/income_tax_india/overrides/company.py index bdd93160c3..78f4317cb1 100644 --- a/india_compliance/income_tax_india/overrides/company.py +++ b/india_compliance/income_tax_india/overrides/company.py @@ -2,6 +2,8 @@ from frappe.utils import today from erpnext.accounts.utils import FiscalYearError, get_fiscal_year +from india_compliance.gst_india.overrides.company import create_default_company_account + def make_company_fixtures(doc, method=None): if not frappe.flags.country_change or doc.country != "India": @@ -11,35 +13,17 @@ def make_company_fixtures(doc, method=None): def create_company_fixtures(company): - docs = [] company = company or frappe.db.get_value("Global Defaults", None, "default_company") - set_tds_account(docs, company) - - for d in docs: - doc = frappe.get_doc(d) - doc.flags.ignore_permissions = True - doc.insert(ignore_if_duplicate=True) + create_tds_account(company) # create records for Tax Withholding Category set_tax_withholding_category(company) -def set_tds_account(docs, company): - parent_account = frappe.db.get_value( - "Account", filters={"account_name": "Duties and Taxes", "company": company} +def create_tds_account(company): + create_default_company_account( + company, account_name="TDS Payable", parent="Duties and Taxes" ) - if parent_account: - docs.extend( - [ - { - "doctype": "Account", - "account_name": "TDS Payable", - "account_type": "Tax", - "parent_account": parent_account, - "company": company, - } - ] - ) def set_tax_withholding_category(company): diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index 41585a914e..2ef72bbe83 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -3,9 +3,10 @@ [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() #6 -execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() +execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #7 +execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #2 india_compliance.patches.post_install.update_custom_role_for_e_invoice_summary india_compliance.patches.v14.remove_ecommerce_gstin_from_purchase_invoice india_compliance.patches.v14.set_sandbox_mode_in_gst_settings execute:from india_compliance.gst_india.setup import add_fields_to_item_variant_settings; add_fields_to_item_variant_settings() +execute:from india_compliance.gst_india.setup import create_accounting_dimension_fields; create_accounting_dimension_fields() diff --git a/india_compliance/patches/post_install/create_company_fixtures.py b/india_compliance/patches/post_install/create_company_fixtures.py index 11288c413d..d38152cfb9 100644 --- a/india_compliance/patches/post_install/create_company_fixtures.py +++ b/india_compliance/patches/post_install/create_company_fixtures.py @@ -1,7 +1,11 @@ import frappe -from india_compliance.gst_india.overrides.company import make_default_tax_templates -from india_compliance.income_tax_india.overrides.company import create_company_fixtures +from india_compliance.gst_india.overrides.company import ( + create_company_fixtures as create_gst_fixtures, +) +from india_compliance.income_tax_india.overrides.company import ( + create_company_fixtures as create_income_tax_fixtures, +) """ This patch is used to create company fixtures for Indian Companies created before installing India Compliance. @@ -15,8 +19,8 @@ def execute(): if not frappe.db.exists( "Account", {"company": company, "account_name": "TDS Payable"} ): - create_company_fixtures(company) + create_income_tax_fixtures(company) # GST fixtures if not frappe.db.exists("GST Account", {"company": company}): - make_default_tax_templates(company, "India") + create_gst_fixtures(company) diff --git a/india_compliance/public/js/transaction.js b/india_compliance/public/js/transaction.js index 8c68dc7fd1..0b434c0d19 100644 --- a/india_compliance/public/js/transaction.js +++ b/india_compliance/public/js/transaction.js @@ -63,14 +63,15 @@ async function update_gst_details(frm, event) { }; // wait for GSTINs to get fetched - await frappe.after_ajax().then(() => { - frm.__gst_update_triggered = false; + await frappe.after_ajax(); - if (frm.__update_place_of_supply) { - args.update_place_of_supply = 1; - frm.__update_place_of_supply = false; - } - }); + // reset flags + frm.__gst_update_triggered = false; + + if (frm.__update_place_of_supply) { + args.update_place_of_supply = 1; + frm.__update_place_of_supply = false; + } const party_details = {}; diff --git a/india_compliance/tests/test_records.json b/india_compliance/tests/test_records.json index dd82d14958..218d7377c3 100644 --- a/india_compliance/tests/test_records.json +++ b/india_compliance/tests/test_records.json @@ -168,6 +168,13 @@ "supplier_type": "Individual", "gstin": "", "gst_category": "Unregistered" + }, + { + "name": "_Test Foreign Supplier", + "supplier_name": "_Test Foreign Supplier", + "supplier_type": "Individual", + "gstin": "", + "gst_category": "Overseas" } ], "Address": [