Merge pull request #38560 from blaggacao/feat/so-po-advance-payment-status

feat!: advance payment status on advance payment doctypes
This commit is contained in:
ruthra kumar 2024-01-23 16:39:22 +05:30 committed by GitHub
commit 0d7dd93284
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 241 additions and 21 deletions

View File

@ -186,9 +186,12 @@ class JournalEntry(AccountsController):
def update_advance_paid(self):
advance_paid = frappe._dict()
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
for d in self.get("accounts"):
if d.is_advance:
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"):
if d.reference_type in advance_payment_doctypes:
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items():

View File

@ -925,7 +925,10 @@ class PaymentEntry(AccountsController):
def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if d.reference_doctype in advance_payment_doctypes:
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes
@ -1423,8 +1426,11 @@ class PaymentEntry(AccountsController):
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_doc(
d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid()

View File

@ -169,6 +169,13 @@ class PaymentRequest(Document):
elif self.payment_channel == "Phone":
self.request_phone_payment()
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
def request_phone_payment(self):
controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount()
@ -207,6 +214,14 @@ class PaymentRequest(Document):
self.check_if_payment_entry_exists()
self.set_as_cancelled()
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
def make_invoice(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":

View File

@ -600,7 +600,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they might be getting unlinked
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if jv_detail.get("reference_type") in advance_payment_doctypes:
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
@ -673,7 +676,10 @@ def update_reference_in_payment_entry(
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they are getting unlinked
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_customer_doctypes"
) + frappe.get_hooks("advance_payment_supplier_doctypes")
if existing_row.get("reference_doctype") in advance_payment_doctypes:
frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name
).set_total_advance_paid()

View File

@ -134,6 +134,7 @@
"more_info_tab",
"tracking_section",
"status",
"advance_payment_status",
"column_break_75",
"per_billed",
"per_received",
@ -1269,13 +1270,25 @@
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "advance_payment_status",
"fieldtype": "Select",
"hidden": 1,
"in_standard_filter": 1,
"label": "Advance Payment Status",
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Not Initiated\nInitiated\nPartially Paid\nFully Paid",
"print_hide": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2023-10-01 20:58:07.851037",
"modified": "2023-10-10 13:37:40.158761",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
@ -1330,4 +1343,4 @@
"timeline_field": "supplier",
"title_field": "supplier_name",
"track_changes": 1
}
}

View File

@ -215,6 +215,10 @@ class PurchaseOrder(BuyingController):
self.validate_fg_item_for_subcontracting()
self.set_received_qty_for_drop_ship_items()
if not self.advance_payment_status:
self.advance_payment_status = "Not Initiated"
validate_inter_company_party(
self.doctype, self.supplier, self.company, self.inter_company_order_reference
)

View File

@ -1,6 +1,6 @@
frappe.listview_settings['Purchase Order'] = {
add_fields: ["base_grand_total", "company", "currency", "supplier",
"supplier_name", "per_received", "per_billed", "status"],
"supplier_name", "per_received", "per_billed", "status", "advance_payment_status"],
get_indicator: function (doc) {
if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
@ -8,6 +8,8 @@ frappe.listview_settings['Purchase Order'] = {
return [__("On Hold"), "orange", "status,=,On Hold"];
} else if (doc.status === "Delivered") {
return [__("Delivered"), "green", "status,=,Closed"];
} else if (doc.advance_payment_status == "Initiated") {
return [__("To Pay"), "gray", "advance_payment_status,=,Initiated"];
} else if (flt(doc.per_received, 2) < 100 && doc.status !== "Closed") {
if (flt(doc.per_billed, 2) < 100) {
return [__("To Receive and Bill"), "orange",

View File

@ -1021,6 +1021,33 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertTrue(frappe.db.get_value("Subcontracting Order", {"purchase_order": po.name}))
def test_purchase_order_advance_payment_status(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
po = create_purchase_order()
self.assertEqual(
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated"
)
pr = make_payment_request(dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True)
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
pe = get_payment_entry(po.doctype, po.name).save().submit()
self.assertEqual(
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid"
)
pe.reload()
pe.cancel()
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
pr.reload()
pr.cancel()
self.assertEqual(
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated"
)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@ -54,7 +54,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
"fieldtype": "MultiSelectList",
"width": "80",
get_data: function(txt) {
let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"]
let status = ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"]
let options = []
for (let option of status){
options.push({

View File

@ -1749,7 +1749,10 @@ class AccountsController(TransactionBase):
def set_total_advance_paid(self):
ple = frappe.qb.DocType("Payment Ledger Entry")
party = self.customer if self.doctype == "Sales Order" else self.supplier
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
party = self.customer
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
party = self.supplier
advance = (
frappe.qb.from_(ple)
.select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount"))
@ -1763,6 +1766,8 @@ class AccountsController(TransactionBase):
.run(as_dict=True)
)
advance_paid, order_total = None, None
if advance:
advance = advance[0]
@ -1791,7 +1796,38 @@ class AccountsController(TransactionBase):
).format(formatted_advance_paid, self.name, formatted_order_total)
)
frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid)
self.db_set("advance_paid", advance_paid)
self.set_advance_payment_status(advance_paid, order_total)
def set_advance_payment_status(
self, advance_paid: float | None = None, order_total: float | None = None
):
new_status = None
# if money is paid set the paid states
if advance_paid:
new_status = "Partially Paid" if advance_paid < order_total else "Fully Paid"
if not new_status:
prs = frappe.db.count(
"Payment Request",
{
"reference_doctype": self.doctype,
"reference_name": self.name,
"docstatus": 1,
},
)
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
new_status = "Requested" if prs else "Not Requested"
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
new_status = "Initiated" if prs else "Not Initiated"
if new_status == self.advance_payment_status:
return
self.db_set("advance_payment_status", new_status)
self.set_status(update=True)
self.notify_update()
@property
def company_abbr(self):

View File

@ -53,6 +53,10 @@ status_map = {
"To Deliver",
"eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note",
],
[
"To Pay",
"eval:self.advance_payment_status == 'Requested' and self.docstatus == 1",
],
[
"Completed",
"eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1",
@ -63,15 +67,19 @@ status_map = {
],
"Purchase Order": [
["Draft", None],
[
"To Receive and Bill",
"eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1",
],
["To Bill", "eval:self.per_received >= 100 and self.per_billed < 100 and self.docstatus == 1"],
[
"To Receive",
"eval:self.per_received < 100 and self.per_billed == 100 and self.docstatus == 1",
],
[
"To Receive and Bill",
"eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1",
],
[
"To Pay",
"eval:self.advance_payment_status == 'Initiated' and self.docstatus == 1",
],
[
"Completed",
"eval:self.per_received >= 100 and self.per_billed == 100 and self.docstatus == 1",

View File

@ -481,7 +481,8 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
communication_doctypes = ["Customer", "Supplier"]
advance_payment_doctypes = ["Sales Order", "Purchase Order"]
advance_payment_customer_doctypes = ["Sales Order"]
advance_payment_supplier_doctypes = ["Purchase Order"]
invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]

View File

@ -352,6 +352,7 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
execute:frappe.db.set_default("date_format", frappe.db.get_single_value("System Settings", "date_format"))
erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v15_0.create_advance_payment_status
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20

View File

@ -0,0 +1,54 @@
import frappe
def execute():
"""
Description:
Calculate the new Advance Payment Statuse column in SO & PO
"""
if frappe.reload_doc("selling", "doctype", "Sales Order"):
so = frappe.qb.DocType("Sales Order")
frappe.qb.update(so).set(so.advance_payment_status, "Not Requested").where(
so.docstatus == 1
).where(so.advance_paid == 0.0).run()
frappe.qb.update(so).set(so.advance_payment_status, "Partially Paid").where(
so.docstatus == 1
).where(so.advance_payment_status.isnull()).where(
so.advance_paid < (so.rounded_total or so.grand_total)
).run()
frappe.qb.update(so).set(so.advance_payment_status, "Fully Paid").where(so.docstatus == 1).where(
so.advance_payment_status.isnull()
).where(so.advance_paid == (so.rounded_total or so.grand_total)).run()
pr = frappe.qb.DocType("Payment Request")
frappe.qb.update(so).join(pr).on(so.name == pr.reference_name).set(
so.advance_payment_status, "Requested"
).where(so.docstatus == 1).where(pr.docstatus == 1).where(
so.advance_payment_status == "Not Requested"
).run()
if frappe.reload_doc("buying", "doctype", "Purchase Order"):
po = frappe.qb.DocType("Purchase Order")
frappe.qb.update(po).set(po.advance_payment_status, "Not Initiated").where(
po.docstatus == 1
).where(po.advance_paid == 0.0).run()
frappe.qb.update(po).set(po.advance_payment_status, "Partially Paid").where(
po.docstatus == 1
).where(po.advance_payment_status.isnull()).where(
po.advance_paid < (po.rounded_total or po.grand_total)
).run()
frappe.qb.update(po).set(po.advance_payment_status, "Fully Paid").where(po.docstatus == 1).where(
po.advance_payment_status.isnull()
).where(po.advance_paid == (po.rounded_total or po.grand_total)).run()
pr = frappe.qb.DocType("Payment Request")
frappe.qb.update(po).join(pr).on(po.name == pr.reference_name).set(
po.advance_payment_status, "Initiated"
).where(po.docstatus == 1).where(pr.docstatus == 1).where(
po.advance_payment_status == "Not Initiated"
).run()

View File

@ -131,6 +131,7 @@
"per_billed",
"per_picked",
"billing_status",
"advance_payment_status",
"sales_team_section_break",
"sales_partner",
"column_break7",
@ -1269,7 +1270,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nOn Hold\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed",
"options": "\nDraft\nOn Hold\nTo Pay\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed",
"print_hide": 1,
"read_only": 1,
"reqd": 1,
@ -1638,6 +1639,18 @@
"no_copy": 1,
"print_hide": 1,
"report_hide": 1
},
{
"fieldname": "advance_payment_status",
"fieldtype": "Select",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"in_standard_filter": 1,
"label": "Advance Payment Status",
"no_copy": 1,
"options": "Not Requested\nRequested\nPartially Paid\nFully Paid",
"print_hide": 1
}
],
"icon": "fa fa-file-text",
@ -1722,4 +1735,4 @@
"title_field": "customer_name",
"track_changes": 1,
"track_seen": 1
}
}

View File

@ -223,6 +223,8 @@ class SalesOrder(SellingController):
self.billing_status = "Not Billed"
if not self.delivery_status:
self.delivery_status = "Not Delivered"
if not self.advance_payment_status:
self.advance_payment_status = "Not Requested"
self.reset_default_field_value("set_warehouse", "items", "warehouse")

View File

@ -1,6 +1,6 @@
frappe.listview_settings['Sales Order'] = {
add_fields: ["base_grand_total", "customer_name", "currency", "delivery_date",
"per_delivered", "per_billed", "status", "order_type", "name", "skip_delivery_note"],
"per_delivered", "per_billed", "status", "advance_payment_status", "order_type", "name", "skip_delivery_note"],
get_indicator: function (doc) {
if (doc.status === "Closed") {
// Closed
@ -10,6 +10,8 @@ frappe.listview_settings['Sales Order'] = {
return [__("On Hold"), "orange", "status,=,On Hold"];
} else if (doc.status === "Completed") {
return [__("Completed"), "green", "status,=,Completed"];
} else if (doc.advance_payment_status === "Requested") {
return [__("To Pay"), "gray", "advance_payment_status,=,Requested"];
} else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) {
if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
// not delivered & overdue

View File

@ -1996,6 +1996,33 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
def test_sales_order_advance_payment_status(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
so = make_sales_order(qty=1, rate=100)
self.assertEqual(
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
)
pr = make_payment_request(dt=so.doctype, dn=so.name, submit_doc=True, return_doc=True)
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
pe = get_payment_entry(so.doctype, so.name).save().submit()
self.assertEqual(
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid"
)
pe.reload()
pe.cancel()
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
pr.reload()
pr.cancel()
self.assertEqual(
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
)
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")

View File

@ -209,7 +209,7 @@ def get_so_with_invoices(filters):
)
.where(
(so.docstatus == 1)
& (so.status.isin(["To Deliver and Bill", "To Bill"]))
& (so.status.isin(["To Deliver and Bill", "To Bill", "To Pay"]))
& (so.payment_terms_template != "NULL")
& (so.company == conditions.company)
& (so.transaction_date[conditions.start_date : conditions.end_date])

View File

@ -56,7 +56,7 @@ frappe.query_reports["Sales Order Analysis"] = {
"fieldtype": "MultiSelectList",
"width": "80",
get_data: function(txt) {
let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]
let status = ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"]
let options = []
for (let option of status){
options.push({