Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,14 @@ function get_sub_suppy_type_options(frm, is_foreign_transaction) {
if (frm.doc.purpose === "Send to Subcontractor") {
supply_type = "Outward";
sub_supply_type = ["Job Work"];
} else if (frm.doc.purpose === "Subcontracting Delivery") {
supply_type = "Outward";
sub_supply_type = ["Others"];
sub_supply_desc = "Job Work Delivery";
} else if (frm.doc.purpose === "Return Raw Material to Customer") {
supply_type = "Outward";
sub_supply_type = ["Others"];
sub_supply_desc = "Return Raw Material";
} else if (["Material Transfer", "Material Issue"].includes(frm.doc.purpose)) {
const same_gstin = frm.doc.bill_from_gstin === frm.doc.bill_to_gstin;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,13 @@ class StockEntryEwaybill extends EwaybillApplicability {
if (
!gst_settings.enable_e_waybill ||
!gst_settings.enable_e_waybill_for_sc ||
!["Material Transfer", "Material Issue", "Send to Subcontractor"].includes(
this.frm.doc.purpose
)
![
"Material Transfer",
"Material Issue",
"Send to Subcontractor",
"Subcontracting Delivery",
"Return Raw Material to Customer",
].includes(this.frm.doc.purpose)
)
return false;

Expand All @@ -269,7 +273,12 @@ class StockEntryEwaybill extends EwaybillApplicability {

const same_gstin = this.frm.doc.bill_from_gstin === this.frm.doc.bill_to_gstin;
const applicable_for_same_gstin = !(
is_return || this.frm.doc.purpose === "Send to Subcontractor"
is_return ||
[
"Send to Subcontractor",
"Subcontracting Delivery",
"Return Raw Material to Customer",
].includes(this.frm.doc.purpose)
);

if (same_gstin && !applicable_for_same_gstin) {
Expand Down Expand Up @@ -319,9 +328,13 @@ class StockEntryEwaybill extends EwaybillApplicability {

is_e_waybill_api_enabled() {
return (
["Material Transfer", "Material Issue", "Send to Subcontractor"].includes(
this.frm.doc.purpose
) &&
[
"Material Transfer",
"Material Issue",
"Send to Subcontractor",
"Subcontracting Delivery",
"Return Raw Material to Customer",
].includes(this.frm.doc.purpose) &&
super.is_e_waybill_api_enabled() &&
gst_settings.enable_e_waybill_for_sc
);
Expand Down
29 changes: 27 additions & 2 deletions india_compliance/gst_india/client_scripts/stock_entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ frappe.ui.form.on(DOCTYPE, {
if (is_e_waybill_applicable(frm) && !is_e_waybill_generatable(frm))
frappe.show_alert(
{
message: __("Supplier Address is required to create e-Waybill"),
message: __("Party Address is required to create e-Waybill"),
indicator: "yellow",
},
10
Expand Down Expand Up @@ -112,7 +112,7 @@ frappe.ui.form.on(DOCTYPE, {
},

company(frm) {
if (frm.doc.company && frm.doc.purpose === "Send to Subcontractor") {
if (frm.doc.company && is_subcontracting_entry(frm)) {
frappe.call({
method: "frappe.contacts.doctype.address.address.get_default_address",
args: {
Expand Down Expand Up @@ -207,6 +207,20 @@ function get_items(doc) {
return Array.from(new Set(doc.items.map(row => row.item_code)));
}

function is_subcontracting_entry(frm) {
return [
"Send to Subcontractor",
"Subcontracting Delivery",
"Return Raw Material to Customer",
].includes(frm.doc.purpose);
}

function is_subcontracting_inward_entry(frm) {
return ["Subcontracting Delivery", "Return Raw Material to Customer"].includes(
frm.doc.purpose
);
}

function get_field_and_label(frm, field) {
let field_label_dict = {};

Expand All @@ -219,6 +233,17 @@ function get_field_and_label(frm, field) {
],
company_field: ["bill_to_address", __("Bill To")],
};
} else if (is_subcontracting_inward_entry(frm)) {
// For Subcontracting Inward related entries
// company bills to the customer
field_label_dict = {
party_field: [
"bill_to_address",
__("Bill To (same as Customer Address)"),
__("Bill To"),
],
company_field: ["bill_from_address", __("Bill From")],
};
} else {
field_label_dict = {
party_field: [
Expand Down
14 changes: 14 additions & 0 deletions india_compliance/gst_india/constants/custom_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,20 @@
"print_hide": 1,
"hidden": 0,
},
{
"fieldname": "additional_taxable_value",
"label": "Additional Taxable Value",
"fieldtype": "Currency",
"insert_after": "taxable_value",
"options": "Company:company:default_currency",
"read_only": 1,
"translatable": 0,
"no_copy": 1,
"print_hide": 1,
"hidden": 0,
"depends_on": 'eval:["Subcontracting Delivery", "Return Raw Material to Customer"].includes(parent.purpose)',
"description": "Value of customer-provided materials for Subcontracting Inward",
},
],
"Subcontracting Receipt Item": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def onload(doc, method=None):


def validate(doc, method=None):

field_map = (
STOCK_ENTRY_FIELD_MAP
if doc.doctype == "Stock Entry"
Expand Down Expand Up @@ -539,6 +540,8 @@ def is_e_waybill_applicable(doc):
"Material Transfer",
"Material Issue",
"Send to Subcontractor",
"Subcontracting Delivery",
"Return Raw Material to Customer",
]:
return False

Expand Down
110 changes: 109 additions & 1 deletion india_compliance/gst_india/utils/taxes_controller.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from collections import defaultdict

import frappe
from frappe import _
Expand Down Expand Up @@ -117,6 +118,7 @@ def __init__(self, doc, field_map=None):

def set_taxes_and_totals(self):
self.set_item_wise_tax_rates()
self.set_additional_taxable_value()
self.update_item_taxable_value()
self.update_tax_amount()
self.update_base_grand_total()
Expand Down Expand Up @@ -154,7 +156,24 @@ def set_item_wise_tax_rates(self, item_name=None, tax_name=None):

def update_item_taxable_value(self):
for item in self.doc.get("items"):
item.taxable_value = self.get_value("amount", item)
taxable_value = self.get_value("amount", item)
taxable_value += flt(
item.get("additional_taxable_value", 0), item.precision("taxable_value")
)

item.taxable_value = taxable_value

def set_additional_taxable_value(self):
if self.doc.doctype != "Stock Entry" or not self.doc.items:
return

for item in self.doc.items:
item.additional_taxable_value = 0

if self.doc.purpose == "Subcontracting Delivery":
_set_subcontracting_delivery_additional_value(self.doc)
elif self.doc.purpose == "Return Raw Material to Customer":
_set_return_raw_material_additional_value(self.doc)

def update_tax_amount(self):
total_taxes = 0
Expand Down Expand Up @@ -272,3 +291,92 @@ def validate_taxes(doc):
tax.idx, doc.doctype
)
)


def _set_subcontracting_delivery_additional_value(doc):
"""
additional_taxable_value = SUM(received_items.rate * received_items.consumed_qty)
"""
scio_details = [item.scio_detail for item in doc.items if item.get("scio_detail")]

if not scio_details:
return

quantity_processed = frappe._dict(
frappe.get_all(
"Subcontracting Inward Order Item",
filters={"name": ["in", scio_details]},
fields=["name", "produced_qty"],
as_list=True,
)
)

received_items = frappe.get_all(
"Subcontracting Inward Order Received Item",
filters={
"reference_name": ["in", scio_details],
"is_customer_provided_item": 1,
},
fields=["reference_name", "rate", "consumed_qty"],
)

if not received_items:
return

# Calculate total material cost per FG item
fg_material_cost = defaultdict(float)
for received_item in received_items:
key = received_item.reference_name
cost = flt(received_item.rate) * flt(received_item.consumed_qty)
fg_material_cost[key] += cost

precision = doc.precision("additional_taxable_value", "items")

# Set additional_taxable_value for each item
for item in doc.items:
if not item.get("scio_detail"):
continue
item.additional_taxable_value = flt(
fg_material_cost.get(item.scio_detail)
/ quantity_processed.get(item.scio_detail, 1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZeroDivisionError when produced_qty is 0

quantity_processed.get(item.scio_detail, 1) uses 1 as the fallback only for missing keys. If the key exists but produced_qty is 0 (e.g., a subcontracting order where production has not yet completed), the method returns 0, causing a ZeroDivisionError when dividing fg_material_cost.get(item.scio_detail) by it.

Suggested change
/ quantity_processed.get(item.scio_detail, 1)
/ (quantity_processed.get(item.scio_detail) or 1)

* item.qty,
precision,
)
Comment on lines +339 to +344
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeError when SCIO detail has no customer-provided items

fg_material_cost is a defaultdict(float), but calling .get() on a defaultdict does not invoke the default factory — it returns None for missing keys. If any item.scio_detail is absent from fg_material_cost (i.e., that SCIO detail has no customer-provided received items), the expression None / quantity_processed.get(...) raises a TypeError at runtime.

The early-return guard at line 323 (if not received_items: return) only handles the case where no received items exist at all. A mixed scenario — some SCIO details have customer-provided items, others don't — reaches this loop and crashes.

Fix: use fg_material_cost.get(item.scio_detail, 0.0) (or index directly as fg_material_cost[item.scio_detail] which returns 0.0 via the defaultdict) and skip the item when the cost is zero:

Suggested change
item.additional_taxable_value = flt(
fg_material_cost.get(item.scio_detail)
/ quantity_processed.get(item.scio_detail, 1)
* item.qty,
precision,
)
material_cost = fg_material_cost.get(item.scio_detail, 0.0)
if not material_cost:
continue
item.additional_taxable_value = flt(
material_cost
/ quantity_processed.get(item.scio_detail, 1)
* item.qty,
precision,
)

Comment on lines +339 to +344
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against division by zero when produced_qty is 0.

If quantity_processed.get(item.scio_detail) returns 0 (not missing, but an actual 0 value), a ZeroDivisionError will occur. The default value of 1 only protects against missing keys.

While produced_qty being 0 may be unlikely in normal business flow for "Subcontracting Delivery", data integrity issues or edge cases could cause this.

🛡️ Proposed fix to guard against zero division
     # Set additional_taxable_value for each item
     for item in doc.items:
         if not item.get("scio_detail"):
             continue
+        produced_qty = quantity_processed.get(item.scio_detail) or 1
         item.additional_taxable_value = flt(
             fg_material_cost.get(item.scio_detail)
-            / quantity_processed.get(item.scio_detail, 1)
+            / produced_qty
             * item.qty,
             precision,
         )



def _set_return_raw_material_additional_value(doc):
"""
- additional_taxable_value = (SCIO Received Item rate * qty) - Stock Entry amount
"""
scio_details = [item.scio_detail for item in doc.items if item.get("scio_detail")]

if not scio_details:
return

received_items = frappe._dict(
frappe.get_all(
"Subcontracting Inward Order Received Item",
filters={
"name": ["in", scio_details],
},
fields=["name", "rate"],
as_list=True,
)
)

if not received_items:
return

precision = doc.precision("additional_taxable_value", "items")

for item in doc.items:
scio_detail = item.get("scio_detail")
if not scio_detail:
continue

scio_rate = received_items.get(scio_detail)
if not scio_rate:
continue

scio_value = flt(scio_rate) * flt(item.qty)
item.additional_taxable_value = flt(scio_value - flt(item.amount), precision)
Comment on lines +381 to +382
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Negative additional_taxable_value produces incorrect taxable amount

If the Stock Entry item.amount (qty × stock rate) exceeds the SCIO received-item rate × qty — which can happen when the internal valuation rate is higher than the customer-agreed rate — the subtraction yields a negative value. This negative additional_taxable_value is then added to taxable_value in update_item_taxable_value, silently reducing it below the actual item amount and producing an understated GST base.

Consider clamping the result to zero so that the addition in update_item_taxable_value is never negative:

Suggested change
scio_value = flt(scio_rate) * flt(item.qty)
item.additional_taxable_value = flt(scio_value - flt(item.amount), precision)
item.additional_taxable_value = flt(max(scio_value - flt(item.amount), 0), precision)

2 changes: 1 addition & 1 deletion india_compliance/patches.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ execute:from frappe.installer import add_module_defs; add_module_defs("india_com

[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() #69
execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #67
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patch counter decremented — new custom field may not be created on existing deployments

The counter was changed from #69#67. In Frappe's patching system the entire line (including the comment) is stored as the unique patch key in PatchLog. If #67 was already executed on any existing deployment (i.e., it was the value used in a prior release), changing back to #67 means that deployment will skip this execution and the new additional_taxable_value custom field will never be created.

The standard practice is to increment the counter (e.g., to #70) so the line is a previously-unseen key on every existing instance, guaranteeing create_custom_fields() is re-run and the new field is added.

Suggested change
execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #67
execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #70

execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #11
execute:from india_compliance.income_tax_india.setup import create_custom_fields; create_custom_fields() #4
india_compliance.patches.post_install.remove_old_fields #2
Expand Down
7 changes: 6 additions & 1 deletion india_compliance/public/js/taxes_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ india_compliance.taxes_controller = class TaxesController {

// Function to calculate amount
const calculateAmount = (qty, rate, precisionType) => {
return flt(flt(qty) * flt(rate), precision(precisionType, row));
let amount = flt(flt(qty) * flt(rate), precision(precisionType, row));

if (this.frm.doc.doctype === "Stock Entry") {
amount += flt(row.additional_taxable_value, precision(precisionType, row));
}
return amount;
};

if (this.frm.doc.doctype === "Subcontracting Receipt") {
Expand Down
10 changes: 7 additions & 3 deletions india_compliance/public/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,13 @@ Object.assign(india_compliance, {
if (doc.doctype != "Stock Entry") return true;

if (
!["Material Transfer", "Material Issue", "Send to Subcontractor"].includes(
doc.purpose
)
![
"Material Transfer",
"Material Issue",
"Send to Subcontractor",
"Subcontracting Delivery",
"Return Raw Material to Customer",
].includes(doc.purpose)
) {
return false;
}
Expand Down
Loading