Skip to content

Commit a6436d1

Browse files
authored
Merge pull request #18 from LISACORNEL/latest-fix
fix(landms): payment-term allocation, sync circular-cancel, lifecycle…
2 parents a059827 + 3e65616 commit a6436d1

19 files changed

Lines changed: 442 additions & 151 deletions

landms/landms/doctype/landms_settings/landms_settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@
9999
"fieldtype": "Section Break",
100100
"label": "Application Fee"
101101
},
102+
{
103+
"fieldname": "application_fee_item",
104+
"fieldtype": "Link",
105+
"label": "Application Fee Item",
106+
"options": "Item",
107+
"reqd": 1,
108+
"description": "Service item used on the application fee Sales Invoice"
109+
},
102110
{
103111
"fieldname": "application_fee_amount",
104112
"fieldtype": "Float",

landms/landms/doctype/plot_application/plot_application.js

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,42 +25,6 @@ frappe.ui.form.on('Plot Application', {
2525
}, __('Actions'));
2626
}
2727

28-
// Create Sales Order — only on Paid applications
29-
if (frm.doc.docstatus === 1 && frm.doc.status === 'Paid' && !frm.doc.sales_order) {
30-
frm.add_custom_button(__('Create Sales Order'), () => {
31-
frappe.call({
32-
method: 'create_sales_order',
33-
doc: frm.doc,
34-
args: { notify: 1 },
35-
freeze: true,
36-
freeze_message: __('Creating Sales Order...'),
37-
callback(r) {
38-
if (!r.exc) frm.reload_doc();
39-
},
40-
});
41-
}, __('Actions'));
42-
}
43-
44-
// Cancel Application — manual cancellation for Submitted or Paid
45-
if (frm.doc.docstatus === 1 && ['Submitted', 'Paid'].includes(frm.doc.status)) {
46-
frm.add_custom_button(__('Cancel Application'), () => {
47-
frappe.confirm(
48-
__('Are you sure you want to cancel this Plot Application? The plot will be released and any linked Sales Order will be cancelled.'),
49-
() => {
50-
frappe.call({
51-
method: 'frappe.client.cancel',
52-
args: { doctype: 'Plot Application', name: frm.doc.name },
53-
freeze: true,
54-
freeze_message: __('Cancelling...'),
55-
callback(r) {
56-
if (!r.exc) frm.reload_doc();
57-
},
58-
});
59-
}
60-
);
61-
}, __('Actions'));
62-
}
63-
6428
// Jump to the linked Sales Order
6529
if (frm.doc.sales_order) {
6630
frm.add_custom_button(__('View Sales Order'), () => {
@@ -148,7 +112,7 @@ function open_fee_payment_dialog(frm) {
148112
reference_no: values.reference_no || '',
149113
},
150114
freeze: true,
151-
freeze_message: __('Recording payment...'),
115+
freeze_message: __('Recording payment & creating Sales Order...'),
152116
callback(r) {
153117
if (!r.exc) {
154118
d.hide();

landms/landms/doctype/plot_application/plot_application.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,17 +155,15 @@
155155
},
156156
{
157157
"fieldname": "sales_invoice",
158-
"fieldtype": "Link",
158+
"fieldtype": "Data",
159159
"label": "Sales Invoice",
160-
"options": "Sales Invoice",
161160
"read_only": 1,
162161
"description": "Sales Invoice created for the application fee"
163162
},
164163
{
165164
"fieldname": "payment_entry",
166-
"fieldtype": "Link",
165+
"fieldtype": "Data",
167166
"label": "Payment Entry",
168-
"options": "Payment Entry",
169167
"read_only": 1,
170168
"description": "Payment Entry that settled the application fee SI"
171169
},
@@ -177,9 +175,8 @@
177175
},
178176
{
179177
"fieldname": "sales_order",
180-
"fieldtype": "Link",
178+
"fieldtype": "Data",
181179
"label": "Sales Order",
182-
"options": "Sales Order",
183180
"read_only": 1,
184181
"description": "ERP Sales Order created after application fee payment"
185182
},

landms/landms/doctype/plot_application/plot_application.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ def record_fee_payment(self, payment_date, bank_account=None, reference_no=None)
258258
self._validate_receiving_account(bank_account, settings.company)
259259

260260
# --- Sales Invoice ---
261+
if not settings.application_fee_item:
262+
frappe.throw("Application Fee Item is not configured in LandMS Settings.")
263+
261264
si = frappe.get_doc({
262265
"doctype": "Sales Invoice",
263266
"customer": self.customer,
@@ -266,8 +269,7 @@ def record_fee_payment(self, payment_date, bank_account=None, reference_no=None)
266269
"company": settings.company,
267270
"remarks": f"Application fee for Plot {self.plot} — Application {self.name}",
268271
"items": [{
269-
"item_name": "Application Fee",
270-
"description": f"Plot application fee — {self.name} / Plot {self.plot}",
272+
"item_code": settings.application_fee_item,
271273
"qty": 1,
272274
"rate": fee_amount,
273275
"income_account": settings.application_fee_income_account,
@@ -313,11 +315,12 @@ def record_fee_payment(self, payment_date, bank_account=None, reference_no=None)
313315
frappe.db.set_value("Plot Master", self.plot, "status", "Pending Advance")
314316
self._sync_land_acquisition_summary()
315317

318+
so_name = self.create_sales_order(notify=0)
319+
316320
frappe.msgprint(
317321
f"Application fee of TZS {fee_amount:,.0f} recorded. "
318-
f"Plot {self.plot} is now Pending Advance until {expiry}. "
319-
f"Sales Invoice {si.name} settled via Payment Entry {pe.name}. "
320-
"Next: create the Sales Order and collect the first advance within the validity window.",
322+
f"Sales Order <b>{so_name}</b> created for Plot {self.plot}. "
323+
f"Reservation valid until {expiry}.",
321324
indicator="green",
322325
alert=True,
323326
)

landms/landms/doctype/plot_contract/plot_contract.json

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@
137137
"fieldname": "apply_auto_cancellation",
138138
"fieldtype": "Check",
139139
"label": "Apply Auto-Cancellation",
140-
"default": "1",
141-
"description": "Inherited from Sales Order. Uncheck to allow payment beyond deadline."
140+
"read_only": 1,
141+
"description": "Inherited from Sales Order. Change it on the Sales Order to update this value."
142142
},
143143
{
144144
"fieldname": "control_number",
@@ -264,16 +264,14 @@
264264
},
265265
{
266266
"fieldname": "sales_order",
267-
"fieldtype": "Link",
267+
"fieldtype": "Data",
268268
"label": "Sales Order",
269-
"options": "Sales Order",
270269
"read_only": 1
271270
},
272271
{
273272
"fieldname": "plot_application",
274-
"fieldtype": "Link",
273+
"fieldtype": "Data",
275274
"label": "Plot Application",
276-
"options": "Plot Application",
277275
"read_only": 1
278276
},
279277
{
@@ -282,24 +280,21 @@
282280
},
283281
{
284282
"fieldname": "booking_fee_invoice",
285-
"fieldtype": "Link",
283+
"fieldtype": "Data",
286284
"label": "Plot Sales Invoice",
287-
"options": "Sales Invoice",
288285
"read_only": 1
289286
},
290287
{
291288
"fieldname": "government_fee_entry",
292-
"fieldtype": "Link",
289+
"fieldtype": "Data",
293290
"label": "Government Fee Journal Entry",
294-
"options": "Journal Entry",
295291
"read_only": 1,
296292
"depends_on": "eval:doc.government_fee_entry"
297293
},
298294
{
299295
"fieldname": "forfeiture_entry",
300-
"fieldtype": "Link",
296+
"fieldtype": "Data",
301297
"label": "Forfeiture Journal Entry",
302-
"options": "Journal Entry",
303298
"read_only": 1,
304299
"depends_on": "eval:doc.forfeiture_entry"
305300
},

landms/landms/doctype/plot_contract/plot_contract.py

Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ def sync_payment_status(self):
333333
invoice_name = self._get_plot_invoice_name()
334334
if not invoice_name:
335335
self._sync_header_from_sales_order(so_doc)
336+
self._persist_payment_sync_state()
336337
return
337338

338339
si_doc = frappe.get_doc("Sales Invoice", invoice_name)
@@ -346,24 +347,21 @@ def sync_payment_status(self):
346347
if len(self.payment_schedule or []) != len(si_doc.payment_schedule or []) and self.docstatus == 0:
347348
self._rebuild_schedule_from_invoice(si_doc)
348349

350+
self._sync_header_from_sales_order(so_doc, invoice_name=si_doc.name)
351+
self._sync_schedule_rows_from_invoice(si_doc, paid_dates)
352+
self.total_contract_value = flt(si_doc.grand_total)
353+
self.total_paid = total_paid
354+
self.total_outstanding = total_outstanding
355+
self.payment_progress = self._derive_payment_progress(total_paid, total_outstanding, si_doc=si_doc)
356+
self._persist_payment_sync_state()
357+
349358
advance_met = self._is_advance_installment_met(si_doc)
350359

351-
# Only auto-submit the contract once the full advance is covered.
360+
# Only auto-submit once the synced draft state is already safely saved.
352361
if advance_met and self.docstatus == 0:
353362
self.submit()
354363
self.reload()
355364

356-
self._sync_header_from_sales_order(so_doc, invoice_name=si_doc.name)
357-
self._sync_schedule_rows_from_invoice(si_doc, paid_dates)
358-
self.db_set("total_contract_value", flt(si_doc.grand_total), update_modified=False)
359-
self.db_set("total_paid", total_paid, update_modified=False)
360-
self.db_set("total_outstanding", total_outstanding, update_modified=False)
361-
self.db_set(
362-
"payment_progress",
363-
self._derive_payment_progress(total_paid, total_outstanding, si_doc=si_doc),
364-
update_modified=False,
365-
)
366-
367365
if so_doc.get("plot_application"):
368366
app_status = frappe.db.get_value("Plot Application", so_doc.plot_application, "status")
369367
if advance_met and app_status == "Paid":
@@ -372,38 +370,47 @@ def sync_payment_status(self):
372370
so_doc.plot_application,
373371
"status",
374372
"Converted",
373+
update_modified=True,
375374
)
375+
frappe.clear_document_cache("Plot Application", so_doc.plot_application)
376376

377377
if total_outstanding <= 0 and self.docstatus == 1:
378-
self.db_set("contract_status", "Completed", update_modified=False)
378+
self.contract_status = "Completed"
379379
plot_status = frappe.db.get_value("Plot Master", self.plot, "status")
380380
if plot_status not in ("Delivered", "Title Closed"):
381-
frappe.db.set_value("Plot Master", self.plot, "status", "Ready for Handover")
381+
frappe.db.set_value("Plot Master", self.plot, "status", "Ready for Handover", update_modified=True)
382+
frappe.clear_document_cache("Plot Master", self.plot)
382383
self._sync_land_acquisition_summary()
383384

385+
# Persist the submitted contract state before posting linked entries.
386+
self._persist_payment_sync_state()
384387
settings = frappe.get_single("LandMS Settings")
385388
self.reload()
386389
je_name = self._post_completion_entries(settings)
390+
handover_name = self._ensure_handover_draft()
387391

388392
msg = f"Contract fully paid. Plot {self.plot} marked as Ready for Handover."
389393
if je_name:
390394
msg += f" Government fee posted — Journal Entry: {je_name}."
395+
if handover_name:
396+
msg += f" Plot Handover <b>{handover_name}</b> created (Draft) — fill the handover parties and submit."
391397
frappe.msgprint(msg, indicator="green", alert=True)
398+
return # already saved above
392399

393400
elif advance_met and self.docstatus == 1:
394-
self.db_set("contract_status", "Ongoing", update_modified=False)
401+
self.contract_status = "Ongoing"
395402
if frappe.db.get_value("Plot Master", self.plot, "status") == "Pending Advance":
396-
frappe.db.set_value("Plot Master", self.plot, "status", "Reserved")
403+
frappe.db.set_value("Plot Master", self.plot, "status", "Reserved", update_modified=True)
404+
frappe.clear_document_cache("Plot Master", self.plot)
397405
self._sync_land_acquisition_summary()
398406

407+
self._persist_payment_sync_state()
408+
399409
def _sync_header_from_sales_order(self, so_doc, *, invoice_name: str | None = None):
400-
updates = {
401-
"plot_application": so_doc.get("plot_application") or "",
402-
"control_number": so_doc.get("control_number") or "",
403-
"booking_fee_invoice": invoice_name or so_doc.get("plot_sales_invoice") or "",
404-
"payment_deadline": so_doc.get("payment_deadline"),
405-
}
406-
frappe.db.set_value("Plot Contract", self.name, updates, update_modified=False)
410+
self.plot_application = so_doc.get("plot_application") or ""
411+
self.control_number = so_doc.get("control_number") or ""
412+
self.booking_fee_invoice = invoice_name or so_doc.get("plot_sales_invoice") or ""
413+
self.payment_deadline = so_doc.get("payment_deadline")
407414

408415
def _rebuild_schedule_from_invoice(self, invoice):
409416
# Compute expected from the original grand_total * invoice_portion,
@@ -450,21 +457,39 @@ def _sync_schedule_rows_from_invoice(self, invoice, paid_dates):
450457
status = self._derive_installment_status(source.due_date, expected, outstanding, today_date=today_date)
451458
paid_date = paid_dates.get(idx) if outstanding <= 0 else None
452459

453-
frappe.db.set_value(
454-
"Plot Contract Payment",
455-
target.name,
456-
{
457-
"description": source.description or self._default_installment_label(idx),
458-
"due_date": source.due_date,
459-
"expected_amount": expected,
460-
"paid_amount": paid_amount,
461-
"outstanding_amount": outstanding,
462-
"paid_date": paid_date,
463-
"sales_invoice": invoice.name,
464-
"status": status,
465-
},
466-
update_modified=False,
467-
)
460+
target.description = source.description or self._default_installment_label(idx)
461+
target.due_date = source.due_date
462+
target.expected_amount = expected
463+
target.paid_amount = paid_amount
464+
target.outstanding_amount = outstanding
465+
target.paid_date = paid_date
466+
target.sales_invoice = invoice.name
467+
target.status = status
468+
if self.docstatus == 1 and target.name:
469+
target.db_update()
470+
471+
def _persist_payment_sync_state(self):
472+
if self.docstatus == 0:
473+
self.save(ignore_permissions=True)
474+
return
475+
476+
frappe.db.set_value(
477+
"Plot Contract",
478+
self.name,
479+
{
480+
"plot_application": self.plot_application or "",
481+
"control_number": self.control_number or "",
482+
"booking_fee_invoice": self.booking_fee_invoice or "",
483+
"payment_deadline": self.payment_deadline,
484+
"total_contract_value": flt(self.total_contract_value),
485+
"total_paid": flt(self.total_paid),
486+
"total_outstanding": flt(self.total_outstanding),
487+
"payment_progress": self.payment_progress or "",
488+
"contract_status": self.contract_status or "Draft",
489+
},
490+
update_modified=True,
491+
)
492+
frappe.clear_document_cache("Plot Contract", self.name)
468493

469494
def _derive_installment_status(self, due_date, expected, outstanding, *, today_date=None):
470495
today_date = today_date or getdate(today())
@@ -585,6 +610,31 @@ def _post_completion_entries(self, settings):
585610
self.db_set("government_fee_entry", je.name)
586611
return je.name
587612

613+
def _ensure_handover_draft(self):
614+
existing = frappe.db.get_value(
615+
"Plot Handover",
616+
{"contract": self.name, "docstatus": ["!=", 2]},
617+
"name",
618+
)
619+
if existing:
620+
return existing
621+
622+
handover = frappe.get_doc({
623+
"doctype": "Plot Handover",
624+
"contract": self.name,
625+
"handover_date": today(),
626+
"customer": self.customer,
627+
"plot": self.plot,
628+
"acquisition_name": self.get("acquisition_name") or "",
629+
"land_acquisition": self.get("land_acquisition") or "",
630+
"contract_date": self.get("contract_date"),
631+
"selling_price": flt(self.selling_price),
632+
})
633+
handover.flags.ignore_permissions = True
634+
handover.flags.ignore_mandatory = True
635+
handover.insert()
636+
return handover.name
637+
588638
def _post_termination_journal_entry(self, settings):
589639
if self.forfeiture_entry:
590640
return None

landms/landms/doctype/plot_handover/plot_handover.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@
3939
},
4040
{
4141
"fieldname": "delivery_note",
42-
"fieldtype": "Link",
42+
"fieldtype": "Data",
4343
"label": "Delivery Note",
44-
"options": "Delivery Note",
4544
"read_only": 1,
4645
"description": "Created automatically on submit to reduce serialized plot stock"
4746
},

0 commit comments

Comments
 (0)