diff --git a/.gitignore b/.gitignore index 22c0575..72d67d6 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,4 @@ build/ landms/landms/cleanup.py DOCUMENTATION.md TCB_INTEGRATION.md +docs/ diff --git a/landms/hooks.py b/landms/hooks.py index 71d2c22..75f73e2 100644 --- a/landms/hooks.py +++ b/landms/hooks.py @@ -7,6 +7,11 @@ required_apps = ["erpnext"] +doctype_js = { + "Purchase Order": "public/js/purchase_order.js", + "Purchase Invoice": "public/js/purchase_invoice.js", +} + after_install = "landms.install.after_install" after_migrate = ["landms.utils.import_setup_data"] diff --git a/landms/landms/doctype/land_acquisition/land_acquisition.js b/landms/landms/doctype/land_acquisition/land_acquisition.js index 1f28e58..a3864ad 100644 --- a/landms/landms/doctype/land_acquisition/land_acquisition.js +++ b/landms/landms/doctype/land_acquisition/land_acquisition.js @@ -24,12 +24,12 @@ frappe.ui.form.on('Land Acquisition', { refresh_plot_counts(frm); frm.add_custom_button('Purchase Order', () => { - frappe.route_options = { land_acquisition: frm.doc.name }; + frappe.flags.new_po_land_acquisition = frm.doc.name; frappe.new_doc('Purchase Order'); }, __('Create')); frm.add_custom_button('Purchase Invoice', () => { - frappe.route_options = { land_acquisition: frm.doc.name }; + frappe.flags.new_pi_land_acquisition = frm.doc.name; frappe.new_doc('Purchase Invoice'); }, __('Create')); } diff --git a/landms/landms/doctype/landms_settings/landms_settings.json b/landms/landms/doctype/landms_settings/landms_settings.json index b2bd19c..3151b58 100644 --- a/landms/landms/doctype/landms_settings/landms_settings.json +++ b/landms/landms/doctype/landms_settings/landms_settings.json @@ -6,6 +6,11 @@ "is_submittable": 0, "track_changes": 1, "fields": [ + { + "fieldname": "tab_general", + "fieldtype": "Tab Break", + "label": "General" + }, { "fieldname": "company_section", "fieldtype": "Section Break", @@ -26,9 +31,58 @@ "reqd": 1 }, { - "fieldname": "accounts_section", + "fieldname": "col_break_company", + "fieldtype": "Column Break" + }, + { + "fieldname": "plot_inventory_warehouse", + "fieldtype": "Link", + "label": "Plot Inventory Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "plot_items_section", "fieldtype": "Section Break", - "label": "GL Account Mappings" + "label": "Plot Stock Items" + }, + { + "fieldname": "residential_plot_item", + "fieldtype": "Link", + "label": "Residential Plot Item", + "options": "Item", + "reqd": 1, + "description": "Stock item used when a Residential plot enters inventory" + }, + { + "fieldname": "commercial_plot_item", + "fieldtype": "Link", + "label": "Commercial Plot Item", + "options": "Item", + "reqd": 1, + "description": "Stock item used when a Commercial plot enters inventory" + }, + { + "fieldname": "col_break_plot_items", + "fieldtype": "Column Break" + }, + { + "fieldname": "mixed_use_plot_item", + "fieldtype": "Link", + "label": "Mixed Use Plot Item", + "options": "Item", + "reqd": 1, + "description": "Stock item used when a Mixed Use plot enters inventory" + }, + { + "fieldname": "tab_accounts", + "fieldtype": "Tab Break", + "label": "Accounts" + }, + { + "fieldname": "asset_accounts_section", + "fieldtype": "Section Break", + "label": "Asset Accounts" }, { "fieldname": "land_under_development_account", @@ -45,12 +99,16 @@ "reqd": 1 }, { - "fieldname": "customer_advance_account", + "fieldname": "tcb_bank_account", "fieldtype": "Link", - "label": "Customer Advances Account", + "label": "TCB Bank Account", "options": "Account", "reqd": 1 }, + { + "fieldname": "col_break_accounts", + "fieldtype": "Column Break" + }, { "fieldname": "revenue_account", "fieldtype": "Link", @@ -66,12 +124,17 @@ "reqd": 1 }, { - "fieldname": "tcb_bank_account", + "fieldname": "customer_advance_account", "fieldtype": "Link", - "label": "TCB Bank Account", + "label": "Customer Advances Account", "options": "Account", "reqd": 1 }, + { + "fieldname": "liability_accounts_section", + "fieldtype": "Section Break", + "label": "Liability and Income Accounts" + }, { "fieldname": "government_payable_account", "fieldtype": "Link", @@ -79,6 +142,18 @@ "options": "Account", "reqd": 1 }, + { + "fieldname": "seller_payable_account", + "fieldtype": "Link", + "label": "Seller Payable Account", + "options": "Account", + "reqd": 1, + "description": "Creditors account used when land is acquired — Dr Land Under Development / Cr this account" + }, + { + "fieldname": "col_break_liabilities", + "fieldtype": "Column Break" + }, { "fieldname": "forfeited_deposits_account", "fieldtype": "Link", @@ -87,17 +162,14 @@ "reqd": 1 }, { - "fieldname": "seller_payable_account", - "fieldtype": "Link", - "label": "Seller Payable Account", - "options": "Account", - "reqd": 1, - "description": "Creditors account used when land is acquired — Dr Land Under Development / Cr this account" + "fieldname": "tab_application_fee", + "fieldtype": "Tab Break", + "label": "Application Fee" }, { - "fieldname": "application_fee_section", + "fieldname": "app_fee_details_section", "fieldtype": "Section Break", - "label": "Application Fee" + "label": "Fee Details" }, { "fieldname": "application_fee_item", @@ -114,6 +186,10 @@ "reqd": 1, "description": "Fixed application fee charged per plot before a Sales Order can be created" }, + { + "fieldname": "col_break_app_fee", + "fieldtype": "Column Break" + }, { "fieldname": "unpaid_application_expiry_days", "fieldtype": "Int", @@ -131,8 +207,9 @@ "description": "Days the paid application reserves the plot before the Sales Order must be created" }, { - "fieldname": "application_fee_col_break", - "fieldtype": "Column Break" + "fieldname": "app_fee_accounts_section", + "fieldtype": "Section Break", + "label": "Fee Accounts" }, { "fieldname": "application_fee_income_account", @@ -142,6 +219,10 @@ "reqd": 1, "description": "Income account where application fees are posted" }, + { + "fieldname": "col_break_app_fee_accounts", + "fieldtype": "Column Break" + }, { "fieldname": "application_fee_receiving_account", "fieldtype": "Link", @@ -150,21 +231,14 @@ "description": "Default Bank/Cash account where application fee payments are received" }, { - "fieldname": "warehouse_section", - "fieldtype": "Section Break", - "label": "Warehouse" - }, - { - "fieldname": "plot_inventory_warehouse", - "fieldtype": "Link", - "label": "Plot Inventory Warehouse", - "options": "Warehouse", - "reqd": 1 + "fieldname": "tab_naming", + "fieldtype": "Tab Break", + "label": "Naming Series" }, { "fieldname": "naming_section", "fieldtype": "Section Break", - "label": "Naming Series" + "label": "Document Naming" }, { "fieldname": "naming_series_contract", @@ -178,6 +252,10 @@ "label": "Installment Schedule Naming Series", "default": "ISCH-.YYYY.-.####" }, + { + "fieldname": "col_break_naming", + "fieldtype": "Column Break" + }, { "fieldname": "naming_series_tcb_log", "fieldtype": "Data", diff --git a/landms/landms/doctype/plot_handover/plot_handover.py b/landms/landms/doctype/plot_handover/plot_handover.py index b15a99e..8e8fd58 100644 --- a/landms/landms/doctype/plot_handover/plot_handover.py +++ b/landms/landms/doctype/plot_handover/plot_handover.py @@ -3,7 +3,7 @@ from frappe.utils import 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 PLOT_TYPE_TO_ITEM +from landms.landms.doctype.plot_master.plot_master import get_plot_item_code class PlotHandover(Document): @@ -126,9 +126,7 @@ def _make_delivery_note_from_sales_order(self, sales_order_name, plot): return dn def _make_delivery_note_direct(self, contract, plot, settings): - item_code = PLOT_TYPE_TO_ITEM.get(plot.plot_type) - if not item_code: - frappe.throw(f"No item is mapped for plot type '{plot.plot_type}'.") + item_code = get_plot_item_code(plot.plot_type) return frappe.get_doc( { diff --git a/landms/landms/doctype/plot_master/plot_master.py b/landms/landms/doctype/plot_master/plot_master.py index bb8659b..fe35698 100644 --- a/landms/landms/doctype/plot_master/plot_master.py +++ b/landms/landms/doctype/plot_master/plot_master.py @@ -6,11 +6,10 @@ sync_land_acquisition_plot_summary, ) -# Plot Type → stock Item code (must exist as fixtures with Maintain Stock = Yes) -PLOT_TYPE_TO_ITEM = { - "Residential": "RESIDENTIAL PLOT", - "Commercial": "COMMERCIAL PLOT", - "Mixed Use": "MIXED USE PLOT", +PLOT_TYPE_TO_SETTINGS_FIELD = { + "Residential": "residential_plot_item", + "Commercial": "commercial_plot_item", + "Mixed Use": "mixed_use_plot_item", } # Plot Type → Land Acquisition selling-rate field @@ -184,9 +183,7 @@ def create_stock_entry(self): warehouse, against a freshly minted Serial No tied to this plot.""" settings = frappe.get_single("LandMS Settings") - item_code = PLOT_TYPE_TO_ITEM.get(self.plot_type) - if not item_code: - frappe.throw(f"No stock item is mapped for plot type '{self.plot_type}'.") + item_code = get_plot_item_code(self.plot_type, settings) warehouse = settings.plot_inventory_warehouse if not warehouse: @@ -245,6 +242,22 @@ def cancel_stock_entry(self): self.db_set("serial_no", None) +def get_plot_item_code(plot_type, settings=None): + """Return the stock item code for the given plot type from LandMS Settings.""" + field = PLOT_TYPE_TO_SETTINGS_FIELD.get(plot_type) + if not field: + frappe.throw(f"Unknown plot type '{plot_type}'.") + if settings is None: + settings = frappe.get_single("LandMS Settings") + item_code = settings.get(field) + if not item_code: + frappe.throw( + f"No stock item is set for plot type '{plot_type}'. " + f"Go to LandMS Settings → Plot Stock Items and set it." + ) + return item_code + + def get_plot_type_selling_rate(la_values, plot_type): """Look up the per-sqm selling rate for the given plot type on the LA.""" rate_field = PLOT_TYPE_TO_RATE_FIELD.get(plot_type) diff --git a/landms/public/js/purchase_invoice.js b/landms/public/js/purchase_invoice.js new file mode 100644 index 0000000..fce6d7a --- /dev/null +++ b/landms/public/js/purchase_invoice.js @@ -0,0 +1,23 @@ +frappe.ui.form.on('Purchase Invoice', { + onload(frm) { + if (!frm.doc.__islocal) return; + const la = frappe.flags.new_pi_land_acquisition; + if (!la) return; + delete frappe.flags.new_pi_land_acquisition; + frm._land_acquisition = la; + (frm.doc.items || []).forEach(item => { + frappe.model.set_value(item.doctype, item.name, 'land_acquisition', la); + }); + } +}); + +frappe.ui.form.on('Purchase Invoice Item', { + item_code(frm, cdt, cdn) { + if (frm._land_acquisition) { + const row = frappe.get_doc(cdt, cdn); + if (!row.land_acquisition) { + frappe.model.set_value(cdt, cdn, 'land_acquisition', frm._land_acquisition); + } + } + } +}); diff --git a/landms/public/js/purchase_order.js b/landms/public/js/purchase_order.js new file mode 100644 index 0000000..7db89cc --- /dev/null +++ b/landms/public/js/purchase_order.js @@ -0,0 +1,23 @@ +frappe.ui.form.on('Purchase Order', { + onload(frm) { + if (!frm.doc.__islocal) return; + const la = frappe.flags.new_po_land_acquisition; + if (!la) return; + delete frappe.flags.new_po_land_acquisition; + frm._land_acquisition = la; + (frm.doc.items || []).forEach(item => { + frappe.model.set_value(item.doctype, item.name, 'land_acquisition', la); + }); + } +}); + +frappe.ui.form.on('Purchase Order Item', { + item_code(frm, cdt, cdn) { + if (frm._land_acquisition) { + const row = frappe.get_doc(cdt, cdn); + if (!row.land_acquisition) { + frappe.model.set_value(cdt, cdn, 'land_acquisition', frm._land_acquisition); + } + } + } +}); diff --git a/landms/sales_order_hooks.py b/landms/sales_order_hooks.py index fd57c89..56f9e46 100644 --- a/landms/sales_order_hooks.py +++ b/landms/sales_order_hooks.py @@ -22,7 +22,7 @@ import frappe from frappe.utils import add_days, cint, cstr, flt, getdate, today -from landms.landms.doctype.plot_master.plot_master import PLOT_TYPE_TO_ITEM +from landms.landms.doctype.plot_master.plot_master import get_plot_item_code from landms.tcb import ( _get_tcb_settings, create_or_get_registry, @@ -202,9 +202,7 @@ def ensure_plot_sales_invoice_for_sales_order( def build_sales_order_item_row(plot, warehouse, delivery_date): - item_code = PLOT_TYPE_TO_ITEM.get(plot.plot_type) - if not item_code: - frappe.throw(f"No item is mapped for plot type {plot.plot_type}.") + item_code = get_plot_item_code(plot.plot_type) item = frappe.db.get_value( "Item",