Merge pull request #26252 from deepeshgarg007/loan_security_pledge_on_cancel
fix: Cancellation of Loan Security Pledges
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 35097b9..8196cff 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -11,7 +11,7 @@
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
-from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
+from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate
@@ -497,9 +497,6 @@
})
if future_sle_exists(args):
create_repost_item_valuation_entry(args)
- elif not is_reposting_pending():
- check_if_stock_and_account_balance_synced(self.posting_date,
- self.company, self.doctype, self.name)
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js
index c3c3cb8..7964078 100644
--- a/erpnext/hr/doctype/attendance/attendance.js
+++ b/erpnext/hr/doctype/attendance/attendance.js
@@ -11,5 +11,5 @@
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{
query: "erpnext.controllers.queries.employee_query"
- }
+ }
}
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index f3b8a79..3412675 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -15,6 +15,7 @@
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
self.validate_attendance_date()
self.validate_duplicate_record()
+ self.validate_employee_status()
self.check_leave_record()
def validate_attendance_date(self):
@@ -38,6 +39,10 @@
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
+ def validate_employee_status(self):
+ if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
+ frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
+
def check_leave_record(self):
leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 0c7eafe..9a3bac0 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -21,6 +21,9 @@
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
+ get_query: () => {
+ return {query: "erpnext.controllers.queries.employee_query"}
+ },
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
index 4c31bd0..f19a1b0 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js
@@ -13,7 +13,7 @@
refresh: function(frm) {
erpnext.hide_company();
- if (frm.doc.customer && frm.doc.docstatus === 1) {
+ if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
frm.add_custom_button(__("Sales Order"), function() {
frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
index 0330e5c..a63fc4d 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2018-05-24 07:18:08.256060",
"doctype": "DocType",
@@ -79,6 +80,7 @@
"reqd": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
@@ -129,8 +131,10 @@
"label": "Terms and Conditions Details"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-11-18 19:37:37.151686",
+ "links": [],
+ "modified": "2021-06-29 00:30:30.621636",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 3e85560..c58f017 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1,7 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
+from typing import List
+from collections import deque
import frappe, erpnext
from frappe.utils import cint, cstr, flt, today
from frappe import _
@@ -16,14 +17,85 @@
import functools
-from six import string_types
-
from operator import itemgetter
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
}
+
+class BOMTree:
+ """Full tree representation of a BOM"""
+
+ # specifying the attributes to save resources
+ # ref: https://docs.python.org/3/reference/datamodel.html#slots
+ __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
+
+ def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None:
+ self.name = name # name of node, BOM number if is_bom else item_code
+ self.child_items: List["BOMTree"] = [] # list of child items
+ self.is_bom = is_bom # true if the node is a BOM and not a leaf item
+ self.item_code: str = None # item_code associated with node
+ self.qty = qty # required unit quantity to make one unit of parent item.
+ self.exploded_qty = exploded_qty # total exploded qty required for making root of tree.
+ if not self.is_bom:
+ self.item_code = self.name
+ else:
+ self.__create_tree()
+
+ def __create_tree(self):
+ bom = frappe.get_cached_doc("BOM", self.name)
+ self.item_code = bom.item
+
+ for item in bom.get("items", []):
+ qty = item.qty / bom.quantity # quantity per unit
+ exploded_qty = self.exploded_qty * qty
+ if item.bom_no:
+ child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
+ self.child_items.append(child)
+ else:
+ self.child_items.append(
+ BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty)
+ )
+
+ def level_order_traversal(self) -> List["BOMTree"]:
+ """Get level order traversal of tree.
+ E.g. for following tree the traversal will return list of nodes in order from top to bottom.
+ BOM:
+ - SubAssy1
+ - item1
+ - item2
+ - SubAssy2
+ - item3
+ - item4
+
+ returns = [SubAssy1, item1, item2, SubAssy2, item3, item4]
+ """
+ traversal = []
+ q = deque()
+ q.append(self)
+
+ while q:
+ node = q.popleft()
+
+ for child in node.child_items:
+ traversal.append(child)
+ q.append(child)
+
+ return traversal
+
+ def __str__(self) -> str:
+ return (
+ f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}"
+ f" exploded_qty: {self.exploded_qty}"
+ )
+
+ def __repr__(self, level: int = 0) -> str:
+ rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n"
+ for child in self.child_items:
+ rep += child.__repr__(level=level + 1)
+ return rep
+
class BOM(WebsiteGenerator):
website = frappe._dict(
# page_title_field = "item_name",
@@ -152,7 +224,7 @@
if not args:
args = frappe.form_dict.get('args')
- if isinstance(args, string_types):
+ if isinstance(args, str):
import json
args = json.loads(args)
@@ -600,6 +672,11 @@
if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1
+ def get_tree_representation(self) -> BOMTree:
+ """Get a complete tree representation preserving order of child items."""
+ return BOMTree(self.name)
+
+
def get_bom_item_rate(args, bom_doc):
if bom_doc.rm_cost_as_per == 'Valuation Rate':
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 1f443fb..57a5458 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -2,14 +2,13 @@
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
+from collections import deque
import unittest
import frappe
from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
-from six import string_types
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.tests.test_subcontracting import set_backflush_based_on
@@ -227,11 +226,88 @@
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEqual(bom_items, supplied_items)
+ def test_bom_tree_representation(self):
+ bom_tree = {
+ "Assembly": {
+ "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+ "SubAssembly2": {"ChildPart3": {}},
+ "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
+ "ChildPart5": {},
+ "ChildPart6": {},
+ "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
+ }
+ }
+ parent_bom = create_nested_bom(bom_tree, prefix="")
+ created_tree = parent_bom.get_tree_representation()
+
+ reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
+ created_order = created_tree.level_order_traversal()
+
+ self.assertEqual(len(reqd_order), len(created_order))
+
+ for reqd_item, created_item in zip(reqd_order, created_order):
+ self.assertEqual(reqd_item, created_item.item_code)
+
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
+
+
+
+def level_order_traversal(node):
+ traversal = []
+ q = deque()
+ q.append(node)
+
+ while q:
+ node = q.popleft()
+
+ for node_name, subtree in node.items():
+ traversal.append(node_name)
+ q.append(subtree)
+
+ return traversal
+
+def create_nested_bom(tree, prefix="_Test bom "):
+ """ Helper function to create a simple nested bom from tree describing item names. (along with required items)
+ """
+
+ def create_items(bom_tree):
+ for item_code, subtree in bom_tree.items():
+ bom_item_code = prefix + item_code
+ if not frappe.db.exists("Item", bom_item_code):
+ frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert()
+ create_items(subtree)
+ create_items(tree)
+
+ def dfs(tree, node):
+ """naive implementation for searching right subtree"""
+ for node_name, subtree in tree.items():
+ if node_name == node:
+ return subtree
+ else:
+ result = dfs(subtree, node)
+ if result is not None:
+ return result
+
+ order_of_creating_bom = reversed(level_order_traversal(tree))
+
+ for item in order_of_creating_bom:
+ child_items = dfs(tree, item)
+ if child_items:
+ bom_item_code = prefix + item
+ bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
+ for child_item in child_items.keys():
+ bom.append("items", {"item_code": prefix + child_item})
+ bom.insert()
+ bom.submit()
+
+ return bom # parent bom is last bom
+
+
def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None):
- if warehouse_list and isinstance(warehouse_list, string_types):
+ if warehouse_list and isinstance(warehouse_list, str):
warehouse_list = [warehouse_list]
if not warehouse_list:
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index cb1ee92..68de0b2 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -389,17 +389,12 @@
ste.submit()
stock_entries.append(ste)
- job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
+ job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc')
self.assertEqual(len(job_cards), len(bom.operations))
for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card)
- doc.append("time_logs", {
- "from_time": add_to_date(None, i),
- "hours": 1,
- "to_time": add_to_date(None, i + 1),
- "completed_qty": doc.for_quantity
- })
+ doc.time_logs[0].completed_qty = 1
doc.submit()
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index e343ed2..180815d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1,7 +1,6 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
import frappe
import json
import math
@@ -30,9 +29,6 @@
class SerialNoQtyError(frappe.ValidationError):
pass
-form_grid_templates = {
- "operations": "templates/form_grid/work_order_grid.html"
-}
class WorkOrder(Document):
def onload(self):
@@ -472,46 +468,47 @@
def set_work_order_operations(self):
"""Fetch operations from BOM and set in 'Work Order'"""
- self.set('operations', [])
+ def _get_operations(bom_no, qty=1):
+ return frappe.db.sql(
+ f"""select
+ operation, description, workstation, idx,
+ base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
+ "Pending" as status, parent as bom, batch_size, sequence_id
+ from
+ `tabBOM Operation`
+ where
+ parent = %s order by idx
+ """, bom_no, as_dict=1)
+
+
+ self.set('operations', [])
if not self.bom_no:
return
- if self.use_multi_level_bom:
- bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree()
+ operations = []
+ if not self.use_multi_level_bom:
+ bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
+ operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
else:
- bom_list = [self.bom_no]
+ bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
+ bom_traversal = list(reversed(bom_tree.level_order_traversal()))
+ bom_traversal.append(bom_tree) # add operation on top level item last
- operations = frappe.db.sql("""
- select
- operation, description, workstation, idx,
- base_hour_rate as hour_rate, time_in_mins,
- "Pending" as status, parent as bom, batch_size, sequence_id
- from
- `tabBOM Operation`
- where
- parent in (%s) order by idx
- """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1)
+ for d in bom_traversal:
+ if d.is_bom:
+ operations.extend(_get_operations(d.name, qty=d.exploded_qty))
+
+ for correct_index, operation in enumerate(operations, start=1):
+ operation.idx = correct_index
+
self.set('operations', operations)
-
- if self.use_multi_level_bom and self.get('operations') and self.get('items'):
- raw_material_operations = [d.operation for d in self.get('items')]
- operations = [d.operation for d in self.get('operations')]
-
- for operation in raw_material_operations:
- if operation not in operations:
- self.append('operations', {
- 'operation': operation
- })
-
self.calculate_time()
def calculate_time(self):
- bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
-
for d in self.get("operations"):
- d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size))
+ d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
self.calculate_operating_cost()
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index 6d8fb80..f7b8787 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -2,7 +2,6 @@
"actions": [],
"creation": "2014-10-16 14:35:41.950175",
"doctype": "DocType",
- "editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"details",
@@ -49,6 +48,7 @@
{
"fieldname": "bom",
"fieldtype": "Link",
+ "in_list_view": 1,
"label": "BOM",
"no_copy": 1,
"options": "BOM",
@@ -68,6 +68,7 @@
"fieldtype": "Column Break"
},
{
+ "columns": 1,
"description": "Operation completed for how many finished goods?",
"fieldname": "completed_qty",
"fieldtype": "Float",
@@ -77,6 +78,7 @@
"read_only": 1
},
{
+ "columns": 1,
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
@@ -119,6 +121,7 @@
"fieldtype": "Column Break"
},
{
+ "columns": 1,
"description": "in Minutes",
"fieldname": "time_in_mins",
"fieldtype": "Float",
@@ -205,7 +208,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-12 14:48:31.061286",
+ "modified": "2021-06-24 14:36:12.835543",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",
@@ -214,4 +217,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index e71d81f..5c7c0a3 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -459,6 +459,7 @@
where
t1.name = t2.employee
and t2.docstatus = 1
+ and t1.status != 'Inactive'
%s order by t2.from_date desc
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index ac4ed5e..a01db80 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -48,37 +48,54 @@
},
get_items: function(frm) {
- frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1,
+ let fields = [{
+ label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1,
"get_query": function() {
return {
"filters": {
"company": frm.doc.company,
}
- }
- }},
- function(data) {
- frappe.call({
- method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
- args: {
- warehouse: data.warehouse,
- posting_date: frm.doc.posting_date,
- posting_time: frm.doc.posting_time,
- company:frm.doc.company
- },
- callback: function(r) {
- var items = [];
- frm.clear_table("items");
- for(var i=0; i< r.message.length; i++) {
- var d = frm.add_child("items");
- $.extend(d, r.message[i]);
- if(!d.qty) d.qty = null;
- if(!d.valuation_rate) d.valuation_rate = null;
- }
- frm.refresh_field("items");
- }
- });
+ };
}
- , __("Get Items"), __("Update"));
+ }, {
+ label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item",
+ "get_query": function() {
+ return {
+ "filters": {
+ "disabled": 0,
+ }
+ };
+ }
+ }];
+
+ frappe.prompt(fields, function(data) {
+ frappe.call({
+ method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
+ args: {
+ warehouse: data.warehouse,
+ posting_date: frm.doc.posting_date,
+ posting_time: frm.doc.posting_time,
+ company: frm.doc.company,
+ item_code: data.item_code
+ },
+ callback: function(r) {
+ frm.clear_table("items");
+ for (var i=0; i<r.message.length; i++) {
+ var d = frm.add_child("items");
+ $.extend(d, r.message[i]);
+
+ if (!d.qty) {
+ d.qty = 0;
+ }
+
+ if (!d.valuation_rate) {
+ d.valuation_rate = 0;
+ }
+ }
+ frm.refresh_field("items");
+ }
+ });
+ }, __("Get Items"), __("Update"));
},
posting_date: function(frm) {
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 2b51c1a..e646600 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -481,45 +481,99 @@
self._cancel()
@frappe.whitelist()
-def get_items(warehouse, posting_date, posting_time, company):
+def get_items(warehouse, posting_date, posting_time, company, item_code=None):
+ items = [frappe._dict({
+ 'item_code': item_code,
+ 'warehouse': warehouse
+ })]
+
+ if not item_code:
+ items = get_items_for_stock_reco(warehouse, company)
+
+ res = []
+ itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
+
+ for d in items:
+ if d.item_code in itemwise_batch_data:
+ stock_bal = get_stock_balance(d.item_code, d.warehouse,
+ posting_date, posting_time, with_valuation_rate=True)
+
+ for row in itemwise_batch_data.get(d.item_code):
+ args = get_item_data(row, row.qty, stock_bal[1])
+ res.append(args)
+ else:
+ stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time,
+ with_valuation_rate=True , with_serial_no=cint(d.has_serial_no))
+
+ args = get_item_data(d, stock_bal[0], stock_bal[1],
+ stock_bal[2] if cint(d.has_serial_no) else '')
+
+ res.append(args)
+
+ return res
+
+def get_items_for_stock_reco(warehouse, company):
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
items = frappe.db.sql("""
- select i.name, i.item_name, bin.warehouse, i.has_serial_no
+ select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no
from tabBin bin, tabItem i
- where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1
- and i.has_variants = 0 and i.has_batch_no = 0
- and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse)
- """, (lft, rgt))
+ where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1
+ and i.has_variants = 0 and exists(
+ select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse
+ )
+ """, (lft, rgt), as_dict=1)
items += frappe.db.sql("""
- select i.name, i.item_name, id.default_warehouse, i.has_serial_no
+ select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no
from tabItem i, `tabItem Default` id
where i.name = id.parent
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse)
- and i.is_stock_item = 1 and i.has_batch_no = 0
- and i.has_variants = 0 and i.disabled = 0 and id.company=%s
+ and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s
group by i.name
- """, (lft, rgt, company))
+ """, (lft, rgt, company), as_dict=1)
- res = []
- for d in set(items):
- stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time,
- with_valuation_rate=True , with_serial_no=cint(d[3]))
+ return items
- if frappe.db.get_value("Item", d[0], "disabled") == 0:
- res.append({
- "item_code": d[0],
- "warehouse": d[2],
- "qty": stock_bal[0],
- "item_name": d[1],
- "valuation_rate": stock_bal[1],
- "current_qty": stock_bal[0],
- "current_valuation_rate": stock_bal[1],
- "current_serial_no": stock_bal[2] if cint(d[3]) else '',
- "serial_no": stock_bal[2] if cint(d[3]) else ''
- })
+def get_item_data(row, qty, valuation_rate, serial_no=None):
+ return {
+ 'item_code': row.item_code,
+ 'warehouse': row.warehouse,
+ 'qty': qty,
+ 'item_name': row.item_name,
+ 'valuation_rate': valuation_rate,
+ 'current_qty': qty,
+ 'current_valuation_rate': valuation_rate,
+ 'current_serial_no': serial_no,
+ 'serial_no': serial_no,
+ 'batch_no': row.get('batch_no')
+ }
- return res
+def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
+ from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute
+ itemwise_batch_data = {}
+
+ filters = frappe._dict({
+ 'warehouse': warehouse,
+ 'from_date': posting_date,
+ 'to_date': posting_date,
+ 'company': company
+ })
+
+ if item_code:
+ filters.item_code = item_code
+
+ columns, data = execute(filters)
+
+ for row in data:
+ itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({
+ 'item_code': row[0],
+ 'warehouse': warehouse,
+ 'qty': row[8],
+ 'item_name': row[1],
+ 'batch_no': row[4]
+ }))
+
+ return itemwise_batch_data
@frappe.whitelist()
def get_stock_balance_for(item_code, warehouse,
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index c64084f..ca174a3 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -438,6 +438,13 @@
@frappe.whitelist()
def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None):
out = {}
+
+ if item_tax_templates is None:
+ item_tax_templates = {}
+
+ if item_rates is None:
+ item_rates = {}
+
if isinstance(item_codes, (str,)):
item_codes = json.loads(item_codes)
@@ -453,7 +460,7 @@
out[item_code[1]] = {}
item = frappe.get_cached_doc("Item", item_code[0])
- args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]}
+ args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])}
if item_tax_templates:
args.update({"item_tax_template": item_tax_templates.get(item_code[1])})
diff --git a/erpnext/stock/report/incorrect_stock_value_report/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
new file mode 100644
index 0000000..ff42480
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Stock Value Report"] = {
+ "filters": [
+ {
+ "label": __("Company"),
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "label": __("Account"),
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "options": "Account",
+ get_query: function() {
+ var company = frappe.query_report.get_filter_value('company');
+ return {
+ filters: {
+ "account_type": "Stock",
+ "company": company
+ }
+ }
+ }
+ },
+ {
+ "label": __("From Date"),
+ "fieldname": "from_date",
+ "fieldtype": "Date"
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
new file mode 100644
index 0000000..a7e9f20
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-06-22 15:35:05.148177",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-06-22 15:35:05.148177",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Stock Value Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Stock Value Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Accounts Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
new file mode 100644
index 0000000..a724387
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from six import iteritems
+from frappe.utils import add_days, today, getdate
+from erpnext.stock.utils import get_stock_value_on
+from erpnext.accounts.utils import get_stock_and_account_balance
+
+def execute(filters=None):
+ if not erpnext.is_perpetual_inventory_enabled(filters.company):
+ frappe.throw(_("Perpetual inventory required for the company {0} to view this report.")
+ .format(filters.company))
+
+ data = get_data(filters)
+ columns = get_columns(filters)
+
+ return columns, data
+
+def get_unsync_date(filters):
+ date = filters.from_date
+ if not date:
+ date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""")
+ date = date[0][0]
+
+ if not date:
+ return
+
+ while getdate(date) < getdate(today()):
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date,
+ company=filters.company, account = filters.account)
+
+ if abs(account_bal - stock_bal) > 0.1:
+ return date
+
+ date = add_days(date, 1)
+
+def get_data(report_filters):
+ from_date = get_unsync_date(report_filters)
+
+ if not from_date:
+ return []
+
+ result = []
+
+ voucher_wise_dict = {}
+ data = frappe.db.sql('''
+ SELECT
+ name, posting_date, posting_time, voucher_type, voucher_no,
+ stock_value_difference, stock_value, warehouse, item_code
+ FROM
+ `tabStock Ledger Entry`
+ WHERE
+ posting_date
+ = %s and company = %s
+ and is_cancelled = 0
+ ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+ ''', (from_date, report_filters.company), as_dict=1)
+
+ for d in data:
+ voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)
+
+ closing_date = add_days(from_date, -1)
+ for key, stock_data in iteritems(voucher_wise_dict):
+ prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1])
+ for data in stock_data:
+ expected_stock_value = prev_stock_value + data.stock_value_difference
+ if abs(data.stock_value - expected_stock_value) > 0.1:
+ data.difference_value = abs(data.stock_value - expected_stock_value)
+ data.expected_stock_value = expected_stock_value
+ result.append(data)
+
+ return result
+
+def get_columns(filters):
+ return [
+ {
+ "label": _("Stock Ledger ID"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Stock Ledger Entry",
+ "width": "80"
+ },
+ {
+ "label": _("Posting Date"),
+ "fieldname": "posting_date",
+ "fieldtype": "Date"
+ },
+ {
+ "label": _("Posting Time"),
+ "fieldname": "posting_time",
+ "fieldtype": "Time"
+ },
+ {
+ "label": _("Voucher Type"),
+ "fieldname": "voucher_type",
+ "width": "110"
+ },
+ {
+ "label": _("Voucher No"),
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "options": "voucher_type",
+ "width": "110"
+ },
+ {
+ "label": _("Item Code"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": "110"
+ },
+ {
+ "label": _("Warehouse"),
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "options": "Warehouse",
+ "width": "110"
+ },
+ {
+ "label": _("Expected Stock Value"),
+ "fieldname": "expected_stock_value",
+ "fieldtype": "Currency",
+ "width": "150"
+ },
+ {
+ "label": _("Stock Value"),
+ "fieldname": "stock_value",
+ "fieldtype": "Currency",
+ "width": "120"
+ },
+ {
+ "label": _("Difference Value"),
+ "fieldname": "difference_value",
+ "fieldtype": "Currency",
+ "width": "150"
+ }
+ ]
\ No newline at end of file