Merge pull request #40856 from ruthra-kumar/better_approach_for_exc_rate_updating
fix: unwanted Exc Gain/Loss journals on Payment against Journal entry
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index a2a5bbc..1ff1cb7 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -188,6 +188,7 @@
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_payment_schedule()
+ self.set_payment_req_status()
self.set_status()
def set_liability_account(self):
diff --git a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
index 7a8cdf7..6c8f640 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
+++ b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
@@ -5,6 +5,8 @@
# test_records = frappe.get_test_records('Payment Gateway Account')
+test_ignore = ["Payment Gateway"]
+
class TestPaymentGatewayAccount(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 196838a..5272294 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -149,35 +149,37 @@
).format(self.grand_total, amount)
)
- def on_submit(self):
- if self.payment_request_type == "Outward":
- self.db_set("status", "Initiated")
- return
- elif self.payment_request_type == "Inward":
- self.db_set("status", "Requested")
-
- send_mail = self.payment_gateway_validation() if self.payment_gateway else None
+ def on_change(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
-
- if (
- hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
- ) or self.flags.mute_email:
- send_mail = False
-
- if send_mail and self.payment_channel != "Phone":
- self.set_payment_request_url()
- self.send_email()
- self.make_communication_entry()
-
- elif self.payment_channel == "Phone":
- self.request_phone_payment()
-
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
- ref_doc.set_total_advance_paid()
+ ref_doc.set_advance_payment_status()
+
+ def on_submit(self):
+ if self.payment_request_type == "Outward":
+ self.db_set("status", "Initiated")
+ elif self.payment_request_type == "Inward":
+ self.db_set("status", "Requested")
+
+ if self.payment_request_type == "Inward":
+ send_mail = self.payment_gateway_validation() if self.payment_gateway else None
+ ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
+
+ if (
+ hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
+ ) or self.flags.mute_email:
+ send_mail = False
+
+ if send_mail and self.payment_channel != "Phone":
+ self.set_payment_request_url()
+ self.send_email()
+ self.make_communication_entry()
+
+ elif self.payment_channel == "Phone":
+ self.request_phone_payment()
def request_phone_payment(self):
controller = _get_payment_gateway_controller(self.payment_gateway)
@@ -217,14 +219,6 @@
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_receivable_doctypes") + frappe.get_hooks(
- "advance_payment_payable_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 ref_doc.order_type == "Shopping Cart":
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index c390edd..15b7fa1 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -1127,10 +1127,17 @@
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)
+ pr = make_payment_request(
+ dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True, payment_request_type="Outward"
+ )
+
+ po.reload()
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
pe = get_payment_entry(po.doctype, po.name).save().submit()
+
+ pr.reload()
+ self.assertEqual(pr.status, "Paid")
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid")
pe.reload()
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index d90e09e..c9e8e4e 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1924,32 +1924,43 @@
self.db_set("advance_paid", advance_paid)
- self.set_advance_payment_status(advance_paid, order_total)
+ self.set_advance_payment_status()
- def set_advance_payment_status(self, advance_paid: float | None = None, order_total: float | None = None):
+ def set_advance_payment_status(self):
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_receivable_doctypes"):
- new_status = "Requested" if prs else "Not Requested"
- if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
- new_status = "Initiated" if prs else "Not Initiated"
+ stati = frappe.get_list(
+ "Payment Request",
+ {
+ "reference_doctype": self.doctype,
+ "reference_name": self.name,
+ "docstatus": 1,
+ },
+ pluck="status",
+ )
+ if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
+ if not stati:
+ new_status = "Not Requested"
+ elif "Requested" in stati or "Failed" in stati:
+ new_status = "Requested"
+ elif "Partially Paid" in stati:
+ new_status = "Partially Paid"
+ elif "Paid" in stati:
+ new_status = "Fully Paid"
+ if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
+ if not stati:
+ new_status = "Not Initiated"
+ elif "Initiated" in stati or "Failed" in stati or "Payment Ordered" in stati:
+ new_status = "Initiated"
+ elif "Partially Paid" in stati:
+ new_status = "Partially Paid"
+ elif "Paid" in stati:
+ new_status = "Fully Paid"
if new_status == self.advance_payment_status:
return
- self.db_set("advance_payment_status", new_status)
+ self.db_set("advance_payment_status", new_status, update_modified=False)
self.set_status(update=True)
self.notify_update()
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 013cfb1..b3cfb35 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -349,7 +349,6 @@
"Payment Entry": {
"on_submit": [
"erpnext.regional.create_transaction_log",
- "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
],
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js
index 255383b..c3bf9ef 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.js
+++ b/erpnext/manufacturing/doctype/workstation/workstation.js
@@ -182,6 +182,7 @@
me.job_cards = [r.message];
me.prepare_timer();
me.update_job_card_details();
+ me.frm.reload_doc();
}
},
});
@@ -229,6 +230,7 @@
me.job_cards = [r.message];
me.prepare_timer();
me.update_job_card_details();
+ me.frm.reload_doc();
}
},
});
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 2c9e887..d4a6b87 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import json
+from unittest.mock import patch
import frappe
import frappe.permissions
@@ -1956,10 +1957,48 @@
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):
+ @patch(
+ # this also shadows one (1) call to _get_payment_gateway_controller
+ "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
+ return_value=None,
+ )
+ def test_sales_order_advance_payment_status(self, mocked_get_payment_url):
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
+ # Flow progressing to SI with payment entries "moved" from SO to SI
+ so = make_sales_order(qty=1, rate=100, do_not_submit=True)
+ # no-op; for optical consistency with how a webshop SO would look like
+ so.order_type = "Shopping Cart"
+ so.submit()
+ 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, order_type="Shopping Cart", submit_doc=True, return_doc=True
+ )
+ self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
+
+ pe = pr.set_as_paid()
+ pr.reload() # status updated
+ pe.reload() # references moved to Sales Invoice
+ self.assertEqual(pr.status, "Paid")
+ self.assertEqual(pe.references[0].reference_doctype, "Sales Invoice")
+ self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid")
+
+ pe.cancel()
+ pr.reload()
+ self.assertEqual(pr.status, "Paid") # TODO: this might be a bug
+ so.reload() # reload
+ # regardless, since the references have already "handed-over" to SI,
+ # the SO keeps its historical state at the time of hand over
+ self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid")
+
+ pr.cancel()
+ self.assertEqual(
+ frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
+ ) # TODO: this might be a bug; handover has happened
+
+ # Flow NOT progressing to SI with payment entries NOT "moved"
so = make_sales_order(qty=1, rate=100)
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
@@ -1971,11 +2010,15 @@
pe.reload()
pe.cancel()
- self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
+ self.assertEqual(
+ frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested"
+ ) # here: reset
pr.reload()
pr.cancel()
- self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested")
+ self.assertEqual(
+ frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
+ ) # here: reset
def test_pick_list_without_rejected_materials(self):
serial_and_batch_item = make_item(
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index a5c0cb8..fe93239 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -18,6 +18,7 @@
"parent_warehouse",
"consider_rejected_warehouses",
"get_item_locations",
+ "pick_manually",
"section_break_6",
"scan_barcode",
"column_break_13",
@@ -192,11 +193,18 @@
"fieldname": "consider_rejected_warehouses",
"fieldtype": "Check",
"label": "Consider Rejected Warehouses"
+ },
+ {
+ "default": "0",
+ "description": "If enabled then system won't override the picked qty / batches / serial numbers.",
+ "fieldname": "pick_manually",
+ "fieldtype": "Check",
+ "label": "Pick Manually"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2024-03-27 13:10:13.177072",
+ "modified": "2024-03-27 22:49:16.954637",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
@@ -264,7 +272,7 @@
"write": 1
}
],
- "sort_field": "creation",
+ "sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 56bdca5..9aaa08e 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -41,6 +41,7 @@
amended_from: DF.Link | None
company: DF.Link
+ consider_rejected_warehouses: DF.Check
customer: DF.Link | None
customer_name: DF.Data | None
for_qty: DF.Float
@@ -49,6 +50,7 @@
material_request: DF.Link | None
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
parent_warehouse: DF.Link | None
+ pick_manually: DF.Check
prompt_qty: DF.Check
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
scan_barcode: DF.Data | None
@@ -70,7 +72,8 @@
def before_save(self):
self.update_status()
- self.set_item_locations()
+ if not self.pick_manually:
+ self.set_item_locations()
if self.get("locations"):
self.validate_sales_order_percentage()
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 913051a..ccd7f64 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -1021,7 +1021,9 @@
@frappe.whitelist()
def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False):
ignore_empty_stock = cint(ignore_empty_stock)
- items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
+ items = []
+ if item_code and warehouse:
+ items = get_item_and_warehouses(item_code, warehouse)
if not item_code:
items = get_items_for_stock_reco(warehouse, company)
@@ -1066,6 +1068,20 @@
return res
+def get_item_and_warehouses(item_code, warehouse):
+ from frappe.utils.nestedset import get_descendants_of
+
+ items = []
+ if frappe.get_cached_value("Warehouse", warehouse, "is_group"):
+ childrens = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
+ for ch_warehouse in childrens:
+ items.append(frappe._dict({"item_code": item_code, "warehouse": ch_warehouse}))
+ else:
+ items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})]
+
+ return items
+
+
def get_items_for_stock_reco(warehouse, company):
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
items = frappe.db.sql(
@@ -1080,7 +1096,7 @@
and i.is_stock_item = 1
and i.has_variants = 0
and exists(
- select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse
+ select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse and is_group = 0
)
""",
as_dict=1,
@@ -1095,7 +1111,7 @@
where
i.name = id.parent
and exists(
- select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse
+ select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse and is_group = 0
)
and i.is_stock_item = 1
and i.has_variants = 0
@@ -1157,7 +1173,7 @@
frappe._dict(
{
"item_code": row[0],
- "warehouse": warehouse,
+ "warehouse": row[3],
"qty": row[8],
"item_name": row[1],
"batch_no": row[4],