diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index 655c4ec0035..115b415eeda 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -21,8 +21,24 @@ class POSClosingEntry(StatusUpdater):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
+ self.validate_duplicate_pos_invoices()
self.validate_pos_invoices()
+ def validate_duplicate_pos_invoices(self):
+ pos_occurences = {}
+ for idx, inv in enumerate(self.pos_transactions, 1):
+ pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
+
+ error_list = []
+ for key, value in pos_occurences.items():
+ if len(value) > 1:
+ error_list.append(
+ _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
+ )
+
+ if error_list:
+ frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
+
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 70e3baff2e4..438ff9f3c4c 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -18,6 +18,22 @@ class POSInvoiceMergeLog(Document):
def validate(self):
self.validate_customer()
self.validate_pos_invoice_status()
+ self.validate_duplicate_pos_invoices()
+
+ def validate_duplicate_pos_invoices(self):
+ pos_occurences = {}
+ for idx, inv in enumerate(self.pos_invoices, 1):
+ pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
+
+ error_list = []
+ for key, value in pos_occurences.items():
+ if len(value) > 1:
+ error_list.append(
+ _("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
+ )
+
+ if error_list:
+ frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_customer(self):
if self.merge_invoices_based_on == "Customer Group":
@@ -426,6 +442,8 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
if closing_entry:
closing_entry.set_status(update=True, status="Failed")
+ if type(error_message) == list:
+ error_message = frappe.json.dumps(error_message)
closing_entry.db_set("error_message", error_message)
raise
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 8ce7d1d3368..e4b4f2260c6 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -395,6 +395,7 @@ def get_column_names():
class GrossProfitGenerator(object):
def __init__(self, filters=None):
+ self.sle = {}
self.data = []
self.average_buying_rate = {}
self.filters = frappe._dict(filters)
@@ -404,7 +405,6 @@ class GrossProfitGenerator(object):
if filters.group_by == "Invoice":
self.group_items_by_invoice()
- self.load_stock_ledger_entries()
self.load_product_bundle()
self.load_non_stock_items()
self.get_returned_invoice_items()
@@ -633,7 +633,7 @@ class GrossProfitGenerator(object):
return flt(row.qty) * item_rate
else:
- my_sle = self.sle.get((item_code, row.warehouse))
+ my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent
if row.dn_detail:
@@ -651,7 +651,7 @@ class GrossProfitGenerator(object):
dn["item_row"],
dn["warehouse"],
)
- my_sle = self.sle.get((item_code, warehouse))
+ my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
@@ -667,15 +667,12 @@ class GrossProfitGenerator(object):
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
- delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
- frappe.qb.from_(delivery_note)
- .inner_join(delivery_note_item)
- .on(delivery_note.name == delivery_note_item.parent)
+ frappe.qb.from_(delivery_note_item)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
- .where(delivery_note.docstatus == 1)
+ .where(delivery_note_item.docstatus == 1)
.where(delivery_note_item.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail)
@@ -940,24 +937,36 @@ class GrossProfitGenerator(object):
"Item", item_code, ["item_name", "description", "item_group", "brand"]
)
- def load_stock_ledger_entries(self):
- res = frappe.db.sql(
- """select item_code, voucher_type, voucher_no,
- voucher_detail_no, stock_value, warehouse, actual_qty as qty
- from `tabStock Ledger Entry`
- where company=%(company)s and is_cancelled = 0
- order by
- item_code desc, warehouse desc, posting_date desc,
- posting_time desc, creation desc""",
- self.filters,
- as_dict=True,
- )
- self.sle = {}
- for r in res:
- if (r.item_code, r.warehouse) not in self.sle:
- self.sle[(r.item_code, r.warehouse)] = []
+ def get_stock_ledger_entries(self, item_code, warehouse):
+ if item_code and warehouse:
+ if (item_code, warehouse) not in self.sle:
+ sle = qb.DocType("Stock Ledger Entry")
+ res = (
+ qb.from_(sle)
+ .select(
+ sle.item_code,
+ sle.voucher_type,
+ sle.voucher_no,
+ sle.voucher_detail_no,
+ sle.stock_value,
+ sle.warehouse,
+ sle.actual_qty.as_("qty"),
+ )
+ .where(
+ (sle.company == self.filters.company)
+ & (sle.item_code == item_code)
+ & (sle.warehouse == warehouse)
+ & (sle.is_cancelled == 0)
+ )
+ .orderby(sle.item_code)
+ .orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
+ .run(as_dict=True)
+ )
- self.sle[(r.item_code, r.warehouse)].append(r)
+ self.sle[(item_code, warehouse)] = res
+
+ return self.sle[(item_code, warehouse)]
+ return []
def load_product_bundle(self):
self.product_bundles = {}
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 935796c7a71..8bd09982bf4 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -252,7 +252,6 @@ def get_already_returned_items(doc):
child.parent = par.name and par.docstatus = 1
and par.is_return = 1 and par.return_against = %s
group by item_code
- for update
""".format(
column, doc.doctype, doc.doctype
),
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
index 7beecaceedf..e7f67caf249 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
@@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = {
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
+
if (column.id == "item") {
- if (data["enough_parts_to_build"] > 0) {
+ if (data["in_stock_qty"] >= data["required_qty"]) {
value = `${data['item']}`;
} else {
value = `${data['item']}`;
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 8ff01f5cb4c..5ce6e9c1460 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -418,8 +418,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
callback: function(r) {
if(r.message) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
- } else {
- frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
}
}
});
diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js
index 12cf6cf84d5..ce489ff52b4 100644
--- a/erpnext/stock/doctype/item_price/item_price.js
+++ b/erpnext/stock/doctype/item_price/item_price.js
@@ -2,7 +2,18 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Item Price", {
- onload: function (frm) {
+ setup(frm) {
+ frm.set_query("item_code", function() {
+ return {
+ filters: {
+ "disabled": 0,
+ "has_variants": 0
+ }
+ };
+ });
+ },
+
+ onload(frm) {
// Fetch price list details
frm.add_fetch("price_list", "buying", "buying");
frm.add_fetch("price_list", "selling", "selling");
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index bcd31ada83e..54d1ae634f5 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Cast_
@@ -21,6 +21,7 @@ class ItemPrice(Document):
self.update_price_list_details()
self.update_item_details()
self.check_duplicates()
+ self.validate_item_template()
def validate_item(self):
if not frappe.db.exists("Item", self.item_code):
@@ -49,6 +50,12 @@ class ItemPrice(Document):
"Item", self.item_code, ["item_name", "description"]
)
+ def validate_item_template(self):
+ if frappe.get_cached_value("Item", self.item_code, "has_variants"):
+ msg = f"Item Price cannot be created for the template item {bold(self.item_code)}"
+
+ frappe.throw(_(msg))
+
def check_duplicates(self):
item_price = frappe.qb.DocType("Item Price")
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index 30d933e247d..8fd4938fa35 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase):
frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True)
+ def test_template_item_price(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item = make_item(
+ "Test Template Item 1",
+ {
+ "has_variants": 1,
+ "variant_based_on": "Manufacturer",
+ },
+ )
+
+ doc = frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "price_list": "_Test Price List",
+ "item_code": item.name,
+ "price_list_rate": 100,
+ }
+ )
+
+ self.assertRaises(frappe.ValidationError, doc.save)
+
def test_duplicate_item(self):
doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.save)
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index b3af309359a..111a0861b71 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -55,7 +55,6 @@ class LandedCostVoucher(Document):
self.get_items_from_purchase_receipts()
self.set_applicable_charges_on_item()
- self.validate_applicable_charges_for_item()
def check_mandatory(self):
if not self.get("purchase_receipts"):
@@ -115,6 +114,13 @@ class LandedCostVoucher(Document):
total_item_cost += item.get(based_on_field)
for item in self.get("items"):
+ if not total_item_cost and not item.get(based_on_field):
+ frappe.throw(
+ _(
+ "It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'"
+ )
+ )
+
item.applicable_charges = flt(
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
item.precision("applicable_charges"),
@@ -162,6 +168,7 @@ class LandedCostVoucher(Document):
)
def on_submit(self):
+ self.validate_applicable_charges_for_item()
self.update_landed_cost()
def on_cancel(self):
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 979b5c4f838..00fa1686c0d 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -175,6 +175,59 @@ class TestLandedCostVoucher(FrappeTestCase):
)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
+ def test_landed_cost_voucher_for_zero_purchase_rate(self):
+ "Test impact of LCV on future stock balances."
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ item = make_item("LCV Stock Item", {"is_stock_item": 1})
+ warehouse = "Stores - _TC"
+
+ pr = make_purchase_receipt(
+ item_code=item.name,
+ warehouse=warehouse,
+ qty=10,
+ rate=0,
+ posting_date=add_days(frappe.utils.nowdate(), -2),
+ )
+
+ self.assertEqual(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
+ "stock_value_difference",
+ ),
+ 0,
+ )
+
+ lcv = make_landed_cost_voucher(
+ company=pr.company,
+ receipt_document_type="Purchase Receipt",
+ receipt_document=pr.name,
+ charges=100,
+ distribute_charges_based_on="Distribute Manually",
+ do_not_save=True,
+ )
+
+ lcv.get_items_from_purchase_receipts()
+ lcv.items[0].applicable_charges = 100
+ lcv.save()
+ lcv.submit()
+
+ self.assertTrue(
+ frappe.db.exists(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
+ )
+ )
+ self.assertEqual(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
+ "stock_value_difference",
+ ),
+ 100,
+ )
+
def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice(
@@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args):
lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = args.company or "_Test Company"
- lcv.distribute_charges_based_on = "Amount"
+ lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
lcv.set(
"purchase_receipts",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index e6025abf067..bb318f72526 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -1134,13 +1134,25 @@ def get_item_account_wise_additional_cost(purchase_document):
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
- "amount"
- ] += (account.amount * item.get(based_on_field) / total_item_cost)
+ if total_item_cost > 0:
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
+ account.expense_account
+ ]["amount"] += (
+ account.amount * item.get(based_on_field) / total_item_cost
+ )
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
- "base_amount"
- ] += (account.base_amount * item.get(based_on_field) / total_item_cost)
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
+ account.expense_account
+ ]["base_amount"] += (
+ account.base_amount * item.get(based_on_field) / total_item_cost
+ )
+ else:
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
+ account.expense_account
+ ]["amount"] += item.applicable_charges
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
+ account.expense_account
+ ]["base_amount"] += item.applicable_charges
return item_account_wise_cost