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
71 changes: 66 additions & 5 deletions india_compliance/gst_india/overrides/subcontracting_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,77 @@ def after_mapping_stock_entry(doc, method, source_doc):
doc.taxes_and_charges = ""
doc.taxes = []

if doc.purpose != "Material Transfer" or not doc.is_return:
set_item_tax_template(doc, source_doc)

address_map = _get_fields_mapping(doc, source_doc)

if not address_map:
return

doc.bill_to_address = source_doc.billing_address
doc.bill_from_address = source_doc.supplier_address
doc.bill_to_gstin = source_doc.company_gstin
doc.bill_from_gstin = source_doc.supplier_gstin
(bill_from_address, bill_from_gstin), (bill_to_address, bill_to_gstin) = address_map

doc.bill_from_address = source_doc.get(bill_from_address)
doc.bill_from_gstin = source_doc.get(bill_from_gstin)
doc.bill_to_address = source_doc.get(bill_to_address)
doc.bill_to_gstin = source_doc.get(bill_to_gstin)

set_address_display(doc)


def _get_fields_mapping(doc, source_doc):
from_fields = ("billing_address", "company_gstin")
to_fields = ("supplier_address", "supplier_gstin")

if source_doc.doctype == "Subcontracting Order":
if doc.purpose == "Send to Subcontractor":
return from_fields, to_fields

elif doc.purpose == "Material Transfer" and doc.is_return:
return to_fields, from_fields

elif (
source_doc.doctype == "Purchase Receipt" and doc.purpose == "Material Transfer" and not doc.is_return
):
return from_fields, to_fields

elif source_doc.doctype == "Stock Entry" and doc.purpose == "Material Transfer" and not doc.is_return:
from_fields = ("bill_from_address", "bill_from_gstin")
to_fields = ("bill_to_address", "bill_to_gstin")

return from_fields, to_fields

return None


def set_item_tax_template(doc, source_doc):
if source_doc.doctype not in ("Subcontracting Order", "Purchase Order"):
return

rm_detail_field = None
if source_doc.doctype == "Subcontracting Order":
rm_detail_field = "sco_rm_detail"

elif source_doc.doctype == "Purchase Order":
rm_detail_field = "po_detail"

item_tax_template_map = frappe._dict()
for supplied_item in source_doc.supplied_items:
item_tax_template = next(
(
item.item_tax_template
for item in source_doc.items
if supplied_item.get("reference_name") == item.name
),
None,
)

if item_tax_template:
item_tax_template_map[supplied_item.name] = item_tax_template

for item in doc.items:
item.item_tax_template = item_tax_template_map.get(item.get(rm_detail_field))
Comment on lines +149 to +150
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Second loop silently nulls out unmatched item tax templates

item_tax_template_map is only populated for supplied items whose corresponding source order item actually has an item_tax_template set (line 153 guards with if item_tax_template:). For every doc.items row whose sco_rm_detail either doesn't match any key in the map or whose source item had no template, the assignment unconditionally writes None, destroying whatever template the mapping process may have already placed on the item.

Consider only overwriting when a real value is found:

    for item in doc.items:
        template = item_tax_template_map.get(item.get(rm_detail_field))
        if template:
            item.item_tax_template = template

This preserves any template that was already correctly set on items that lack a lookup match.



def before_mapping_subcontracting_receipt(doc, method, source_doc, table_maps):
table_maps["India Compliance Taxes and Charges"] = {
"doctype": "India Compliance Taxes and Charges",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
from erpnext.manufacturing.doctype.production_plan.test_production_plan import (
make_bom,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_stock_entry as make_se_from_pr,
)
from erpnext.stock.doctype.stock_entry.stock_entry import make_stock_in_entry
from erpnext.subcontracting.doctype.subcontracting_order.test_subcontracting_order import (
create_subcontracting_order,
)
Expand Down Expand Up @@ -380,3 +384,113 @@ def test_validation_when_gstin_field_empty(self):
sco.supplier_warehouse = "Finished Goods - _TIUC"
sco.save()
sco.submit()


class TestAddressMappingAfterMapping(IntegrationTestCase):
"""
Verifies bill_from_address / bill_to_address and their GSTINs are mapped
correctly in Stock Entries created via get_mapped_doc from each source doctype.

Scenarios (mirrors _get_fields_mapping logic):
1. Subcontracting Order → SE "Send to Subcontractor"
2. Subcontracting Order → SE "Material Transfer" (return of inputs)
3. Purchase Receipt → SE "Material Transfer"
4. Stock Entry → SE "Material Transfer"
"""

@classmethod
def setUpClass(cls):
super().setUpClass()
frappe.db.savepoint("before_test_address_mapping")
create_subcontracting_data()

@classmethod
def tearDownClass(cls):
super().tearDownClass()
frappe.db.rollback(save_point="before_test_address_mapping")

def _make_sco(self):
po = create_purchase_order(**SERVICE_ITEM, supplier_warehouse="Finished Goods - _TIRC")
return create_subcontracting_order(po_name=po.name)

def test_sco_to_se_send_to_subcontractor(self):
sco = self._make_sco()
rm_items = get_rm_items(sco.supplied_items)

se = make_rm_stock_entry(sco.name, rm_items)

self.assertEqual(se.purpose, "Send to Subcontractor")
self.assertEqual(se.bill_from_address, sco.billing_address)
self.assertEqual(se.bill_from_gstin, sco.company_gstin)
self.assertEqual(se.bill_to_address, sco.supplier_address)
self.assertEqual(se.bill_to_gstin, sco.supplier_gstin)

def test_sco_to_se_material_transfer_return(self):
sco = self._make_sco()
rm_items = get_rm_items(sco.supplied_items)

# Materials must reach the supplier warehouse before they can be returned.
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
bill_from_address=sco.billing_address,
bill_to_address=sco.supplier_address,
)

return_se = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])

self.assertEqual(return_se.purpose, "Material Transfer")
self.assertTrue(return_se.is_return)
# Supplier becomes the sender; company becomes the receiver.
self.assertEqual(return_se.bill_from_address, sco.supplier_address)
self.assertEqual(return_se.bill_from_gstin, sco.supplier_gstin)
self.assertEqual(return_se.bill_to_address, sco.billing_address)
self.assertEqual(return_se.bill_to_gstin, sco.company_gstin)

def test_pr_to_se_material_transfer(self):
pr = create_transaction(doctype="Purchase Receipt")

se = make_se_from_pr(pr.name)

self.assertEqual(se.purpose, "Material Transfer")
self.assertEqual(se.bill_from_address, pr.billing_address)
self.assertEqual(se.bill_from_gstin, pr.company_gstin)
self.assertEqual(se.bill_to_address, pr.supplier_address)
self.assertEqual(se.bill_to_gstin, pr.supplier_gstin)

def test_se_to_se_material_transfer(self):
# Add stock so the Material Transfer SE can be submitted.
create_transaction(doctype="Purchase Receipt")

source_se = frappe.get_doc(
{
"doctype": "Stock Entry",
"purpose": "Material Transfer",
"stock_entry_type": "Material Transfer",
"company": "_Test Indian Registered Company",
"bill_from_address": "_Test Indian Registered Company-Billing",
"bill_from_gstin": "24AAQCA8719H1ZC",
"bill_to_address": "_Test Registered Supplier-Billing",
"bill_to_gstin": "24AABCR6898M1ZN",
"bill_to_gst_category": "Registered Regular",
"items": [
{
"item_code": "_Test Trading Goods 1",
"qty": 1,
"gst_hsn_code": "61149090",
"s_warehouse": "Stores - _TIRC",
"t_warehouse": "Finished Goods - _TIRC",
}
],
}
)
source_se.save()
source_se.submit()

target_se = make_stock_in_entry(source_se.name)

self.assertEqual(target_se.purpose, "Material Transfer")
self.assertEqual(target_se.bill_from_address, source_se.bill_from_address)
self.assertEqual(target_se.bill_from_gstin, source_se.bill_from_gstin)
self.assertEqual(target_se.bill_to_address, source_se.bill_to_address)
self.assertEqual(target_se.bill_to_gstin, source_se.bill_to_gstin)
Loading