diff --git a/landms/landms/doctype/land_acquisition/land_acquisition.py b/landms/landms/doctype/land_acquisition/land_acquisition.py index 4d48b3b..34b84cf 100644 --- a/landms/landms/doctype/land_acquisition/land_acquisition.py +++ b/landms/landms/doctype/land_acquisition/land_acquisition.py @@ -972,35 +972,51 @@ def _finish(log, cost_to_save=None): continue try: - sr = frappe.get_doc({ - "doctype": "Stock Reconciliation", - "purpose": "Stock Reconciliation", - "company": la.company, - "posting_date": today(), - "expense_account": lud_account, - "cost_center": cost_center, - "items": [{ - "item_code": item_code, - "warehouse": warehouse, - "qty": 1, - "valuation_rate": new_plot_cost, - "serial_no": plot.serial_no or plot.name, - "use_serial_batch_fields": 1, - }], - }) - sr.insert(ignore_permissions=True) - sr.submit() - frappe.db.set_value("Plot Master", plot.name, { "allocated_cost": new_plot_cost, "cost_per_sqm": flt(cost_per_sqm), }, update_modified=False) + + if plot.serial_no: + frappe.db.set_value("Serial No", plot.serial_no, "purchase_rate", new_plot_cost, update_modified=False) + + # Update original stock entry SLE + Serial and Batch Bundle + # so Repost and future DN outgoing rates use the correct cost + if plot.stock_entry: + sle_name = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": plot.stock_entry, "is_cancelled": 0}, + "name" + ) + if sle_name: + frappe.db.set_value("Stock Ledger Entry", sle_name, { + "incoming_rate": new_plot_cost, + "stock_value_difference": new_plot_cost, + }, update_modified=False) + + # Update the inward Serial and Batch Bundle avg_rate + bundle_name = frappe.db.get_value( + "Stock Entry Detail", + {"parent": plot.stock_entry, "serial_no": plot.serial_no}, + "serial_and_batch_bundle" + ) + if bundle_name: + frappe.db.set_value("Serial and Batch Bundle", bundle_name, { + "avg_rate": new_plot_cost, + "total_amount": new_plot_cost, + }, update_modified=False) + frappe.db.sql(""" + UPDATE `tabSerial and Batch Entry` + SET incoming_rate=%s + WHERE parent=%s AND serial_no=%s + """, (new_plot_cost, bundle_name, plot.serial_no)) + item_codes_touched.add(item_code) updated.append(f"{plot.name} ({old_plot_cost:.0f} → {new_plot_cost:.0f})") except Exception: - frappe.log_error(frappe.get_traceback(), f"Plot cost recalc SR failed: {plot.name}") - failures.append(f"{plot.name} — stock reconciliation failed (see Error Log)") + frappe.log_error(frappe.get_traceback(), f"Plot cost recalc failed: {plot.name}") + failures.append(f"{plot.name} — cost update failed (see Error Log)") # Repost Item Valuation — one per unique item code for ic in item_codes_touched: diff --git a/landms/landms/doctype/plot_handover/plot_handover.py b/landms/landms/doctype/plot_handover/plot_handover.py index 8189384..4b6655d 100644 --- a/landms/landms/doctype/plot_handover/plot_handover.py +++ b/landms/landms/doctype/plot_handover/plot_handover.py @@ -1,6 +1,6 @@ import frappe from frappe.model.document import Document -from frappe.utils import get_fullname, today +from frappe.utils import flt, get_fullname, today from landms.landms.doctype.land_acquisition.land_acquisition import sync_land_acquisition_plot_summary from landms.landms.doctype.plot_master.plot_master import get_plot_item_code @@ -95,6 +95,8 @@ def _ensure_delivery_note(self): if not plot.serial_no: frappe.throw(f"Plot {plot.name} is missing its Serial No.") + self._cancel_blocking_stock_reconciliations(plot.serial_no) + if contract.sales_order and frappe.db.exists("Sales Order", contract.sales_order): dn = self._make_delivery_note_from_sales_order(contract.sales_order, plot) else: @@ -125,11 +127,13 @@ def _make_delivery_note_from_sales_order(self, sales_order_name, plot): if not dn.items: frappe.throw(f"Sales Order {sales_order_name} has no deliverable rows for handover.") + inv_warehouse = frappe.db.get_single_value("LandMS Settings", "plot_inventory_warehouse") for row in dn.items: row.qty = 1 row.serial_no = plot.serial_no row.use_serial_batch_fields = 1 - row.warehouse = row.warehouse or frappe.db.get_single_value("LandMS Settings", "plot_inventory_warehouse") + row.warehouse = inv_warehouse + row.incoming_rate = flt(plot.allocated_cost) return dn @@ -147,6 +151,7 @@ def _make_delivery_note_direct(self, contract, plot, settings): "item_code": item_code, "qty": 1, "rate": plot.selling_price, + "incoming_rate": flt(plot.allocated_cost), "warehouse": settings.plot_inventory_warehouse, "serial_no": plot.serial_no, "use_serial_batch_fields": 1, @@ -156,6 +161,48 @@ def _make_delivery_note_direct(self, contract, plot, settings): } ) + def _cancel_blocking_stock_reconciliations(self, serial_no): + # Cancel ALL SRs for this LA newest-first to avoid chained dependency errors. + # A single SR can reference multiple plot serials so we must clear the whole LA. + la = frappe.db.get_value("Plot Master", self.plot, "land_acquisition") + if not la: + return + + sr_names = frappe.db.sql(""" + SELECT DISTINCT sr.name + FROM `tabStock Reconciliation` sr + JOIN `tabStock Reconciliation Item` sri ON sri.parent = sr.name + WHERE sri.serial_no IN ( + SELECT serial_no FROM `tabPlot Master` + WHERE land_acquisition = %s AND docstatus = 1 AND serial_no IS NOT NULL + ) AND sr.docstatus = 1 + ORDER BY sr.posting_date DESC, sr.name DESC + """, la, pluck="name") + + if not sr_names: + return + + for name in sr_names: + try: + sr = frappe.get_doc("Stock Reconciliation", name) + sr.flags.ignore_permissions = True + sr.cancel() + frappe.db.commit() + except Exception: + frappe.log_error(frappe.get_traceback(), f"Could not cancel blocking SR {name} for serial {serial_no}") + + # Reset any plot serials that are stuck in Delivered state after SR cancellations + plot_serials = frappe.db.sql(""" + SELECT serial_no FROM `tabPlot Master` + WHERE land_acquisition = %s AND docstatus = 1 + AND serial_no IS NOT NULL AND status NOT IN ('Delivered', 'Title Closed') + """, la, pluck="serial_no") + + for sn in plot_serials: + sn_status = frappe.db.get_value("Serial No", sn, "status") + if sn_status == "Delivered": + frappe.db.set_value("Serial No", sn, "status", "Active") + def _cancel_delivery_note(self): if not self.delivery_note or not frappe.db.exists("Delivery Note", self.delivery_note): return