@@ -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
0 commit comments