Merge branch 'develop' into early-payment-loss
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index d67d59b..a4f6a74 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -211,8 +211,7 @@
# Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]:
- # TODO: Set new balance in Base/Account currency
- if d.balance > 0:
+ if d.balance != 0:
current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0'
@@ -399,6 +398,9 @@
journal_entry_accounts = []
for d in accounts:
+ if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
+ continue
+
dr_or_cr = (
"debit_in_account_currency"
if d.get("balance_in_account_currency") > 0
@@ -448,7 +450,13 @@
}
)
- journal_entry_accounts.append(
+ journal_entry.set("accounts", journal_entry_accounts)
+ journal_entry.set_amounts_in_company_currency()
+ journal_entry.set_total_debit_credit()
+
+ self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
+ journal_entry.append(
+ "accounts",
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
@@ -460,10 +468,9 @@
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
- }
+ },
)
- journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
journal_entry.save()
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 2f43914..7005c17 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -495,26 +495,22 @@
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
- grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid)
-
+ grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
-
elif dt == "POS Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
-
elif dt == "Fees":
grand_total = ref_doc.outstanding_amount
if grand_total > 0:
return grand_total
-
else:
frappe.throw(_("Payment Entry is already created"))
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 477c726..4279aa4 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -45,7 +45,10 @@
frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self):
- so_inr = make_sales_order(currency="INR")
+ so_inr = make_sales_order(currency="INR", do_not_save=True)
+ so_inr.disable_rounded_total = 1
+ so_inr.save()
+
pr = make_payment_request(
dt="Sales Order",
dn=so_inr.name,
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 8b4d28b..fc16a91 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -136,7 +136,7 @@
self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self):
- if not self.meta.get_field("commission_rate"):
+ if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted():
return
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 1f76a1a..b261795 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -19,10 +19,6 @@
}
}
});
-
- if (frm.doc.opportunity_from && frm.doc.party_name){
- frm.trigger('set_contact_link');
- }
},
validate: function(frm) {
@@ -130,6 +126,10 @@
} else {
frappe.contacts.clear_address_and_contact(frm);
}
+
+ if (frm.doc.opportunity_from && frm.doc.party_name) {
+ frm.trigger('set_contact_link');
+ }
},
set_contact_link: function(frm) {
@@ -137,6 +137,8 @@
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'}
+ } else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) {
+ frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'}
}
},
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 3133628..e82f379 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -561,7 +561,34 @@
)
def set_transferred_qty_in_job_card_item(self, ste_doc):
- from frappe.query_builder.functions import Sum
+ def _get_job_card_items_transferred_qty(ste_doc):
+ from frappe.query_builder.functions import Sum
+
+ job_card_items_transferred_qty = {}
+ job_card_items = [
+ x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")
+ ]
+
+ if job_card_items:
+ se = frappe.qb.DocType("Stock Entry")
+ sed = frappe.qb.DocType("Stock Entry Detail")
+
+ query = (
+ frappe.qb.from_(sed)
+ .join(se)
+ .on(sed.parent == se.name)
+ .select(sed.job_card_item, Sum(sed.qty))
+ .where(
+ (sed.job_card_item.isin(job_card_items))
+ & (se.docstatus == 1)
+ & (se.purpose == "Material Transfer for Manufacture")
+ )
+ .groupby(sed.job_card_item)
+ )
+
+ job_card_items_transferred_qty = frappe._dict(query.run(as_list=True))
+
+ return job_card_items_transferred_qty
def _validate_over_transfer(row, transferred_qty):
"Block over transfer of items if not allowed in settings."
@@ -578,29 +605,23 @@
exc=JobCardOverTransferError,
)
- for row in ste_doc.items:
- if not row.job_card_item:
- continue
+ job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
- sed = frappe.qb.DocType("Stock Entry Detail")
- se = frappe.qb.DocType("Stock Entry")
- transferred_qty = (
- frappe.qb.from_(sed)
- .join(se)
- .on(sed.parent == se.name)
- .select(Sum(sed.qty))
- .where(
- (sed.job_card_item == row.job_card_item)
- & (se.docstatus == 1)
- & (se.purpose == "Material Transfer for Manufacture")
- )
- ).run()[0][0]
-
+ if job_card_items_transferred_qty:
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
- if not allow_excess:
- _validate_over_transfer(row, transferred_qty)
- frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
+ for row in ste_doc.items:
+ if not row.job_card_item:
+ continue
+
+ transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item))
+
+ if not allow_excess:
+ _validate_over_transfer(row, transferred_qty)
+
+ frappe.db.set_value(
+ "Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)
+ )
def set_transferred_qty(self, update_status=False):
"Set total FG Qty in Job Card for which RM was transferred."
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 4aff42c..97480b2 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -506,7 +506,7 @@
callback: function(r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, {
- "required_qty": 1,
+ "required_qty": row.required_qty || 1,
"item_name": r.message.item_name,
"description": r.message.description,
"source_warehouse": r.message.default_warehouse,
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index cdf1541..3573a3a 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -4,7 +4,8 @@
import frappe
from frappe import _
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import Floor, Sum
+from frappe.utils import cint
from pypika.terms import ExistsCriterion
@@ -34,57 +35,55 @@
def get_bom_stock(filters):
- qty_to_produce = filters.get("qty_to_produce") or 1
- if int(qty_to_produce) < 0:
- frappe.throw(_("Quantity to Produce can not be less than Zero"))
+ qty_to_produce = filters.get("qty_to_produce")
+ if cint(qty_to_produce) <= 0:
+ frappe.throw(_("Quantity to Produce should be greater than zero."))
if filters.get("show_exploded_view"):
bom_item_table = "BOM Explosion Item"
else:
bom_item_table = "BOM Item"
- bin = frappe.qb.DocType("Bin")
- bom = frappe.qb.DocType("BOM")
- bom_item = frappe.qb.DocType(bom_item_table)
-
- query = (
- frappe.qb.from_(bom)
- .inner_join(bom_item)
- .on(bom.name == bom_item.parent)
- .left_join(bin)
- .on(bom_item.item_code == bin.item_code)
- .select(
- bom_item.item_code,
- bom_item.description,
- bom_item.stock_qty,
- bom_item.stock_uom,
- (bom_item.stock_qty / bom.quantity) * qty_to_produce,
- Sum(bin.actual_qty),
- Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
- )
- .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
- .groupby(bom_item.item_code)
+ warehouse_details = frappe.db.get_value(
+ "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
- if filters.get("warehouse"):
- warehouse_details = frappe.db.get_value(
- "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
- )
+ BOM = frappe.qb.DocType("BOM")
+ BOM_ITEM = frappe.qb.DocType(bom_item_table)
+ BIN = frappe.qb.DocType("Bin")
+ WH = frappe.qb.DocType("Warehouse")
+ CONDITIONS = ()
- if warehouse_details:
- wh = frappe.qb.DocType("Warehouse")
- query = query.where(
- ExistsCriterion(
- frappe.qb.from_(wh)
- .select(wh.name)
- .where(
- (wh.lft >= warehouse_details.lft)
- & (wh.rgt <= warehouse_details.rgt)
- & (bin.warehouse == wh.name)
- )
- )
+ if warehouse_details:
+ CONDITIONS = ExistsCriterion(
+ frappe.qb.from_(WH)
+ .select(WH.name)
+ .where(
+ (WH.lft >= warehouse_details.lft)
+ & (WH.rgt <= warehouse_details.rgt)
+ & (BIN.warehouse == WH.name)
)
- else:
- query = query.where(bin.warehouse == filters.get("warehouse"))
+ )
+ else:
+ CONDITIONS = BIN.warehouse == filters.get("warehouse")
- return query.run()
+ QUERY = (
+ frappe.qb.from_(BOM)
+ .inner_join(BOM_ITEM)
+ .on(BOM.name == BOM_ITEM.parent)
+ .left_join(BIN)
+ .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
+ .select(
+ BOM_ITEM.item_code,
+ BOM_ITEM.description,
+ BOM_ITEM.stock_qty,
+ BOM_ITEM.stock_uom,
+ BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
+ Sum(BIN.actual_qty).as_("actual_qty"),
+ Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
+ )
+ .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
+ .groupby(BOM_ITEM.item_code)
+ )
+
+ return QUERY.run()
diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
new file mode 100644
index 0000000..1c56ebe
--- /dev/null
+++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
@@ -0,0 +1,108 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+import frappe
+from frappe.exceptions import ValidationError
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import floor
+
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
+ get_bom_stock as bom_stock_report,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+
+class TestBomStockReport(FrappeTestCase):
+ def setUp(self):
+ self.warehouse = "_Test Warehouse - _TC"
+ self.fg_item, self.rm_items = create_items()
+ make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
+ make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
+ self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
+
+ def test_bom_stock_report(self):
+ # Test 1: When `qty_to_produce` is 0.
+ filters = frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": "Stores - _TC",
+ "qty_to_produce": 0,
+ }
+ )
+ self.assertRaises(ValidationError, bom_stock_report, filters)
+
+ # Test 2: When stock is not available.
+ data = bom_stock_report(
+ frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": "Stores - _TC",
+ "qty_to_produce": 1,
+ }
+ )
+ )
+ expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+ # Test 3: When stock is available.
+ data = bom_stock_report(
+ frappe._dict(
+ {
+ "bom": self.bom.name,
+ "warehouse": self.warehouse,
+ "qty_to_produce": 1,
+ }
+ )
+ )
+ expected_data = get_expected_data(self.bom, self.warehouse, 1)
+ self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+
+def create_items():
+ fg_item = make_item(properties={"is_stock_item": 1}).name
+ rm_item1 = make_item(
+ properties={
+ "is_stock_item": 1,
+ "standard_rate": 100,
+ "opening_stock": 100,
+ "last_purchase_rate": 100,
+ }
+ ).name
+ rm_item2 = make_item(
+ properties={
+ "is_stock_item": 1,
+ "standard_rate": 200,
+ "opening_stock": 200,
+ "last_purchase_rate": 200,
+ }
+ ).name
+
+ return fg_item, [rm_item1, rm_item2]
+
+
+def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
+ expected_data = []
+
+ for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
+ in_stock_qty = frappe.get_cached_value(
+ "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
+ )
+
+ expected_data.append(
+ [
+ item.item_code,
+ item.description,
+ item.stock_qty,
+ item.stock_uom,
+ item.stock_qty * qty_to_produce / bom.quantity,
+ in_stock_qty,
+ floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
+ if in_stock_qty
+ else None,
+ ]
+ )
+
+ return expected_data
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 2d29454..8d69ea0 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1884,11 +1884,13 @@
get_advances() {
if(!this.frm.is_return) {
+ var me = this;
return this.frm.call({
method: "set_advances",
doc: this.frm.doc,
callback: function(r, rt) {
refresh_field("advances");
+ me.frm.dirty();
}
})
}
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 5ce6e9c..f1df3a1 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -253,7 +253,7 @@
}
calculate_commission() {
- if(!this.frm.fields_dict.commission_rate) return;
+ if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
if(this.frm.doc.commission_rate > 100) {
this.frm.set_value("commission_rate", 100);
diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py
index fb1a28d..0c24d3c 100644
--- a/erpnext/stock/doctype/item_alternative/item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/item_alternative.py
@@ -54,7 +54,7 @@
if not item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code))
if self.two_way and not alternative_item_data.allow_alternative_item:
- frappe.throw(alternate_item_check_msg.format(self.item_code))
+ frappe.throw(alternate_item_check_msg.format(self.alternative_item_code))
def validate_duplicate(self):
if frappe.db.get_value(
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index dcbc460..8aeb751 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -10,6 +10,7 @@
import frappe
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
@@ -180,6 +181,34 @@
self.update_requested_qty()
self.update_requested_qty_in_production_plan()
+ def get_mr_items_ordered_qty(self, mr_items):
+ mr_items_ordered_qty = {}
+ mr_items = [d.name for d in self.get("items") if d.name in mr_items]
+
+ doctype = qty_field = None
+ if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
+ doctype = frappe.qb.DocType("Stock Entry Detail")
+ qty_field = doctype.transfer_qty
+ elif self.material_request_type == "Manufacture":
+ doctype = frappe.qb.DocType("Work Order")
+ qty_field = doctype.qty
+
+ if doctype and qty_field:
+ query = (
+ frappe.qb.from_(doctype)
+ .select(doctype.material_request_item, Sum(qty_field))
+ .where(
+ (doctype.material_request == self.name)
+ & (doctype.material_request_item.isin(mr_items))
+ & (doctype.docstatus == 1)
+ )
+ .groupby(doctype.material_request_item)
+ )
+
+ mr_items_ordered_qty = frappe._dict(query.run())
+
+ return mr_items_ordered_qty
+
def update_completed_qty(self, mr_items=None, update_modified=True):
if self.material_request_type == "Purchase":
return
@@ -187,18 +216,13 @@
if not mr_items:
mr_items = [d.name for d in self.get("items")]
+ mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items)
+ mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
+
for d in self.get("items"):
if d.name in mr_items:
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
- d.ordered_qty = flt(
- frappe.db.sql(
- """select sum(transfer_qty)
- from `tabStock Entry Detail` where material_request = %s
- and material_request_item = %s and docstatus = 1""",
- (self.name, d.name),
- )[0][0]
- )
- mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
+ d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
if mr_qty_allowance:
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
@@ -217,14 +241,7 @@
)
elif self.material_request_type == "Manufacture":
- d.ordered_qty = flt(
- frappe.db.sql(
- """select sum(qty)
- from `tabWork Order` where material_request = %s
- and material_request_item = %s and docstatus = 1""",
- (self.name, d.name),
- )[0][0]
- )
+ d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
diff --git a/pyproject.toml b/pyproject.toml
index 1c93eed..0718e5b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,9 +28,6 @@
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
-[tool.bench.dev-dependencies]
-hypothesis = "~=6.31.0"
-
[tool.black]
line-length = 99