Merge branch 'develop' into FIX-34354
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index e3d9c26..c9e3998 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -221,12 +221,15 @@
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
difference_amount = 0
- if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
- "exchange_rate", 1
- ):
- allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
- allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
- difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
+ if frappe.get_cached_value(
+ "Account", self.receivable_payable_account, "account_currency"
+ ) != frappe.get_cached_value("Company", self.company, "default_currency"):
+ if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
+ "exchange_rate", 1
+ ):
+ allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
+ allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
+ difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index f9dda05..3be11ae 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -5,7 +5,7 @@
import frappe
from frappe import qb
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center
@@ -349,6 +349,11 @@
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
si.reload()
@@ -390,6 +395,11 @@
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
# check PR tool output
@@ -414,6 +424,11 @@
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
# assert outstanding
@@ -450,6 +465,11 @@
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ # Difference amount should not be calculated for base currency accounts
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
pr.reconcile()
self.assertEqual(pr.get("invoices"), [])
@@ -824,6 +844,52 @@
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
+ @change_settings(
+ "Accounts Settings",
+ {
+ "allow_multi_currency_invoices_against_single_party_account": 1,
+ },
+ )
+ def test_no_difference_amount_for_base_currency_accounts(self):
+ # Make Sale Invoice
+ si = self.create_sales_invoice(
+ qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
+ )
+ si.customer = self.customer
+ si.currency = "EUR"
+ si.conversion_rate = 85
+ si.debit_to = self.debit_to
+ si.save().submit()
+
+ # Make payment using Payment Entry
+ pe1 = create_payment_entry(
+ company=self.company,
+ payment_type="Receive",
+ party_type="Customer",
+ party=self.customer,
+ paid_from=self.debit_to,
+ paid_to=self.bank,
+ paid_amount=100,
+ )
+
+ pe1.save()
+ pe1.submit()
+
+ pr = self.create_payment_reconciliation()
+ pr.party = self.customer
+ pr.receivable_payable_account = self.debit_to
+ pr.get_unreconciled_entries()
+
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [pr.payments[0].as_dict()]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ self.assertEqual(pr.allocation[0].allocated_amount, 85)
+ self.assertEqual(pr.allocation[0].difference_amount, 0)
+
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 95857e4..8c73e56 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -16,6 +16,7 @@
"transaction_settings_section",
"po_required",
"pr_required",
+ "over_order_allowance",
"column_break_12",
"maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
@@ -156,6 +157,13 @@
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
"fieldtype": "Check",
"label": "Set Landed Cost Based on Purchase Invoice Rate"
+ },
+ {
+ "default": "0",
+ "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
+ "fieldname": "over_order_allowance",
+ "fieldtype": "Float",
+ "label": "Over Order Allowance (%)"
}
],
"icon": "fa fa-cog",
@@ -163,7 +171,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-02-28 15:41:32.686805",
+ "modified": "2023-03-02 17:02:14.404622",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 2415aec..06b9d29 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -21,6 +21,9 @@
from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController
+from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
+ validate_against_blanket_order,
+)
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
@@ -69,6 +72,7 @@
self.validate_with_previous_doc()
self.validate_for_subcontracting()
self.validate_minimum_order_qty()
+ validate_against_blanket_order(self)
if self.is_old_subcontracting_flow:
self.validate_bom_for_subcontracting_items()
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
index d3bb33e..7b26a14 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
@@ -7,6 +7,12 @@
},
setup: function(frm) {
+ frm.custom_make_buttons = {
+ 'Purchase Order': 'Purchase Order',
+ 'Sales Order': 'Sales Order',
+ 'Quotation': 'Quotation',
+ };
+
frm.add_fetch("customer", "customer_name", "customer_name");
frm.add_fetch("supplier", "supplier_name", "supplier_name");
},
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
index ff21401..32f1c36 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
@@ -6,6 +6,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from erpnext.stock.doctype.item.item import get_item_defaults
@@ -29,21 +30,23 @@
def update_ordered_qty(self):
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
+
+ trans = frappe.qb.DocType(ref_doctype)
+ trans_item = frappe.qb.DocType(f"{ref_doctype} Item")
+
item_ordered_qty = frappe._dict(
- frappe.db.sql(
- """
- select trans_item.item_code, sum(trans_item.stock_qty) as qty
- from `tab{0} Item` trans_item, `tab{0}` trans
- where trans.name = trans_item.parent
- and trans_item.blanket_order=%s
- and trans.docstatus=1
- and trans.status not in ('Closed', 'Stopped')
- group by trans_item.item_code
- """.format(
- ref_doctype
- ),
- self.name,
- )
+ (
+ frappe.qb.from_(trans_item)
+ .from_(trans)
+ .select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty"))
+ .where(
+ (trans.name == trans_item.parent)
+ & (trans_item.blanket_order == self.name)
+ & (trans.docstatus == 1)
+ & (trans.status.notin(["Stopped", "Closed"]))
+ )
+ .groupby(trans_item.item_code)
+ ).run()
)
for d in self.items:
@@ -79,7 +82,43 @@
"doctype": doctype + " Item",
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
"postprocess": update_item,
+ "condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0,
},
},
)
return target_doc
+
+
+def validate_against_blanket_order(order_doc):
+ if order_doc.doctype in ("Sales Order", "Purchase Order"):
+ order_data = {}
+
+ for item in order_doc.get("items"):
+ if item.against_blanket_order and item.blanket_order:
+ if item.blanket_order in order_data:
+ if item.item_code in order_data[item.blanket_order]:
+ order_data[item.blanket_order][item.item_code] += item.qty
+ else:
+ order_data[item.blanket_order][item.item_code] = item.qty
+ else:
+ order_data[item.blanket_order] = {item.item_code: item.qty}
+
+ if order_data:
+ allowance = flt(
+ frappe.db.get_single_value(
+ "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings",
+ "over_order_allowance",
+ )
+ )
+ for bo_name, item_data in order_data.items():
+ bo_doc = frappe.get_doc("Blanket Order", bo_name)
+ for item in bo_doc.get("items"):
+ if item.item_code in item_data:
+ remaining_qty = item.qty - item.ordered_qty
+ allowed_qty = remaining_qty + (remaining_qty * (allowance / 100))
+ if allowed_qty < item_data[item.item_code]:
+ frappe.throw(
+ _("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format(
+ item.item_code, allowed_qty, bo_name
+ )
+ )
diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
index 2f1f3ae..58f3c95 100644
--- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
@@ -63,6 +63,33 @@
po1.currency = get_company_currency(po1.company)
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
+ def test_over_order_allowance(self):
+ # Sales Order
+ bo = make_blanket_order(blanket_order_type="Selling", quantity=100)
+
+ frappe.flags.args.doctype = "Sales Order"
+ so = make_order(bo.name)
+ so.currency = get_company_currency(so.company)
+ so.delivery_date = today()
+ so.items[0].qty = 110
+ self.assertRaises(frappe.ValidationError, so.submit)
+
+ frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10)
+ so.submit()
+
+ # Purchase Order
+ bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100)
+
+ frappe.flags.args.doctype = "Purchase Order"
+ po = make_order(bo.name)
+ po.currency = get_company_currency(po.company)
+ po.schedule_date = today()
+ po.items[0].qty = 110
+ self.assertRaises(frappe.ValidationError, po.submit)
+
+ frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
+ po.submit()
+
def make_blanket_order(**args):
args = frappe._dict(args)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 385d0f3..ee9161b 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -21,6 +21,9 @@
)
from erpnext.accounts.party import get_party_account
from erpnext.controllers.selling_controller import SellingController
+from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
+ validate_against_blanket_order,
+)
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
)
@@ -52,6 +55,7 @@
self.validate_warehouse()
self.validate_drop_ship()
self.validate_serial_no_based_delivery()
+ validate_against_blanket_order(self)
validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_order_reference
)
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 6ea66a0..45ad7d9 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -24,6 +24,7 @@
"so_required",
"dn_required",
"sales_update_frequency",
+ "over_order_allowance",
"column_break_5",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
@@ -179,6 +180,12 @@
"fieldname": "allow_sales_order_creation_for_expired_quotation",
"fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation"
+ },
+ {
+ "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
+ "fieldname": "over_order_allowance",
+ "fieldtype": "Float",
+ "label": "Over Order Allowance (%)"
}
],
"icon": "fa fa-cog",
@@ -186,7 +193,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-02-04 12:37:53.380857",
+ "modified": "2023-03-03 11:16:54.333615",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",