diff --git a/india_compliance/gst_india/overrides/subcontracting_transaction.py b/india_compliance/gst_india/overrides/subcontracting_transaction.py index 095945084e..ab099a6884 100644 --- a/india_compliance/gst_india/overrides/subcontracting_transaction.py +++ b/india_compliance/gst_india/overrides/subcontracting_transaction.py @@ -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)) + + def before_mapping_subcontracting_receipt(doc, method, source_doc, table_maps): table_maps["India Compliance Taxes and Charges"] = { "doctype": "India Compliance Taxes and Charges", diff --git a/india_compliance/gst_india/overrides/test_subcontracting_transaction.py b/india_compliance/gst_india/overrides/test_subcontracting_transaction.py index 41438cae7c..4ea3ebfe08 100644 --- a/india_compliance/gst_india/overrides/test_subcontracting_transaction.py +++ b/india_compliance/gst_india/overrides/test_subcontracting_transaction.py @@ -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, ) @@ -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)