Merge pull request #30945 from ankush/stock_analytics_fix
fix: stock analytics report shows incorrect data there's no stock movement in a period
diff --git a/.eslintrc b/.eslintrc
index 46fb354..276d6ff 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -5,7 +5,7 @@
"es6": true
},
"parserOptions": {
- "ecmaVersion": 9,
+ "ecmaVersion": 11,
"sourceType": "module"
},
"extends": "eslint:recommended",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 5860c4c..9189f18 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -637,6 +637,8 @@
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
+
+ stock_entry.set_missing_values()
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer"))
@@ -724,7 +726,7 @@
add_items_in_ste(ste_doc, value, value.qty, po_details)
ste_doc.set_stock_entry_type()
- ste_doc.calculate_rate_and_amount()
+ ste_doc.set_missing_values()
return ste_doc
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 78645e0..46013bb 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2451,11 +2451,21 @@
parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row
)
- def validate_quantity(child_item, d):
- if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
+ def validate_quantity(child_item, new_data):
+ if not flt(new_data.get("qty")):
+ frappe.throw(
+ _("Row # {0}: Quantity for Item {1} cannot be zero").format(
+ new_data.get("idx"), frappe.bold(new_data.get("item_code"))
+ ),
+ title=_("Invalid Qty"),
+ )
+
+ if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity"))
- if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
+ if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(
+ child_item.received_qty
+ ):
frappe.throw(_("Cannot set quantity less than received quantity"))
data = json.loads(trans_items)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index bf4f82f..a98fc94 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -764,8 +764,6 @@
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
- target.set_transfer_qty()
- target.calculate_rate_and_amount()
target.set_missing_values()
target.set_stock_entry_type()
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d5b1592..1fef240 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -369,4 +369,5 @@
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
erpnext.patches.v14_0.discount_accounting_separation
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
-erpnext.patches.v13_0.create_accounting_dimensions_in_orders
\ No newline at end of file
+erpnext.patches.v13_0.create_accounting_dimensions_in_orders
+erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py b/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py
new file mode 100644
index 0000000..a4d7012
--- /dev/null
+++ b/erpnext/patches/v13_0/set_per_billed_in_return_delivery_note.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+
+
+def execute():
+ dn = frappe.qb.DocType("Delivery Note")
+ dn_item = frappe.qb.DocType("Delivery Note Item")
+
+ dn_list = (
+ frappe.qb.from_(dn)
+ .inner_join(dn_item)
+ .on(dn.name == dn_item.parent)
+ .select(dn.name)
+ .where(dn.docstatus == 1)
+ .where(dn.is_return == 1)
+ .where(dn.per_billed < 100)
+ .where(dn_item.returned_qty > 0)
+ .run(as_dict=True)
+ )
+
+ frappe.qb.update(dn_item).inner_join(dn).on(dn.name == dn_item.parent).set(
+ dn_item.returned_qty, 0
+ ).where(dn.is_return == 1).where(dn_item.returned_qty > 0).run()
+
+ for d in dn_list:
+ dn_doc = frappe.get_doc("Delivery Note", d.get("name"))
+ dn_doc.run_method("update_billing_status")
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 767221e..c3812f3 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1384,12 +1384,15 @@
var me = this;
var args = this._get_args(item);
if (!(args.items && args.items.length)) {
- if(calculate_taxes_and_totals) me.calculate_taxes_and_totals();
+ if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();
return;
}
// Target doc created from a mapped doc
if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
+ // Calculate totals even though pricing rule is not applied.
+ // `apply_pricing_rule` is triggered due to change in data which most likely contributes to Total.
+ if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();
return;
}
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index f72b85c..3ae1234 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -10,6 +10,12 @@
this.serial_no_field = opts.serial_no_field || "serial_no";
this.batch_no_field = opts.batch_no_field || "batch_no";
this.qty_field = opts.qty_field || "qty";
+ // field name on row which defines max quantity to be scanned e.g. picklist
+ this.max_qty_field = opts.max_qty_field;
+ // scanner won't add a new row if this flag is set.
+ this.dont_allow_new_row = opts.dont_allow_new_row;
+ // scanner will ask user to type the quantity instead of incrementing by 1
+ this.prompt_qty = opts.prompt_qty;
this.items_table_name = opts.items_table_name || "items";
this.items_table = this.frm.doc[this.items_table_name];
@@ -42,10 +48,7 @@
.then((r) => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
- frappe.show_alert({
- message: __("Cannot find Item with this Barcode"),
- indicator: "red",
- });
+ this.show_alert(__("Cannot find Item with this Barcode"), "red");
this.clean_up();
return;
}
@@ -56,22 +59,18 @@
update_table(data) {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
- let row = null;
const {item_code, barcode, batch_no, serial_no} = data;
- // Check if batch is scanned and table has batch no field
- let batch_no_scan =
- Boolean(batch_no) && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
-
- if (batch_no_scan) {
- row = this.get_batch_row_to_modify(batch_no);
- } else {
- // serial or barcode scan
- row = this.get_row_to_modify_on_scan(item_code);
- }
+ let row = this.get_row_to_modify_on_scan(item_code, batch_no);
if (!row) {
+ if (this.dont_allow_new_row) {
+ this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
+ this.clean_up();
+ return;
+ }
+
// add new row if new item/batch is scanned
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
// trigger any row add triggers defined on child table.
@@ -83,9 +82,10 @@
return;
}
- this.show_scan_message(row.idx, row.item_code);
this.set_selector_trigger_flag(row, data);
- this.set_item(row, item_code);
+ this.set_item(row, item_code).then(qty => {
+ this.show_scan_message(row.idx, row.item_code, qty);
+ });
this.set_serial_no(row, serial_no);
this.set_batch_no(row, batch_no);
this.set_barcode(row, barcode);
@@ -106,9 +106,23 @@
}
set_item(row, item_code) {
- const item_data = { item_code: item_code };
- item_data[this.qty_field] = (row[this.qty_field] || 0) + 1;
- frappe.model.set_value(row.doctype, row.name, item_data);
+ return new Promise(resolve => {
+ const increment = (value = 1) => {
+ const item_data = {item_code: item_code};
+ item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
+ frappe.model.set_value(row.doctype, row.name, item_data);
+ };
+
+ if (this.prompt_qty) {
+ frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
+ increment(value);
+ resolve(value);
+ });
+ } else {
+ increment();
+ resolve();
+ }
+ });
}
set_serial_no(row, serial_no) {
@@ -137,53 +151,42 @@
}
}
- show_scan_message(idx, exist = null) {
+ show_scan_message(idx, exist = null, qty = 1) {
// show new row or qty increase toast
if (exist) {
- frappe.show_alert(
- {
- message: __("Row #{0}: Qty increased by 1", [idx]),
- indicator: "green",
- },
- 5
- );
+ this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green");
} else {
- frappe.show_alert(
- {
- message: __("Row #{0}: Item added", [idx]),
- indicator: "green",
- },
- 5
- );
+ this.show_alert(__("Row #{0}: Item added", [idx]), "green")
}
}
is_duplicate_serial_no(row, serial_no) {
- const is_duplicate = !!serial_no && !!row[this.serial_no_field]
- && row[this.serial_no_field].includes(serial_no);
+ const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
if (is_duplicate) {
- frappe.show_alert(
- {
- message: __("Serial No {0} is already added", [serial_no]),
- indicator: "orange",
- },
- 5
- );
+ this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
return is_duplicate;
}
- get_batch_row_to_modify(batch_no) {
- // get row if batch already exists in table
- const existing_batch_row = this.items_table.find((d) => d.batch_no === batch_no);
- return existing_batch_row || this.get_existing_blank_row();
- }
+ get_row_to_modify_on_scan(item_code, batch_no) {
+ let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
- get_row_to_modify_on_scan(item_code) {
- // get an existing item row to increment or blank row to modify
- const existing_item_row = this.items_table.find((d) => d.item_code === item_code);
- return existing_item_row || this.get_existing_blank_row();
+ // Check if batch is scanned and table has batch no field
+ let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
+ let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field);
+
+ const matching_row = (row) => {
+ const item_match = row.item_code == item_code;
+ const batch_match = row.batch_no == batch_no;
+ const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
+
+ return item_match
+ && (!is_batch_no_scan || batch_match)
+ && (!check_max_qty || qty_in_limit)
+ }
+
+ return this.items_table.find(matching_row) || this.get_existing_blank_row();
}
get_existing_blank_row() {
@@ -194,4 +197,7 @@
this.scan_barcode_field.set_value("");
refresh_field(this.items_table_name);
}
+ show_alert(msg, indicator, duration=3) {
+ frappe.show_alert({message: msg, indicator: indicator}, duration);
+ }
};
diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index 091c20c..e10df2a 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -238,4 +238,5 @@
"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
},
"type": "bar",
+ "fieldtype": "Currency",
}
diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py
index 4e0758d..4d71ce7 100644
--- a/erpnext/selling/report/quotation_trends/quotation_trends.py
+++ b/erpnext/selling/report/quotation_trends/quotation_trends.py
@@ -54,4 +54,5 @@
},
"type": "line",
"lineOptions": {"regionFill": 1},
+ "fieldtype": "Currency",
}
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py
index 1a2476a..9d7d806 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.py
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.py
@@ -415,3 +415,8 @@
else:
labels = [d.get("label") for d in self.columns[1 : length - 1]]
self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"}
+
+ if self.filters["value_quantity"] == "Value":
+ self.chart["fieldtype"] = "Currency"
+ else:
+ self.chart["fieldtype"] = "Float"
diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
index 719f1c5..18f448c 100644
--- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py
+++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
@@ -51,4 +51,5 @@
},
"type": "line",
"lineOptions": {"regionFill": 1},
+ "fieldtype": "Currency",
}
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index f97e7ca..0738bfb 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -962,6 +962,44 @@
automatically_fetch_payment_terms(enable=0)
+ def test_returned_qty_in_return_dn(self):
+ # SO ---> SI ---> DN
+ # |
+ # |---> DN(Partial Sales Return) ---> SI(Credit Note)
+ # |
+ # |---> DN(Partial Sales Return) ---> SI(Credit Note)
+
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
+ from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+
+ so = make_sales_order(qty=10)
+ si = make_sales_invoice(so.name)
+ si.insert()
+ si.submit()
+ dn = make_delivery_note(si.name)
+ dn.insert()
+ dn.submit()
+ self.assertEqual(dn.items[0].returned_qty, 0)
+ self.assertEqual(dn.per_billed, 100)
+
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+
+ dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
+ si1 = make_sales_invoice(dn1.name)
+ si1.insert()
+ si1.submit()
+ dn1.reload()
+ self.assertEqual(dn1.items[0].returned_qty, 0)
+ self.assertEqual(dn1.per_billed, 100)
+
+ dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
+ si2 = make_sales_invoice(dn2.name)
+ si2.insert()
+ si2.submit()
+ dn2.reload()
+ self.assertEqual(dn2.items[0].returned_qty, 0)
+ self.assertEqual(dn2.per_billed, 100)
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index e2eb2a4..2d7abc8 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -737,7 +737,9 @@
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
- "label": "Returned Qty in Stock UOM"
+ "label": "Returned Qty in Stock UOM",
+ "no_copy": 1,
+ "read_only": 1
},
{
"fieldname": "incoming_rate",
@@ -778,7 +780,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-03-31 18:36:24.671913",
+ "modified": "2022-05-02 12:09:39.610075",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index a70ff17..c998629 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -597,7 +597,7 @@
if source.material_request_type == "Customer Provided":
target.purpose = "Material Receipt"
- target.run_method("calculate_rate_and_amount")
+ target.set_missing_values()
target.set_stock_entry_type()
target.set_job_card_data()
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 13b74b5..799406c 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -158,6 +158,19 @@
get_query_filters: get_query_filters
});
});
+ },
+ scan_barcode: (frm) => {
+ const opts = {
+ frm,
+ items_table_name: 'locations',
+ qty_field: 'picked_qty',
+ max_qty_field: 'qty',
+ dont_allow_new_row: true,
+ prompt_qty: frm.doc.prompt_qty,
+ serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field.
+ };
+ const barcode_scanner = new erpnext.utils.BarcodeScanner(opts);
+ barcode_scanner.process_scan();
}
});
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index e984c08..ff20909 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -17,6 +17,11 @@
"parent_warehouse",
"get_item_locations",
"section_break_6",
+ "scan_barcode",
+ "column_break_13",
+ "scan_mode",
+ "prompt_qty",
+ "section_break_15",
"locations",
"amended_from",
"print_settings_section",
@@ -36,6 +41,7 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:!doc.docstatus",
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
@@ -126,11 +132,38 @@
"fieldtype": "Check",
"label": "Group Same Items",
"print_hide": 1
+ },
+ {
+ "fieldname": "section_break_15",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "scan_barcode",
+ "fieldtype": "Data",
+ "label": "Scan Barcode",
+ "options": "Barcode"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "If checked, picked qty won't automatically be fulfilled on submit of pick list.",
+ "fieldname": "scan_mode",
+ "fieldtype": "Check",
+ "label": "Scan Mode"
+ },
+ {
+ "default": "0",
+ "fieldname": "prompt_qty",
+ "fieldtype": "Check",
+ "label": "Prompt Qty"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2022-04-21 07:56:40.646473",
+ "modified": "2022-05-11 09:09:53.029312",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 70d2f23..7dc3ba0 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -41,8 +41,15 @@
def before_submit(self):
update_sales_orders = set()
for item in self.locations:
- # if the user has not entered any picked qty, set it to stock_qty, before submit
- if item.picked_qty == 0:
+ if self.scan_mode and item.picked_qty < item.stock_qty:
+ frappe.throw(
+ _(
+ "Row {0} picked quantity is less than the required quantity, additional {1} {2} required."
+ ).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom),
+ title=_("Pick List Incomplete"),
+ )
+ elif not self.scan_mode and item.picked_qty == 0:
+ # if the user has not entered any picked qty, set it to stock_qty, before submit
item.picked_qty = item.stock_qty
if item.sales_order_item:
@@ -672,8 +679,7 @@
else:
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
- stock_entry.set_actual_qty()
- stock_entry.calculate_rate_and_amount()
+ stock_entry.set_missing_values()
return stock_entry.as_dict()
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
index a96ebfc..a6f8c0d 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -202,4 +202,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index ec0e809..fcf0cd1 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -1036,6 +1036,7 @@
def set_missing_values(source, target):
target.stock_entry_type = "Material Transfer"
target.purpose = "Material Transfer"
+ target.set_missing_values()
doclist = get_mapped_doc(
"Purchase Receipt",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 1df56ef..540ad18 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -470,7 +470,9 @@
},
callback: function(r) {
if (!r.exc) {
- $.extend(child, r.message);
+ ["actual_qty", "basic_rate"].forEach((field) => {
+ frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
+ });
frm.events.calculate_basic_amount(frm, child);
}
}
@@ -1057,8 +1059,8 @@
function check_should_not_attach_bom_items(bom_no) {
return (
- bom_no === undefined ||
- (erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
+ bom_no === undefined ||
+ (erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
);
}
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 27a6eaf..2a7354d 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -2197,6 +2197,12 @@
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
+ def set_missing_values(self):
+ "Updates rate and availability of all the items of mapped doc."
+ self.set_transfer_qty()
+ self.set_actual_qty()
+ self.calculate_rate_and_amount()
+
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
@@ -2246,6 +2252,7 @@
def make_stock_in_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.set_stock_entry_type()
+ target.set_missing_values()
def update_item(source_doc, target_doc, source_parent):
target_doc.t_warehouse = ""
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index b3df728..c5c0cef 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -132,6 +132,7 @@
)
s.set_stock_entry_type()
+
if not args.do_not_save:
s.insert()
if not args.do_not_submit:
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index b9c57c1..71baf9f 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1424,6 +1424,25 @@
self.assertRaises(frappe.ValidationError, se.save)
+ def test_mapped_stock_entry(self):
+ "Check if rate and stock details are populated in mapped SE given warehouse."
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_stock_entry
+ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+
+ item_code = "_TestMappedItem"
+ create_item(item_code, is_stock_item=True)
+
+ pr = make_purchase_receipt(
+ item_code=item_code, qty=2, rate=100, company="_Test Company", warehouse="Stores - _TC"
+ )
+
+ mapped_se = make_stock_entry(pr.name)
+
+ self.assertEqual(mapped_se.items[0].s_warehouse, "Stores - _TC")
+ self.assertEqual(mapped_se.items[0].actual_qty, 2)
+ self.assertEqual(mapped_se.items[0].basic_rate, 100)
+ self.assertEqual(mapped_se.items[0].basic_amount, 200)
+
def make_serialized_item(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
index e545b8e..9a85431 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
@@ -170,6 +170,7 @@
"options": "Warehouse"
},
{
+ "depends_on": "eval:!doc.docstatus",
"fieldname": "section_break_22",
"fieldtype": "Section Break"
},
@@ -182,7 +183,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-03-27 08:57:47.161959",
+ "modified": "2022-05-11 09:10:26.327652",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/__init__.py
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js
new file mode 100644
index 0000000..0b8f496
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.js
@@ -0,0 +1,53 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+const DIFFERNCE_FIELD_NAMES = [
+ "fifo_qty_diff",
+ "fifo_value_diff",
+];
+
+frappe.query_reports["FIFO Queue vs Qty After Transaction Comparison"] = {
+ "filters": [
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ get_query: function() {
+ return {
+ filters: {is_stock_item: 1, has_serial_no: 0}
+ }
+ }
+ },
+ {
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse",
+ },
+ {
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "label": "From Posting Date",
+ },
+ {
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "From Posting Date",
+ }
+ ],
+ formatter (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+ if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
+ value = "<span style='color:red'>" + value + "</span>";
+ }
+ return value;
+ },
+};
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json
new file mode 100644
index 0000000..5e958aa
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.json
@@ -0,0 +1,27 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2022-05-11 04:09:13.460652",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "abc",
+ "modified": "2022-05-11 04:09:20.232177",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "FIFO Queue vs Qty After Transaction Comparison",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "FIFO Queue vs Qty After Transaction Comparison",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Administrator"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py
new file mode 100644
index 0000000..9e14033
--- /dev/null
+++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py
@@ -0,0 +1,212 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe import _
+from frappe.utils import flt
+from frappe.utils.nestedset import get_descendants_of
+
+SLE_FIELDS = (
+ "name",
+ "item_code",
+ "warehouse",
+ "posting_date",
+ "posting_time",
+ "creation",
+ "voucher_type",
+ "voucher_no",
+ "actual_qty",
+ "qty_after_transaction",
+ "stock_queue",
+ "batch_no",
+ "stock_value",
+ "valuation_rate",
+)
+
+
+def execute(filters=None):
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+
+def get_data(filters):
+ if not any([filters.warehouse, filters.item_code, filters.item_group]):
+ frappe.throw(_("Any one of following filters required: warehouse, Item Code, Item Group"))
+ sles = get_stock_ledger_entries(filters)
+ return find_first_bad_queue(sles)
+
+
+def get_stock_ledger_entries(filters):
+
+ sle_filters = {"is_cancelled": 0}
+
+ if filters.warehouse:
+ children = get_descendants_of("Warehouse", filters.warehouse)
+ sle_filters["warehouse"] = ("in", children + [filters.warehouse])
+
+ if filters.item_code:
+ sle_filters["item_code"] = filters.item_code
+ elif filters.get("item_group"):
+ item_group = filters.get("item_group")
+ children = get_descendants_of("Item Group", item_group)
+ item_group_filter = {"item_group": ("in", children + [item_group])}
+ sle_filters["item_code"] = (
+ "in",
+ frappe.get_all("Item", filters=item_group_filter, pluck="name", order_by=None),
+ )
+
+ if filters.from_date:
+ sle_filters["posting_date"] = (">=", filters.from_date)
+ if filters.to_date:
+ sle_filters["posting_date"] = ("<=", filters.to_date)
+
+ return frappe.get_all(
+ "Stock Ledger Entry",
+ fields=SLE_FIELDS,
+ filters=sle_filters,
+ order_by="timestamp(posting_date, posting_time), creation",
+ )
+
+
+def find_first_bad_queue(sles):
+ item_warehouse_sles = {}
+ for sle in sles:
+ item_warehouse_sles.setdefault((sle.item_code, sle.warehouse), []).append(sle)
+
+ data = []
+
+ for _item_wh, sles in item_warehouse_sles.items():
+ for idx, sle in enumerate(sles):
+ queue = json.loads(sle.stock_queue or "[]")
+
+ sle.fifo_queue_qty = 0.0
+ sle.fifo_stock_value = 0.0
+ for qty, rate in queue:
+ sle.fifo_queue_qty += flt(qty)
+ sle.fifo_stock_value += flt(qty) * flt(rate)
+
+ sle.fifo_qty_diff = sle.qty_after_transaction - sle.fifo_queue_qty
+ sle.fifo_value_diff = sle.stock_value - sle.fifo_stock_value
+
+ if sle.batch_no:
+ sle.use_batchwise_valuation = frappe.db.get_value(
+ "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
+ )
+
+ if abs(sle.fifo_qty_diff) > 0.001 or abs(sle.fifo_value_diff) > 0.1:
+ if idx:
+ data.append(sles[idx - 1])
+ data.append(sle)
+ data.append({})
+ break
+
+ return data
+
+
+def get_columns():
+ return [
+ {
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "label": _("Stock Ledger Entry"),
+ "options": "Stock Ledger Entry",
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": _("Item Code"),
+ "options": "Item",
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": _("Warehouse"),
+ "options": "Warehouse",
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Data",
+ "label": _("Posting Date"),
+ },
+ {
+ "fieldname": "posting_time",
+ "fieldtype": "Data",
+ "label": _("Posting Time"),
+ },
+ {
+ "fieldname": "creation",
+ "fieldtype": "Data",
+ "label": _("Creation"),
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": _("Voucher Type"),
+ "options": "DocType",
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": _("Voucher No"),
+ "options": "voucher_type",
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": _("Batch"),
+ "options": "Batch",
+ },
+ {
+ "fieldname": "use_batchwise_valuation",
+ "fieldtype": "Check",
+ "label": _("Batchwise Valuation"),
+ },
+ {
+ "fieldname": "actual_qty",
+ "fieldtype": "Float",
+ "label": _("Qty Change"),
+ },
+ {
+ "fieldname": "qty_after_transaction",
+ "fieldtype": "Float",
+ "label": _("(A) Qty After Transaction"),
+ },
+ {
+ "fieldname": "stock_queue",
+ "fieldtype": "Data",
+ "label": _("FIFO/LIFO Queue"),
+ },
+ {
+ "fieldname": "fifo_queue_qty",
+ "fieldtype": "Float",
+ "label": _("(C) Total qty in queue"),
+ },
+ {
+ "fieldname": "fifo_qty_diff",
+ "fieldtype": "Float",
+ "label": _("A - C"),
+ },
+ {
+ "fieldname": "stock_value",
+ "fieldtype": "Float",
+ "label": _("(D) Balance Stock Value"),
+ },
+ {
+ "fieldname": "fifo_stock_value",
+ "fieldtype": "Float",
+ "label": _("(E) Balance Stock Value in Queue"),
+ },
+ {
+ "fieldname": "fifo_value_diff",
+ "fieldtype": "Float",
+ "label": _("D - E"),
+ },
+ {
+ "fieldname": "valuation_rate",
+ "fieldtype": "Float",
+ "label": _("(H) Valuation Rate"),
+ },
+ ]
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 55b9104..d118d8e 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -65,6 +65,8 @@
("Delayed Item Report", {"based_on": "Delivery Note"}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
+ ("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
+ ("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),
]
OPTIONAL_FILTERS = {
diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv
index 6ca3344..fb56ff6 100644
--- a/erpnext/translations/ru.csv
+++ b/erpnext/translations/ru.csv
@@ -1357,7 +1357,7 @@
"Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates.","Цена товара отображается несколько раз на основе Прайс-листа, Поставщика / Клиента, Валюты, Предмет, UOM, Кол-во и Даты.",
Item Price updated for {0} in Price List {1},Цена продукта {0} обновлена в прайс-листе {1},
Item Row {0}: {1} {2} does not exist in above '{1}' table,Элемент Row {0}: {1} {2} не существует в таблице «{1}»,
-Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,"Строка налога {0} должен иметь счет типа Налога, Доход, Расходов или Облагаемый налогом,"
+Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,"Строка налога {0} должен иметь счет типа Налога, Доход, Расходов или Облагаемый налогом",
Item Template,Шаблон продукта,
Item Variant Settings,Параметры модификации продкута,
Item Variant {0} already exists with same attributes,Модификация продукта {0} с этими атрибутами уже существует,