From c1b0e65f9f9df5fdad4a813a788f4f458b2e8318 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 12 Nov 2020 09:54:44 +0530 Subject: [PATCH 01/46] feat: Putaway --- .../stock/doctype/putaway_rule/__init__.py | 0 .../doctype/putaway_rule/putaway_rule.js | 18 +++ .../doctype/putaway_rule/putaway_rule.json | 111 ++++++++++++++++++ .../doctype/putaway_rule/putaway_rule.py | 93 +++++++++++++++ .../doctype/putaway_rule/putaway_rule_list.js | 10 ++ .../doctype/putaway_rule/test_putaway_rule.py | 10 ++ 6 files changed, 242 insertions(+) create mode 100644 erpnext/stock/doctype/putaway_rule/__init__.py create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.js create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.json create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule.py create mode 100644 erpnext/stock/doctype/putaway_rule/putaway_rule_list.js create mode 100644 erpnext/stock/doctype/putaway_rule/test_putaway_rule.py diff --git a/erpnext/stock/doctype/putaway_rule/__init__.py b/erpnext/stock/doctype/putaway_rule/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js new file mode 100644 index 00000000000..ae08e82c28b --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -0,0 +1,18 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Putaway Rule', { + setup: function(frm) { + frm.set_query("warehouse", function() { + return { + "filters": { + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + } + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json new file mode 100644 index 00000000000..6a132c7e256 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "autoname": "format:{item_code}-{warehouse}", + "creation": "2020-11-09 11:39:46.489501", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "disable", + "item_code", + "item_name", + "warehouse", + "col_break_capacity", + "company", + "capacity", + "priority", + "stock_uom" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Item", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "col_break_capacity", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "capacity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Capacity", + "reqd": 1 + }, + { + "default": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:!doc.__islocal", + "fieldname": "disable", + "fieldtype": "Check", + "label": "Disable" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-10 17:06:27.151335", + "modified_by": "Administrator", + "module": "Stock", + "name": "Putaway Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py new file mode 100644 index 00000000000..9f028334311 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import copy +from frappe import _ +from frappe.utils import flt +from frappe.model.document import Document + +class PutawayRule(Document): + def validate(self): + self.validate_duplicate_rule() + self.validate_warehouse_and_company() + self.validate_capacity() + self.validate_priority() + + def validate_duplicate_rule(self): + existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) + if existing_rule and existing_rule != self.name: + frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.") + .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)), + title=_("Duplicate")) + + def validate_priority(self): + if self.priority < 1: + frappe.throw(_("Priority cannot be lesser than 1."), title=_("Invalid Priority")) + + def validate_warehouse_and_company(self): + company = frappe.db.get_value("Warehouse", self.warehouse, "company") + if company != self.company: + frappe.throw(_("Warehouse {0} does not belong to Company {1}.") + .format(frappe.bold(self.warehouse), frappe.bold(self.company)), + title=_("Invalid Warehouse")) + + def validate_capacity(self): + # check if capacity is lesser than current balance in warehouse + pass + +@frappe.whitelist() +def get_ordered_putaway_rules(item_code, company, qty): + """Returns an ordered list of putaway rules to apply on an item.""" + + # get enabled putaway rules for this item code in this company that have pending capacity + # order the rules by priority first + # if same priority, order by pending capacity (capacity - get how much stock is in the warehouse) + # return this list + # [{'name': "something", "free space": 20}, {'name': "something", "free space": 10}] + +@frappe.whitelist() +def apply_putaway_rule(items, company): + """ Applies Putaway Rule on line items. + + items: List of line items in a Purchase Receipt + company: Company in Purchase Receipt + """ + items_not_accomodated = [] + for item in items: + item_qty = item.qty + at_capacity, rules = get_ordered_putaway_rules(item.item_code, company, item_qty) + + if not rules: + if at_capacity: + items_not_accomodated.append([item.item_code, item_qty]) + continue + + item_row_updated = False + for rule in rules: + while item_qty > 0: + if not item_row_updated: + # update pre-existing row + item.qty = rule.qty + item.warehouse = rule.warehouse + item_row_updated = True + else: + # add rows for split quantity + added_row = copy.deepcopy(item) + added_row.qty = rule.qty + added_row.warehouse = rule.warehouse + items.append(added_row) + + item_qty -= flt(rule.qty) + + # if pending qty after applying rules, add row without warehouse + if item_qty > 0: + added_row = copy.deepcopy(item) + added_row.qty = item_qty + added_row.warehouse = '' + items.append(added_row) + items_not_accomodated.append([item.item_code, item_qty]) + + # need to check pricing rule, item tax impact \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js new file mode 100644 index 00000000000..bb1654cf241 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Putaway Rule'] = { + add_fields: ["disable"], + get_indicator: (doc) => { + if (doc.disable) { + return [__("Disabled"), "darkgrey", "disable,=,1"]; + } else { + return [__("Active"), "blue", "disable,=,0"]; + }; + } +}; diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py new file mode 100644 index 00000000000..e262217f848 --- /dev/null +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPutawayRule(unittest.TestCase): + pass From c7991f85612a0b3b608e942ec593d9c980b5c302 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 20 Nov 2020 11:53:20 +0530 Subject: [PATCH 02/46] feat: Putaway Rule --- .../doctype/purchase_order/purchase_order.py | 2 + .../doctype/putaway_rule/putaway_rule.json | 4 +- .../doctype/putaway_rule/putaway_rule.py | 104 ++++++++++++------ 3 files changed, 76 insertions(+), 34 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c7efb8a1a17..53326fd6b2b 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -349,7 +349,9 @@ def close_or_unclose_purchase_orders(names, status): frappe.local.message_log = [] def set_missing_values(source, target): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule target.ignore_pricing_rule = 1 + target.items = apply_putaway_rule(target.items, target.company) target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 6a132c7e256..0d90c47b506 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -55,7 +55,7 @@ "reqd": 1 }, { - "default": "item_code.stock_uom", + "fetch_from": "item_code.stock_uom", "fieldname": "stock_uom", "fieldtype": "Link", "label": "Stock UOM", @@ -86,7 +86,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-10 17:06:27.151335", + "modified": "2020-11-12 11:20:52.765163", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 9f028334311..1ac76b6c30f 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -5,9 +5,11 @@ from __future__ import unicode_literals import frappe import copy +from collections import defaultdict from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, nowdate from frappe.model.document import Document +from erpnext.stock.utils import get_stock_balance class PutawayRule(Document): def validate(self): @@ -35,59 +37,97 @@ class PutawayRule(Document): title=_("Invalid Warehouse")) def validate_capacity(self): - # check if capacity is lesser than current balance in warehouse - pass + balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) + if flt(self.capacity) < flt(balance_qty): + frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") + .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) @frappe.whitelist() -def get_ordered_putaway_rules(item_code, company, qty): +def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" + rules = frappe.get_all("Putaway Rule", fields=["name", "capacity", "priority", "warehouse"], + filters={"item_code": item_code, "company": company, "disable": 0}, + order_by="priority asc, capacity desc") - # get enabled putaway rules for this item code in this company that have pending capacity - # order the rules by priority first - # if same priority, order by pending capacity (capacity - get how much stock is in the warehouse) - # return this list - # [{'name': "something", "free space": 20}, {'name': "something", "free space": 10}] + if not rules: + return False, None + + for rule in rules: + balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) + free_space = flt(rule.capacity) - flt(balance_qty) + + if free_space > 0: + rule["free_space"] = free_space + else: + del rule + + if not rules: + # After iterating through rules, if no rules are left + # then there is not enough space left in any rule + True, None + + rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space'])) + return False, rules @frappe.whitelist() def apply_putaway_rule(items, company): """ Applies Putaway Rule on line items. - items: List of line items in a Purchase Receipt - company: Company in Purchase Receipt + items: List of Purchase Receipt Item objects + company: Company in the Purchase Receipt """ - items_not_accomodated = [] + items_not_accomodated, updated_table = [], [] + item_wise_rules = defaultdict(list) + for item in items: - item_qty = item.qty - at_capacity, rules = get_ordered_putaway_rules(item.item_code, company, item_qty) + item_qty, item_code = flt(item.qty), item.item_code + if not item_qty: continue + + at_capacity, rules = get_ordered_putaway_rules(item_code, company) if not rules: if at_capacity: - items_not_accomodated.append([item.item_code, item_qty]) + items_not_accomodated.append([item_code, item_qty]) continue - item_row_updated = False - for rule in rules: - while item_qty > 0: - if not item_row_updated: - # update pre-existing row - item.qty = rule.qty - item.warehouse = rule.warehouse - item_row_updated = True - else: - # add rows for split quantity - added_row = copy.deepcopy(item) - added_row.qty = rule.qty - added_row.warehouse = rule.warehouse - items.append(added_row) + # maintain item wise rules, to handle if item is entered twice + # in the table, due to different price, etc. + if not item_wise_rules[item_code]: + item_wise_rules[item_code] = rules - item_qty -= flt(rule.qty) + for rule in item_wise_rules[item_code]: + # it gets split if rule has lesser qty + # if rule_qty >= pending_qty => allocate pending_qty in row + # if rule_qty < pending_qty => allocate rule_qty in row and check for next rule + if item_qty > 0 and rule.free_space: + to_allocate = flt(rule.free_space) if item_qty >= flt(rule.free_space) else item_qty + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 + new_updated_table_row.qty = to_allocate + new_updated_table_row.warehouse = rule.warehouse + updated_table.append(new_updated_table_row) + + item_qty -= to_allocate + rule["free_space"] -= to_allocate + if item_qty == 0: break # if pending qty after applying rules, add row without warehouse if item_qty > 0: added_row = copy.deepcopy(item) + added_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 added_row.qty = item_qty added_row.warehouse = '' - items.append(added_row) + updated_table.append(added_row) items_not_accomodated.append([item.item_code, item_qty]) - # need to check pricing rule, item tax impact \ No newline at end of file + if items_not_accomodated: + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

" + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + + return updated_table if updated_table else items + # TODO: check pricing rule, item tax impact \ No newline at end of file From 9596276b95baeaa8fd24be6eb9df8372145aefb5 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 10:32:17 +0530 Subject: [PATCH 03/46] fix: Linter and Sider --- erpnext/buying/doctype/purchase_order/purchase_order.py | 4 ++-- erpnext/stock/doctype/putaway_rule/putaway_rule_list.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 53326fd6b2b..bb67eb92c07 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -123,8 +123,8 @@ class PurchaseOrder(BuyingController): if self.is_subcontracted == "Yes": for item in self.items: if not item.bom: - frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\ - .format(item.item_code, item.idx))) + frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}") + .format(item.item_code, item.idx)) def get_schedule_dates(self): for d in self.get('items'): diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js index bb1654cf241..e48c415f14c 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -5,6 +5,6 @@ frappe.listview_settings['Putaway Rule'] = { return [__("Disabled"), "darkgrey", "disable,=,1"]; } else { return [__("Active"), "blue", "disable,=,0"]; - }; + } } }; From 90598ea19cae9f271e7906ee0753cdddb70070c2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 17:35:13 +0530 Subject: [PATCH 04/46] chore: Multi UOM support for Putaway - Added UOM & conversion factor field in Putaway Rule - Items are split and assigned as per UOM - Handled Whole UOMs too --- .../doctype/putaway_rule/putaway_rule.js | 25 ++++++ .../doctype/putaway_rule/putaway_rule.json | 29 ++++++- .../doctype/putaway_rule/putaway_rule.py | 78 +++++++++++-------- 3 files changed, 96 insertions(+), 36 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js index ae08e82c28b..00a84b0e8d7 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -11,7 +11,32 @@ frappe.ui.form.on('Putaway Rule', { } }; }); + }, + + uom: function(frm) { + if(frm.doc.item_code && frm.doc.uom) { + return frm.call({ + method: "erpnext.stock.get_item_details.get_conversion_factor", + args: { + item_code: frm.doc.item_code, + uom: frm.doc.uom + }, + callback: function(r) { + if(!r.exc) { + let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); + frm.set_value('conversion_factor', r.message.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); + } + } + }); + } + }, + + capacity: function(frm) { + let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor); + frm.set_value('stock_capacity', stock_capacity); } + // refresh: function(frm) { // } diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 0d90c47b506..d5ae68faf3e 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -10,17 +10,19 @@ "item_code", "item_name", "warehouse", + "priority", "col_break_capacity", "company", "capacity", - "priority", - "stock_uom" + "uom", + "conversion_factor", + "stock_uom", + "stock_capacity" ], "fields": [ { "fieldname": "item_code", "fieldtype": "Link", - "in_list_view": 1, "in_standard_filter": 1, "label": "Item", "options": "Item", @@ -82,11 +84,30 @@ "fieldname": "disable", "fieldtype": "Check", "label": "Disable" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "stock_capacity", + "fieldtype": "Float", + "label": "Capacity in Stock UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-12 11:20:52.765163", + "modified": "2020-11-23 16:53:48.387054", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 1ac76b6c30f..53a947f4176 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -7,7 +7,7 @@ import frappe import copy from collections import defaultdict from frappe import _ -from frappe.utils import flt, nowdate +from frappe.utils import flt, floor, nowdate from frappe.model.document import Document from erpnext.stock.utils import get_stock_balance @@ -38,14 +38,14 @@ class PutawayRule(Document): def validate_capacity(self): balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) - if flt(self.capacity) < flt(balance_qty): + if flt(self.stock_capacity) < flt(balance_qty): frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", fields=["name", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -54,8 +54,7 @@ def get_ordered_putaway_rules(item_code, company): for rule in rules: balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) - free_space = flt(rule.capacity) - flt(balance_qty) - + free_space = flt(rule.stock_capacity) - flt(balance_qty) if free_space > 0: rule["free_space"] = free_space else: @@ -64,7 +63,7 @@ def get_ordered_putaway_rules(item_code, company): if not rules: # After iterating through rules, if no rules are left # then there is not enough space left in any rule - True, None + return True, None rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space'])) return False, rules @@ -79,15 +78,33 @@ def apply_putaway_rule(items, company): items_not_accomodated, updated_table = [], [] item_wise_rules = defaultdict(list) + def add_row(item, to_allocate, warehouse): + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.name = '' + new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 + new_updated_table_row.qty = to_allocate + new_updated_table_row.warehouse = warehouse + updated_table.append(new_updated_table_row) + for item in items: - item_qty, item_code = flt(item.qty), item.item_code - if not item_qty: continue + conversion = flt(item.conversion_factor) + uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + pending_qty, pending_stock_qty, item_code = flt(item.qty), flt(item.stock_qty), item.item_code + + if not pending_qty: + add_row(item, pending_qty, item.warehouse) + continue at_capacity, rules = get_ordered_putaway_rules(item_code, company) if not rules: if at_capacity: - items_not_accomodated.append([item_code, item_qty]) + # rules available, but no free space + add_row(item, pending_qty, '') + items_not_accomodated.append([item_code, pending_qty]) + else: + # no rules to apply + add_row(item, pending_qty, item.warehouse) continue # maintain item wise rules, to handle if item is entered twice @@ -96,31 +113,28 @@ def apply_putaway_rule(items, company): item_wise_rules[item_code] = rules for rule in item_wise_rules[item_code]: - # it gets split if rule has lesser qty - # if rule_qty >= pending_qty => allocate pending_qty in row - # if rule_qty < pending_qty => allocate rule_qty in row and check for next rule - if item_qty > 0 and rule.free_space: - to_allocate = flt(rule.free_space) if item_qty >= flt(rule.free_space) else item_qty - new_updated_table_row = copy.deepcopy(item) - new_updated_table_row.name = '' - new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 - new_updated_table_row.qty = to_allocate - new_updated_table_row.warehouse = rule.warehouse - updated_table.append(new_updated_table_row) + if pending_stock_qty > 0 and rule.free_space: + stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + qty_to_allocate = stock_qty_to_allocate / (conversion or 1) - item_qty -= to_allocate - rule["free_space"] -= to_allocate - if item_qty == 0: break + if uom_must_be_whole_number: + qty_to_allocate = floor(qty_to_allocate) + stock_qty_to_allocate = qty_to_allocate * conversion - # if pending qty after applying rules, add row without warehouse - if item_qty > 0: - added_row = copy.deepcopy(item) - added_row.name = '' - new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 - added_row.qty = item_qty - added_row.warehouse = '' - updated_table.append(added_row) - items_not_accomodated.append([item.item_code, item_qty]) + if not qty_to_allocate: break + + add_row(item, qty_to_allocate, rule.warehouse) + + pending_stock_qty -= stock_qty_to_allocate + pending_qty -= qty_to_allocate + rule["free_space"] -= stock_qty_to_allocate + + if not pending_stock_qty: break + + # if pending qty after applying all rules, add row without warehouse + if pending_stock_qty > 0: + add_row(item, pending_qty, '') + items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

  • " From 0cec1477f2044f8b09e7f147749fa7b47d9256e9 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 Nov 2020 19:19:35 +0530 Subject: [PATCH 05/46] chore: Format unassigned Items dialog and add freeze message --- .../doctype/purchase_order/purchase_order.js | 3 ++- .../doctype/putaway_rule/putaway_rule.py | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 47483c9d1c3..20faded9bbf 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -347,7 +347,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( make_purchase_receipt: function() { frappe.model.open_mapped_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", - frm: cur_frm + frm: cur_frm, + freeze_message: __("Creating Purchase Receipt ...") }) }, diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 53a947f4176..cc58def33a3 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -42,6 +42,9 @@ class PutawayRule(Document): frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) + if not self.capacity: + frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) + @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" @@ -137,10 +140,26 @@ def apply_putaway_rule(items, company): items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: - msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    • " - formatted_item_qty = [entry[0] + " : " + str(entry[1]) for entry in items_not_accomodated] - msg += "
    • ".join(formatted_item_qty) - msg += "
    " + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " + formatted_item_rows = "" + + for entry in items_not_accomodated: + item_link = frappe.utils.get_link_to_form("Item", entry[0]) + formatted_item_rows += """ + {0} + {1} + """.format(item_link, frappe.bold(entry[1])) + + msg += """ + + + + + + {2} +
    {0}{1}
    + """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) return updated_table if updated_table else items From ccbd432b56b952e7d40003c15202279379338336 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 12:47:13 +0530 Subject: [PATCH 06/46] chore: Added Tests - Fixed Sider Issues - Added perms to Putaway Rule - Added Unit Tests to check warehouse assignment --- .../doctype/putaway_rule/putaway_rule.js | 4 +- .../doctype/putaway_rule/putaway_rule.json | 27 +- .../doctype/putaway_rule/putaway_rule.py | 7 +- .../doctype/putaway_rule/test_putaway_rule.py | 257 +++++++++++++++++- 4 files changed, 287 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js index 00a84b0e8d7..e0569206ef9 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js @@ -14,7 +14,7 @@ frappe.ui.form.on('Putaway Rule', { }, uom: function(frm) { - if(frm.doc.item_code && frm.doc.uom) { + if (frm.doc.item_code && frm.doc.uom) { return frm.call({ method: "erpnext.stock.get_item_details.get_conversion_factor", args: { @@ -22,7 +22,7 @@ frappe.ui.form.on('Putaway Rule', { uom: frm.doc.uom }, callback: function(r) { - if(!r.exc) { + if (!r.exc) { let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor); frm.set_value('conversion_factor', r.message.conversion_factor); frm.set_value('stock_capacity', stock_capacity); diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index d5ae68faf3e..e5b6b2b98fc 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -107,7 +107,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-23 16:53:48.387054", + "modified": "2020-11-23 19:25:50.948068", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", @@ -121,7 +121,30 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Stock Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", "share": 1, "write": 1 } diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index cc58def33a3..73534aa14f1 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -17,6 +17,7 @@ class PutawayRule(Document): self.validate_warehouse_and_company() self.validate_capacity() self.validate_priority() + self.set_stock_capacity() def validate_duplicate_rule(self): existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) @@ -45,10 +46,13 @@ class PutawayRule(Document): if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) + def set_stock_capacity(self): + self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) + @frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "stock_capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -86,6 +90,7 @@ def apply_putaway_rule(items, company): new_updated_table_row.name = '' new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 new_updated_table_row.qty = to_allocate + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse updated_table.append(new_updated_table_row) diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index e262217f848..7b81784d5fa 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -2,9 +2,260 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - -# import frappe +import frappe import unittest +from frappe.utils import add_days, nowdate +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order class TestPutawayRule(unittest.TestCase): - pass + def setUp(self): + if not frappe.db.exists("Item", "_Rice"): + make_item("_Rice", { + 'is_stock_item': 1, + 'has_batch_no' : 1, + 'create_new_batch': 1, + 'stock_uom': 'Kg' + }) + + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}): + create_warehouse("Rack 1") + if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}): + create_warehouse("Rack 2") + + if not frappe.db.exists("UOM", "Bag"): + new_uom = frappe.new_doc("UOM") + new_uom.uom_name = "Bag" + new_uom.save() + + def test_putaway_rules_priority(self): + """Test if rule is applied by priority, irrespective of free space.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=300, + uom="Kg", priority=2) + + po = create_purchase_order(item_code="_Rice", qty=300) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_2) + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_same_priority(self): + """Test if rule with more free space is applied, + among two rules with same priority and capacity.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=500, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500, + uom="Kg") + + # out of 500 kg capacity, occupy 100 kg in warehouse_1 + stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=100, basic_rate=50) + + po = create_purchase_order(item_code="_Rice", qty=700) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 500) + # warehouse_2 has 500 kg free space, it is given priority + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 200) + # warehouse_1 has 400 kg free space, it is given less priority + self.assertEqual(pr.items[1].warehouse, warehouse_1) + + po.cancel() + stock_receipt.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_insufficient_capacity(self): + """Test if qty exceeding capacity, is handled.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=100, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=200, + uom="Kg") + + po = create_purchase_order(item_code="_Rice", qty=350) + self.assertEqual(len(po.items), 1) + + pr = make_purchase_receipt(po.name) + + self.assertEqual(len(pr.items), 3) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_1) + # extra qty has no warehouse assigned + self.assertEqual(pr.items[2].qty, 50) + self.assertEqual(pr.items[2].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom(self): + """Test rules applied on uom other than stock uom.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=3, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 3000) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=4, + uom="Bag") + self.assertEqual(rule_2.stock_capacity, 4000) + + stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=1000, basic_rate=50) + + po = create_purchase_order(item_code="_Rice", qty=6, do_not_save=True) + po.items[0].uom = "Bag" + po.save() + po.submit() + + self.assertEqual(po.items[0].stock_qty, 6000) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 4) + self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[1].qty, 2) + self.assertEqual(pr.items[1].warehouse, warehouse_1) + + po.cancel() + stock_receipt.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_multi_uom_whole_uom(self): + """Test if whole UOMs are handled.""" + item = frappe.get_doc("Item", "_Rice") + if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): + item.append("uoms", { + "uom": "Bag", + "conversion_factor": 1000 + }) + item.save() + + frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) + + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + # Putaway Rule in different UOM + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=1, + uom="Bag") + self.assertEqual(rule_1.stock_capacity, 1000) + # Putaway Rule in Stock UOM + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500) + self.assertEqual(rule_2.stock_capacity, 500) + # total capacity is 1500 Kg + + po = create_purchase_order(item_code="_Rice", qty=2, do_not_save=True) + # PO for 2 Bags (2000 Kg) + po.items[0].uom = "Bag" + po.save() + po.submit() + + self.assertEqual(po.items[0].stock_qty, 2000) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 1) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + # leftover space was for 500 kg (0.5 Bag) + # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned + self.assertEqual(pr.items[1].qty, 1) + self.assertEqual(pr.items[1].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rules_with_reoccurring_item(self): + """Test rules on same item entered multiple times.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=100, + uom="Kg", priority=2) + # total capacity is 300 Kg + + po = create_purchase_order(item_code="_Rice", qty=200, rate=100, do_not_save=True) + po.append("items", { + "item_code":"_Rice", + "warehouse": "_Test Warehouse - _TC", + "qty": 300, + "rate": 120, + "schedule_date": add_days(nowdate(), 1), + }) + po.save() + po.submit() + # PO for 500 Kg (two rows of same item, different rates) + self.assertEqual(len(po.items), 2) + + pr = make_purchase_receipt(po.name) + self.assertEqual(len(pr.items), 3) + self.assertEqual(pr.items[0].qty, 200) + self.assertEqual(pr.items[0].warehouse, warehouse_1) + # same rules applied to second item row + # with previous assignment considered + self.assertEqual(pr.items[1].qty, 100) + self.assertEqual(pr.items[1].warehouse, warehouse_2) + # unassigned 200 Kg + self.assertEqual(pr.items[2].qty, 200) + self.assertEqual(pr.items[2].warehouse, '') + + po.cancel() + rule_1.delete() + rule_2.delete() + +def create_putaway_rule(**args): + args = frappe._dict(args) + putaway = frappe.new_doc("Putaway Rule") + + putaway.disable = args.disable or 0 + putaway.company = args.company or "_Test Company" + putaway.item_code = args.item or args.item_code or "_Test Item" + putaway.warehouse = args.warehouse + putaway.priority = args.priority or 1 + putaway.capacity = args.capacity or 1 + putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") + putaway.uom = args.uom or putaway.stock_uom + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor'] + + if not args.do_not_save: + putaway.save() + + return putaway \ No newline at end of file From 68a49efc8098808386e234c380692791e926b2aa Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 17:38:34 +0530 Subject: [PATCH 07/46] chore: Added Putaway Rule to Desk Page and added Priority to List View --- erpnext/stock/desk_page/stock/stock.json | 4 ++-- erpnext/stock/doctype/putaway_rule/putaway_rule.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 390fcd91e3b..aa4fc28ec94 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Putaway Rule\",\n \"name\": \"Putaway Rule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-10-07 18:40:17.130207", + "modified": "2020-11-24 15:43:20.496057", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index e5b6b2b98fc..325e6f1355d 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -68,6 +68,7 @@ "default": "1", "fieldname": "priority", "fieldtype": "Int", + "in_list_view": 1, "label": "Priority" }, { @@ -107,7 +108,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-23 19:25:50.948068", + "modified": "2020-11-24 16:20:18.306671", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", From 2ed80656aa1be30fb735a85202ce71d62eba6763 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Nov 2020 21:06:43 +0530 Subject: [PATCH 08/46] chore: Code Cleanup - Validate capacity < stock level only on new rule - Mention stock uom while validating capacity in new rule - Separate function to format and display unassigned items - Format ORM args --- .../doctype/putaway_rule/putaway_rule.py | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 73534aa14f1..606e190458c 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -38,10 +38,13 @@ class PutawayRule(Document): title=_("Invalid Warehouse")) def validate_capacity(self): + stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) - if flt(self.stock_capacity) < flt(balance_qty): - frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.") - .format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity")) + + if flt(self.stock_capacity) < flt(balance_qty) and self.get('__islocal'): + frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") + .format(self.item_code, frappe.bold(balance_qty), stock_uom), + title=_("Insufficient Capacity")) if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) @@ -49,10 +52,10 @@ class PutawayRule(Document): def set_stock_capacity(self): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) -@frappe.whitelist() def get_ordered_putaway_rules(item_code, company): """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], + rules = frappe.get_all("Putaway Rule", + fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters={"item_code": item_code, "company": company, "disable": 0}, order_by="priority asc, capacity desc") @@ -145,27 +148,29 @@ def apply_putaway_rule(items, company): items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: - msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " - formatted_item_rows = "" - - for entry in items_not_accomodated: - item_link = frappe.utils.get_link_to_form("Item", entry[0]) - formatted_item_rows += """ - {0} - {1} - """.format(item_link, frappe.bold(entry[1])) - - msg += """ - - - - - - {2} -
    {0}{1}
    - """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) - - frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + format_unassigned_items_error(items_not_accomodated) return updated_table if updated_table else items - # TODO: check pricing rule, item tax impact \ No newline at end of file + +def format_unassigned_items_error(items_not_accomodated): + msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " + formatted_item_rows = "" + + for entry in items_not_accomodated: + item_link = frappe.utils.get_link_to_form("Item", entry[0]) + formatted_item_rows += """ + {0} + {1} + """.format(item_link, frappe.bold(entry[1])) + + msg += """ + + + + + + {2} +
    {0}{1}
    + """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) \ No newline at end of file From 1087d97c03a0ea9973ffc0d70472c4a3fcac8654 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 26 Nov 2020 10:45:44 +0530 Subject: [PATCH 09/46] feat: Warehouse Capacity Summary - Added Page Warehouse Capacity Summary - Added Page to Desk and Putaway List View - Reused Item Dashboard/Stock Balance page render code - Added naming series to Putaway Rule --- erpnext/public/build.json | 4 +- erpnext/stock/dashboard/item_dashboard.js | 75 ++++++++--- .../dashboard/warehouse_capacity_dashboard.py | 69 ++++++++++ erpnext/stock/desk_page/stock/stock.json | 4 +- erpnext/stock/doctype/item/item.js | 5 +- .../purchase_receipt/purchase_receipt.json | 9 +- .../doctype/putaway_rule/putaway_rule.json | 8 +- .../doctype/putaway_rule/putaway_rule_list.js | 10 +- .../stock/page/stock_balance/stock_balance.js | 3 + .../warehouse_capacity_summary/__init__.py | 0 .../warehouse_capacity_summary.html | 40 ++++++ .../warehouse_capacity_summary.js | 120 ++++++++++++++++++ .../warehouse_capacity_summary.json | 26 ++++ .../warehouse_capacity_summary_header.html | 19 +++ 14 files changed, 367 insertions(+), 25 deletions(-) create mode 100644 erpnext/stock/dashboard/warehouse_capacity_dashboard.py create mode 100644 erpnext/stock/page/warehouse_capacity_summary/__init__.py create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json create mode 100644 erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 2695502269a..8b18a1fcfb5 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -54,6 +54,8 @@ "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard_list.html", - "stock/dashboard/item_dashboard.js" + "stock/dashboard/item_dashboard.js", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html", + "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html" ] } diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 9bd03d45cbb..abc286fcc6e 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -24,6 +24,16 @@ erpnext.stock.ItemDashboard = Class.extend({ handle_move_add($(this), "Add") }); + this.content.on('click', '.btn-edit', function() { + let item = unescape($(this).attr('data-item')); + let warehouse = unescape($(this).attr('data-warehouse')); + let company = unescape($(this).attr('data-company')); + frappe.db.get_value('Putaway Rule', + {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => { + frappe.set_route("Form", "Putaway Rule", r.name); + }); + }); + function handle_move_add(element, action) { let item = unescape(element.attr('data-item')); let warehouse = unescape(element.attr('data-warehouse')); @@ -59,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({ // more this.content.find('.btn-more').on('click', function() { - me.start += 20; + me.start += this.page_length; me.refresh(); }); @@ -69,33 +79,41 @@ erpnext.stock.ItemDashboard = Class.extend({ this.before_refresh(); } + let args = { + item_code: this.item_code, + warehouse: this.warehouse, + parent_warehouse: this.parent_warehouse, + item_group: this.item_group, + company: this.company, + start: this.start, + sort_by: this.sort_by, + sort_order: this.sort_order + } + var me = this; frappe.call({ - method: 'erpnext.stock.dashboard.item_dashboard.get_data', - args: { - item_code: this.item_code, - warehouse: this.warehouse, - item_group: this.item_group, - start: this.start, - sort_by: this.sort_by, - sort_order: this.sort_order, - }, + method: this.method, + args: args, callback: function(r) { me.render(r.message); } }); }, render: function(data) { - if(this.start===0) { + if (this.start===0) { this.max_count = 0; this.result.empty(); } + if (this.page_name === "warehouse-capacity-summary") { + var context = this.get_capacity_dashboard_data(data); + } else { + var context = this.get_item_dashboard_data(data, this.max_count, true); + } - var context = this.get_item_dashboard_data(data, this.max_count, true); this.max_count = this.max_count; // show more button - if(data && data.length===21) { + if (data && data.length===(this.page_length + 1)) { this.content.find('.more').removeClass('hidden'); // remove the last element @@ -106,12 +124,17 @@ erpnext.stock.ItemDashboard = Class.extend({ // If not any stock in any warehouses provide a message to end user if (context.data.length > 0) { - $(frappe.render_template('item_dashboard_list', context)).appendTo(this.result); + this.content.find('.result').css('text-align', 'unset'); + $(frappe.render_template(this.template, context)).appendTo(this.result); } else { - var message = __("Currently no stock available in any warehouse"); - $(` ${message} `).appendTo(this.result); + var message = __("No Stock Available Currently"); + this.content.find('.result').css('text-align', 'center'); + + $(`
    + ${message}
    `).appendTo(this.result); } }, + get_item_dashboard_data: function(data, max_count, show_item) { if(!max_count) max_count = 0; if(!data) data = []; @@ -128,7 +151,7 @@ erpnext.stock.ItemDashboard = Class.extend({ d.total_reserved, max_count); }); - var can_write = 0; + let can_write = 0; if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){ can_write = 1; } @@ -139,6 +162,24 @@ erpnext.stock.ItemDashboard = Class.extend({ can_write:can_write, show_item: show_item || false } + }, + + get_capacity_dashboard_data: function(data) { + if(!data) data = []; + + data.forEach(function(d) { + d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef"; + }); + + let can_write = 0; + if(frappe.boot.user.can_write.indexOf("Putaway Rule")>=0){ + can_write = 1; + } + + return { + data: data, + can_write: can_write, + } } }) diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py new file mode 100644 index 00000000000..ab573e566ac --- /dev/null +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals + +import frappe +from frappe.model.db_query import DatabaseQuery +from frappe.utils import nowdate +from frappe.utils import flt +from erpnext.stock.utils import get_stock_balance + +@frappe.whitelist() +def get_data(item_code=None, warehouse=None, parent_warehouse=None, + company=None, start=0, sort_by="stock_capacity", sort_order="desc"): + """Return data to render the warehouse capacity dashboard.""" + filters = get_filters(item_code, warehouse, parent_warehouse, company) + + no_permission, filters = get_warehouse_filter_based_on_permissions(filters) + if no_permission: + return [] + + capacity_data = get_warehouse_capacity_data(filters, start) + + asc_desc = -1 if sort_order == "desc" else 1 + capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc)) + + return capacity_data + +def get_filters(item_code=None, warehouse=None, parent_warehouse=None, + company=None): + filters = [['disable', '=', 0]] + if item_code: + filters.append(['item_code', '=', item_code]) + if warehouse: + filters.append(['warehouse', '=', warehouse]) + if company: + filters.append(['company', '=', company]) + if parent_warehouse: + lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"]) + warehouses = frappe.db.sql_list(""" + select name from `tabWarehouse` + where lft >=%s and rgt<=%s + """, (lft, rgt)) + filters.append(['warehouse', 'in', warehouses]) + return filters + +def get_warehouse_filter_based_on_permissions(filters): + try: + # check if user has any restrictions based on user permissions on warehouse + if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): + filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + return False, filters + except frappe.PermissionError: + # user does not have access on warehouse + return True, [] + +def get_warehouse_capacity_data(filters, start): + capacity_data = frappe.db.get_all('Putaway Rule', + fields=['item_code', 'warehouse','stock_capacity', 'company'], + filters=filters, + limit_start=start, + limit_page_length='11' + ) + + for entry in capacity_data: + balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 + entry.update({ + 'actual_qty': balance_qty, + 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) + }) + + return capacity_data \ No newline at end of file diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index aa4fc28ec94..0038c0a9710 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Stock Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Putaway Rule\"\n ],\n \"label\": \"Warehouse Capacity Summary\",\n \"name\": \"warehouse-capacity-summary\",\n \"type\": \"page\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-11-24 15:43:20.496057", + "modified": "2020-11-26 10:43:48.286663", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index faeeb578fe3..ec32b0f0440 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -384,7 +384,10 @@ $.extend(erpnext.item, { ' + __("Stock Levels") + ''); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, - item_code: frm.doc.name + item_code: frm.doc.name, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }); erpnext.item.item_dashboard.refresh(); }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 13c8ceb7596..7213eb86169 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -21,6 +21,7 @@ "posting_date", "posting_time", "set_posting_time", + "apply_putaway_rule", "is_return", "return_against", "section_addresses", @@ -1104,13 +1105,19 @@ "fieldtype": "Small Text", "label": "Billing Address", "read_only": 1 + }, + { + "default": "0", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-10-30 14:00:08.347534", + "modified": "2020-11-25 18:31:32.234503", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json index 325e6f1355d..a003f4986f9 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.json +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "format:{item_code}-{warehouse}", + "autoname": "PUT-.####", "creation": "2020-11-09 11:39:46.489501", "doctype": "DocType", "editable_grid": 1, @@ -90,12 +90,14 @@ "fieldname": "uom", "fieldtype": "Link", "label": "UOM", + "no_copy": 1, "options": "UOM" }, { "fieldname": "stock_capacity", "fieldtype": "Float", "label": "Capacity in Stock UOM", + "no_copy": 1, "read_only": 1 }, { @@ -103,12 +105,13 @@ "fieldname": "conversion_factor", "fieldtype": "Float", "label": "Conversion Factor", + "no_copy": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-24 16:20:18.306671", + "modified": "2020-11-25 20:39:19.973437", "modified_by": "Administrator", "module": "Stock", "name": "Putaway Rule", @@ -152,5 +155,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "item_code", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js index e48c415f14c..725e91ee8d9 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js @@ -6,5 +6,13 @@ frappe.listview_settings['Putaway Rule'] = { } else { return [__("Active"), "blue", "disable,=,0"]; } - } + }, + + reports: [ + { + name: 'Warehouse Capacity Summary', + report_type: 'Page', + route: 'warehouse-capacity-summary' + } + ] }; diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js index da21c6bc64f..bddffd465e6 100644 --- a/erpnext/stock/page/stock_balance/stock_balance.js +++ b/erpnext/stock/page/stock_balance/stock_balance.js @@ -65,6 +65,9 @@ frappe.pages['stock-balance'].on_page_load = function(wrapper) { frappe.require('assets/js/item-dashboard.min.js', function() { page.item_dashboard = new erpnext.stock.ItemDashboard({ parent: page.main, + page_length: 20, + method: 'erpnext.stock.dashboard.item_dashboard.get_data', + template: 'item_dashboard_list' }) page.item_dashboard.before_refresh = function() { diff --git a/erpnext/stock/page/warehouse_capacity_summary/__init__.py b/erpnext/stock/page/warehouse_capacity_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html new file mode 100644 index 00000000000..90112c78a83 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html @@ -0,0 +1,40 @@ +{% for d in data %} +
    +
    + + +
    + {{ d.stock_capacity }} +
    +
    + {{ d.actual_qty }} +
    +
    +
    +
    +
    +
    +
    +
    + {{ d.percent_occupied }}% +
    + {% if can_write %} +
    +
    + {% endif %} +
    +
    +{% endfor %} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js new file mode 100644 index 00000000000..c3b3b5d8ec4 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -0,0 +1,120 @@ +frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Warehouse Capacity Summary', + single_column: true + }); + page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync'); + page.start = 0; + + page.company_field = page.add_field({ + fieldname: 'company', + label: __('Company'), + fieldtype:'Link', + options:'Company', + reqd: 1, + default: frappe.defaults.get_default("company"), + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.warehouse_field = page.add_field({ + fieldname: 'warehouse', + label: __('Warehouse'), + fieldtype:'Link', + options:'Warehouse', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.item_field = page.add_field({ + fieldname: 'item_code', + label: __('Item'), + fieldtype:'Link', + options:'Item', + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.parent_warehouse_field = page.add_field({ + fieldname: 'parent_warehouse', + label: __('Parent Warehouse'), + fieldtype:'Link', + options:'Warehouse', + get_query: function() { + return { + filters: { + "is_group": 1 + } + }; + }, + change: function() { + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + page.sort_selector = new frappe.ui.SortSelector({ + parent: page.wrapper.find('.page-form'), + args: { + sort_by: 'stock_capacity', + sort_order: 'desc', + options: [ + {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')}, + {fieldname: 'percent_occupied', label:__('% Occupied')}, + {fieldname: 'actual_qty', label:__('Balance Qty (Stock ')} + ] + }, + change: function(sort_by, sort_order) { + page.capacity_dashboard.sort_by = sort_by; + page.capacity_dashboard.sort_order = sort_order; + page.capacity_dashboard.start = 0; + page.capacity_dashboard.refresh(); + } + }); + + frappe.require('assets/js/item-dashboard.min.js', function() { + $(frappe.render_template('warehouse_capacity_summary_header')).appendTo(page.main); + + page.capacity_dashboard = new erpnext.stock.ItemDashboard({ + page_name: "warehouse-capacity-summary", + page_length: 10, + parent: page.main, + sort_by: 'stock_capacity', + sort_order: 'desc', + method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data', + template: 'warehouse_capacity_summary' + }) + + page.capacity_dashboard.before_refresh = function() { + this.item_code = page.item_field.get_value(); + this.warehouse = page.warehouse_field.get_value(); + this.parent_warehouse = page.parent_warehouse_field.get_value(); + this.company = page.company_field.get_value(); + } + + page.capacity_dashboard.refresh(); + + let setup_click = function(doctype) { + page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { + var name = $(this).attr('data-name'); + var field = page[doctype.toLowerCase() + '_field']; + if(field.get_value()===name) { + frappe.set_route('Form', doctype, name) + } else { + field.set_input(name); + page.capacity_dashboard.refresh(); + } + }); + } + + setup_click('Item'); + setup_click('Warehouse'); + }); +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json new file mode 100644 index 00000000000..a6e5b45332b --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2020-11-25 12:07:54.056208", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2020-11-25 11:07:54.056208", + "modified_by": "Administrator", + "module": "Stock", + "name": "warehouse-capacity-summary", + "owner": "Administrator", + "page_name": "Warehouse Capacity Summary", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Warehouse Capacity Summary" +} \ No newline at end of file diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html new file mode 100644 index 00000000000..acaf180a903 --- /dev/null +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html @@ -0,0 +1,19 @@ +
    +
    +
    + Warehouse +
    +
    + Item +
    +
    + Stock Capacity +
    +
    + Balance Stock Qty +
    +
    + % Occupied +
    +
    +
    \ No newline at end of file From fac4035f23b9b655da466ba6a436533b821b61d8 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Dec 2020 21:35:49 +0530 Subject: [PATCH 10/46] feat: Apply Putaway Rules within transaction itself - Added checkbox 'Apply Putaway Rule' in PR and SE - Added link to rule in child tables - Rule is applied on Save - Validation for over receipt - Apply Rule on Stock Entry as well for Material Transfer and Receipt --- .../doctype/purchase_order/purchase_order.py | 2 - erpnext/controllers/stock_controller.py | 38 +++++ erpnext/public/js/controllers/buying.js | 23 +++ .../purchase_receipt/purchase_receipt.js | 4 + .../purchase_receipt/purchase_receipt.py | 7 + .../purchase_receipt_item.json | 12 +- .../doctype/putaway_rule/putaway_rule.py | 153 ++++++++++++------ .../stock/doctype/stock_entry/stock_entry.js | 4 + .../doctype/stock_entry/stock_entry.json | 10 +- .../stock/doctype/stock_entry/stock_entry.py | 8 + .../stock_entry_detail.json | 14 +- 11 files changed, 217 insertions(+), 58 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index bb67eb92c07..d32e98e8d94 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -349,9 +349,7 @@ def close_or_unclose_purchase_orders(names, status): frappe.local.message_log = [] def set_missing_values(source, target): - from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule target.ignore_pricing_rule = 1 - target.items = apply_putaway_rule(target.items, target.company) target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2d2fff8fd54..c7fadde16eb 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,6 +6,7 @@ import frappe, erpnext from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe import _ import frappe.defaults +from collections import defaultdict from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.controllers.accounts_controller import AccountsController @@ -23,6 +24,7 @@ class StockController(AccountsController): self.validate_inspection() self.validate_serialized_batch() self.validate_customer_provided_item() + self.validate_putaway_capacity() def make_gl_entries(self, gl_entries=None): if self.docstatus == 2: @@ -399,6 +401,42 @@ class StockController(AccountsController): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 + def validate_putaway_capacity(self): + # if over receipt is attempted while 'apply putaway rule' is disabled + # and if rule was applied on the transaction, validate it. + from erpnext.stock.doctype.putaway_rule.putaway_rule import get_putaway_capacity + valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry") + rule_applied = any(item.get("putaway_rule") for item in self.get("items")) + + if valid_doctype and rule_applied and not self.apply_putaway_rule: + rule_map = defaultdict(dict) + for item in self.get("items"): + if item.get("putaway_rule"): + rule = item.get("putaway_rule") + disabled = frappe.db.get_value("Putaway Rule", rule, "disable") + if disabled: return # dont validate for disabled rule + stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) + warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" + if not rule_map[rule]: + rule_map[rule]["warehouse"] = item.get(warehouse_field) + rule_map[rule]["item"] = item.get("item_code") + rule_map[rule]["qty_put"] = 0 + rule_map[rule]["capacity"] = get_putaway_capacity(rule) + rule_map[rule]["qty_put"] += flt(stock_qty) + + for rule, values in rule_map.items(): + if flt(values["qty_put"]) > flt(values["capacity"]): + message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ + .format( + frappe.bold(values["qty_put"]), frappe.bold(values["item"]), + frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) + ) + message += "

    " + rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) + message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) + frappe.throw(msg=message, title=_("Over Receipt")) + return rule_map + def compare_existing_and_expected_gle(existing_gle, expected_gle): matched = True for entry in expected_gle: diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 58ac38f0a85..ac48d451b9a 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -524,3 +524,26 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { dialog.show(); } + +erpnext.apply_putaway_rule = (frm) => { + if (!frm.doc.company) { + frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}) + } + if (!frm.doc.items.length) return; + + frappe.call({ + method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule", + args: { + items: frm.doc.items, + company: frm.doc.company + }, + callback: (result) => { + if(!result.exc) { + if(result.message) { + frm.doc.items = result.message; + frm.get_field("items").refresh(); + } + } + } + }); +} \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index bc1d81d3565..45eb646d4b9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -213,6 +213,10 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend }); }, + apply_putaway_rule: function() { + // if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm); + } + }); // for backward compatibility: combine new and previous states diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 2cc4679c8c6..511bae6f58c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -70,6 +70,12 @@ class PurchaseReceipt(BuyingController): where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" }) + def before_save(self): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule + + if self.get("items") and self.apply_putaway_rule: + self.items = apply_putaway_rule(self.doctype, self.get("items"), self.company) + def validate(self): self.validate_posting_time() super(PurchaseReceipt, self).validate() @@ -90,6 +96,7 @@ class PurchaseReceipt(BuyingController): if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + def validate_cwip_accounts(self): for item in self.get('items'): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index c1e1f901ba6..fcbf6ccf6ea 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -72,6 +72,7 @@ "purchase_order_item", "material_request_item", "purchase_receipt_item", + "putaway_rule", "section_break_45", "allow_zero_valuation_rate", "bom", @@ -834,12 +835,21 @@ "collapsible": 1, "fieldname": "image_column", "fieldtype": "Column Break" + }, + { + "fieldname": "putaway_rule", + "fieldtype": "Link", + "label": "Putaway Rule", + "no_copy": 1, + "options": "Putaway Rule", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-28 19:01:21.154963", + "modified": "2020-11-26 12:16:14.897160", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 606e190458c..8838bb75f1d 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -5,11 +5,14 @@ from __future__ import unicode_literals import frappe import copy +import json from collections import defaultdict +from six import string_types from frappe import _ -from frappe.utils import flt, floor, nowdate +from frappe.utils import flt, floor, nowdate, cint from frappe.model.document import Document from erpnext.stock.utils import get_stock_balance +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos class PutawayRule(Document): def validate(self): @@ -52,70 +55,50 @@ class PutawayRule(Document): def set_stock_capacity(self): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) -def get_ordered_putaway_rules(item_code, company): - """Returns an ordered list of putaway rules to apply on an item.""" - rules = frappe.get_all("Putaway Rule", - fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], - filters={"item_code": item_code, "company": company, "disable": 0}, - order_by="priority asc, capacity desc") - - if not rules: - return False, None - - for rule in rules: - balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) - free_space = flt(rule.stock_capacity) - flt(balance_qty) - if free_space > 0: - rule["free_space"] = free_space - else: - del rule - - if not rules: - # After iterating through rules, if no rules are left - # then there is not enough space left in any rule - return True, None - - rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space'])) - return False, rules +@frappe.whitelist() +def get_putaway_capacity(rule): + stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, + ["stock_capacity", "item_code", "warehouse"]) + balance_qty = get_stock_balance(item_code, warehouse, nowdate()) + free_space = flt(stock_capacity) - flt(balance_qty) + return free_space if free_space > 0 else 0 @frappe.whitelist() -def apply_putaway_rule(items, company): +def apply_putaway_rule(doctype, items, company): """ Applies Putaway Rule on line items. items: List of Purchase Receipt Item objects company: Company in the Purchase Receipt """ + if isinstance(items, string_types): + items = json.loads(items) + items_not_accomodated, updated_table = [], [] item_wise_rules = defaultdict(list) - def add_row(item, to_allocate, warehouse): - new_updated_table_row = copy.deepcopy(item) - new_updated_table_row.name = '' - new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1 - new_updated_table_row.qty = to_allocate - new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) - new_updated_table_row.warehouse = warehouse - updated_table.append(new_updated_table_row) - for item in items: - conversion = flt(item.conversion_factor) - uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') - pending_qty, pending_stock_qty, item_code = flt(item.qty), flt(item.stock_qty), item.item_code + if isinstance(item, dict): + item = frappe._dict(item) - if not pending_qty: - add_row(item, pending_qty, item.warehouse) + source_warehouse = item.get("s_warehouse") + serial_nos = get_serial_nos(item.get("serial_no")) + conversion = flt(item.conversion_factor) or 1 + pending_qty, item_code = flt(item.qty), item.item_code + pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) + if not pending_qty or not item_code: + updated_table = add_row(item, pending_qty, item.warehouse, updated_table) continue - at_capacity, rules = get_ordered_putaway_rules(item_code, company) + uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + + at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: + warehouse = item.warehouse if at_capacity: - # rules available, but no free space - add_row(item, pending_qty, '') + warehouse = '' # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) - else: - # no rules to apply - add_row(item, pending_qty, item.warehouse) + updated_table = add_row(item, pending_qty, warehouse, updated_table) continue # maintain item wise rules, to handle if item is entered twice @@ -126,7 +109,7 @@ def apply_putaway_rule(items, company): for rule in item_wise_rules[item_code]: if pending_stock_qty > 0 and rule.free_space: stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty - qty_to_allocate = stock_qty_to_allocate / (conversion or 1) + qty_to_allocate = stock_qty_to_allocate / (conversion) if uom_must_be_whole_number: qty_to_allocate = floor(qty_to_allocate) @@ -134,7 +117,8 @@ def apply_putaway_rule(items, company): if not qty_to_allocate: break - add_row(item, qty_to_allocate, rule.warehouse) + updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, + rule.name, serial_nos=serial_nos) pending_stock_qty -= stock_qty_to_allocate pending_qty -= qty_to_allocate @@ -144,15 +128,71 @@ def apply_putaway_rule(items, company): # if pending qty after applying all rules, add row without warehouse if pending_stock_qty > 0: - add_row(item, pending_qty, '') + # updated_table = add_row(item, pending_qty, '', updated_table, serial_nos=serial_nos) items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: - format_unassigned_items_error(items_not_accomodated) + show_unassigned_items_message(items_not_accomodated) return updated_table if updated_table else items -def format_unassigned_items_error(items_not_accomodated): +def get_ordered_putaway_rules(item_code, company, source_warehouse=None): + """Returns an ordered list of putaway rules to apply on an item.""" + filters = { + "item_code": item_code, + "company": company, + "disable": 0 + } + if source_warehouse: + filters.update({"warehouse": ["!=", source_warehouse]}) + + rules = frappe.get_all("Putaway Rule", + fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], + filters=filters, + order_by="priority asc, capacity desc") + + if not rules: + return False, None + + vacant_rules = [] + for rule in rules: + balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate()) + free_space = flt(rule.stock_capacity) - flt(balance_qty) + if free_space > 0: + rule["free_space"] = free_space + vacant_rules.append(rule) + + if not vacant_rules: + # After iterating through rules, if no rules are left + # then there is not enough space left in any rule + return True, None + + vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space'])) + + return False, vacant_rules + +def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): + new_updated_table_row = copy.deepcopy(item) + new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 + new_updated_table_row.name = "New " + str(item.doctype) + " " + str(new_updated_table_row.idx) + new_updated_table_row.qty = to_allocate + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + if item.doctype == "Stock Entry Detail": + new_updated_table_row.t_warehouse = warehouse + else: + new_updated_table_row.warehouse = warehouse + new_updated_table_row.rejected_qty = 0 + new_updated_table_row.received_qty = to_allocate + + if rule: + new_updated_table_row.putaway_rule = rule + if serial_nos: + new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate) + + updated_table.append(new_updated_table_row) + return updated_table + +def show_unassigned_items_message(items_not_accomodated): msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " formatted_item_rows = "" @@ -173,4 +213,11 @@ def format_unassigned_items_error(items_not_accomodated): """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) - frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) \ No newline at end of file + frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + +def get_serial_nos_to_allocate(serial_nos, to_allocate): + if serial_nos: + allocated_serial_nos = serial_nos[0: cint(to_allocate)] + serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list + return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" + else: return "" \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 91217582ca4..2be70f37f27 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -571,6 +571,10 @@ frappe.ui.form.on('Stock Entry', { } }); } + }, + + apply_putaway_rule: function(frm) { + // if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm); } }) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 61e0df67238..cd01fa72779 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -27,6 +27,7 @@ "set_posting_time", "inspection_required", "from_bom", + "apply_putaway_rule", "sb1", "bom_no", "fg_completed_qty", @@ -640,13 +641,20 @@ "fieldtype": "Check", "label": "Add to Transit", "no_copy": 1 + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], doc.purpose)", + "fieldname": "apply_putaway_rule", + "fieldtype": "Check", + "label": "Apply Putaway Rule" } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-08-11 19:10:07.954981", + "modified": "2020-12-07 14:58:13.267321", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e3159b95c30..aa3425cbd8e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -42,6 +42,13 @@ class StockEntry(StockController): for item in self.get("items"): item.update(get_bin_details(item.item_code, item.s_warehouse)) + def before_save(self): + from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule + apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) + + if self.get("items") and apply_rule: + self.items = apply_putaway_rule(self.doctype, self.get("items"), self.company) + def validate(self): self.pro_doc = frappe._dict() if self.work_order: @@ -79,6 +86,7 @@ class StockEntry(StockController): self.validate_serialized_batch() self.set_actual_qty() self.calculate_rate_and_amount(update_finished_item_rate=False) + self.validate_putaway_capacity() def on_submit(self): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 79e8f9af8fc..4075e28b7be 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-03-29 18:22:12", "doctype": "DocType", @@ -61,6 +62,7 @@ "against_stock_entry", "ste_detail", "po_detail", + "putaway_rule", "column_break_51", "transferred_qty", "reference_purchase_receipt", @@ -498,13 +500,23 @@ "fieldname": "set_basic_rate_manually", "fieldtype": "Check", "label": "Set Basic Rate Manually" + }, + { + "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], parent.purpose)", + "fieldname": "putaway_rule", + "fieldtype": "Link", + "label": "Putaway Rule", + "no_copy": 1, + "options": "Putaway Rule", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-23 17:55:03.384138", + "modified": "2020-12-07 15:00:44.489442", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 0f3cfc502bc81a475cf5f6b8beabfd43730fe5b8 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Dec 2020 19:11:51 +0530 Subject: [PATCH 11/46] feat: Trigger rule application from client side - Table is reset and overwritten with applied rules on checkbox trigger - Sider fixes --- erpnext/public/js/controllers/buying.js | 23 ------------- erpnext/public/js/controllers/transaction.js | 30 +++++++++++++++++ erpnext/stock/dashboard/item_dashboard.js | 22 +++++++------ .../purchase_receipt/purchase_receipt.js | 2 +- .../purchase_receipt/purchase_receipt.py | 2 +- .../doctype/putaway_rule/putaway_rule.py | 22 +++++++------ .../stock/doctype/stock_entry/stock_entry.js | 6 ++-- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../warehouse_capacity_summary.js | 32 +++++++++---------- 9 files changed, 77 insertions(+), 64 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 1cb68a6cda2..0c6bcad7219 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -516,27 +516,4 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { }); dialog.show(); -} - -erpnext.apply_putaway_rule = (frm) => { - if (!frm.doc.company) { - frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}) - } - if (!frm.doc.items.length) return; - - frappe.call({ - method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule", - args: { - items: frm.doc.items, - company: frm.doc.company - }, - callback: (result) => { - if(!result.exc) { - if(result.message) { - frm.doc.items = result.message; - frm.get_field("items").refresh(); - } - } - } - }); } \ No newline at end of file diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7f08cd1359f..31efb6aa344 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2029,3 +2029,33 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_ }, show_dialog); }); } + +erpnext.apply_putaway_rule = (frm) => { + if (!frm.doc.company) { + frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); + } + if (!frm.doc.items.length) return; + + frappe.call({ + method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule", + args: { + doctype: frm.doctype, + items: frm.doc.items, + company: frm.doc.company, + sync: true + }, + callback: (result) => { + if (!result.exc && result.message) { + frm.clear_table("items"); + + let items = result.message; + items.forEach((row) => { + delete row["name"]; + let child = frm.add_child("items"); + Object.assign(child, row); + }); + frm.get_field("items").grid.refresh(); + } + } + }); +}; \ No newline at end of file diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index abc286fcc6e..070589b3dc0 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -30,8 +30,8 @@ erpnext.stock.ItemDashboard = Class.extend({ let company = unescape($(this).attr('data-company')); frappe.db.get_value('Putaway Rule', {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => { - frappe.set_route("Form", "Putaway Rule", r.name); - }); + frappe.set_route("Form", "Putaway Rule", r.name); + }); }); function handle_move_add(element, action) { @@ -88,7 +88,7 @@ erpnext.stock.ItemDashboard = Class.extend({ start: this.start, sort_by: this.sort_by, sort_order: this.sort_order - } + }; var me = this; frappe.call({ @@ -104,10 +104,12 @@ erpnext.stock.ItemDashboard = Class.extend({ this.max_count = 0; this.result.empty(); } + + let context = ""; if (this.page_name === "warehouse-capacity-summary") { - var context = this.get_capacity_dashboard_data(data); + context = this.get_capacity_dashboard_data(data); } else { - var context = this.get_item_dashboard_data(data, this.max_count, true); + context = this.get_item_dashboard_data(data, this.max_count, true); } this.max_count = this.max_count; @@ -152,7 +154,7 @@ erpnext.stock.ItemDashboard = Class.extend({ }); let can_write = 0; - if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){ + if (frappe.boot.user.can_write.indexOf("Stock Entry") >= 0) { can_write = 1; } @@ -165,23 +167,23 @@ erpnext.stock.ItemDashboard = Class.extend({ }, get_capacity_dashboard_data: function(data) { - if(!data) data = []; + if (!data) data = []; data.forEach(function(d) { d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef"; }); let can_write = 0; - if(frappe.boot.user.can_write.indexOf("Putaway Rule")>=0){ + if (frappe.boot.user.can_write.indexOf("Putaway Rule") >= 0) { can_write = 1; } return { data: data, can_write: can_write, - } + }; } -}) +}); erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) { var dialog = new frappe.ui.Dialog({ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 45eb646d4b9..218fb9e1210 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -214,7 +214,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend }, apply_putaway_rule: function() { - // if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm); + if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm); } }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b1ad6103d39..4372bdcc590 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -87,7 +87,7 @@ class PurchaseReceipt(BuyingController): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule if self.get("items") and self.apply_putaway_rule: - self.items = apply_putaway_rule(self.doctype, self.get("items"), self.company) + apply_putaway_rule(self.doctype, self.get("items"), self.company) def validate(self): self.validate_posting_time() diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 8838bb75f1d..4ed4dafac77 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -64,7 +64,7 @@ def get_putaway_capacity(rule): return free_space if free_space > 0 else 0 @frappe.whitelist() -def apply_putaway_rule(doctype, items, company): +def apply_putaway_rule(doctype, items, company, sync=None): """ Applies Putaway Rule on line items. items: List of Purchase Receipt Item objects @@ -82,7 +82,7 @@ def apply_putaway_rule(doctype, items, company): source_warehouse = item.get("s_warehouse") serial_nos = get_serial_nos(item.get("serial_no")) - conversion = flt(item.conversion_factor) or 1 + item.conversion_factor = flt(item.conversion_factor) or 1 pending_qty, item_code = flt(item.qty), item.item_code pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) if not pending_qty or not item_code: @@ -109,11 +109,11 @@ def apply_putaway_rule(doctype, items, company): for rule in item_wise_rules[item_code]: if pending_stock_qty > 0 and rule.free_space: stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty - qty_to_allocate = stock_qty_to_allocate / (conversion) + qty_to_allocate = stock_qty_to_allocate / item.conversion_factor if uom_must_be_whole_number: qty_to_allocate = floor(qty_to_allocate) - stock_qty_to_allocate = qty_to_allocate * conversion + stock_qty_to_allocate = qty_to_allocate * item.conversion_factor if not qty_to_allocate: break @@ -124,17 +124,19 @@ def apply_putaway_rule(doctype, items, company): pending_qty -= qty_to_allocate rule["free_space"] -= stock_qty_to_allocate - if not pending_stock_qty: break + if not pending_stock_qty > 0: break # if pending qty after applying all rules, add row without warehouse if pending_stock_qty > 0: - # updated_table = add_row(item, pending_qty, '', updated_table, serial_nos=serial_nos) items_not_accomodated.append([item.item_code, pending_qty]) if items_not_accomodated: show_unassigned_items_message(items_not_accomodated) - return updated_table if updated_table else items + items[:] = updated_table if updated_table else items # modify items table + + if sync and json.loads(sync): # sync with client side + return items def get_ordered_putaway_rules(item_code, company, source_warehouse=None): """Returns an ordered list of putaway rules to apply on an item.""" @@ -174,12 +176,14 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None): def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): new_updated_table_row = copy.deepcopy(item) new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 - new_updated_table_row.name = "New " + str(item.doctype) + " " + str(new_updated_table_row.idx) + new_updated_table_row.name = None new_updated_table_row.qty = to_allocate - new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + if item.doctype == "Stock Entry Detail": new_updated_table_row.t_warehouse = warehouse + new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) else: + new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse new_updated_table_row.rejected_qty = 0 new_updated_table_row.received_qty = to_allocate diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 2be70f37f27..fd920a585ac 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -573,10 +573,10 @@ frappe.ui.form.on('Stock Entry', { } }, - apply_putaway_rule: function(frm) { - // if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm); + apply_putaway_rule: function (frm) { + if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm); } -}) +}); frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index aa3425cbd8e..f07039f5642 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -47,7 +47,7 @@ class StockEntry(StockController): apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) if self.get("items") and apply_rule: - self.items = apply_putaway_rule(self.doctype, self.get("items"), self.company) + apply_putaway_rule(self.doctype, self.get("items"), self.company) def validate(self): self.pro_doc = frappe._dict() diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js index c3b3b5d8ec4..b610e7dd587 100644 --- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js +++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js @@ -10,8 +10,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.company_field = page.add_field({ fieldname: 'company', label: __('Company'), - fieldtype:'Link', - options:'Company', + fieldtype: 'Link', + options: 'Company', reqd: 1, default: frappe.defaults.get_default("company"), change: function() { @@ -23,8 +23,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.warehouse_field = page.add_field({ fieldname: 'warehouse', label: __('Warehouse'), - fieldtype:'Link', - options:'Warehouse', + fieldtype: 'Link', + options: 'Warehouse', change: function() { page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); @@ -34,8 +34,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.item_field = page.add_field({ fieldname: 'item_code', label: __('Item'), - fieldtype:'Link', - options:'Item', + fieldtype: 'Link', + options: 'Item', change: function() { page.capacity_dashboard.start = 0; page.capacity_dashboard.refresh(); @@ -45,8 +45,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.parent_warehouse_field = page.add_field({ fieldname: 'parent_warehouse', label: __('Parent Warehouse'), - fieldtype:'Link', - options:'Warehouse', + fieldtype: 'Link', + options: 'Warehouse', get_query: function() { return { filters: { @@ -67,8 +67,8 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { sort_order: 'desc', options: [ {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')}, - {fieldname: 'percent_occupied', label:__('% Occupied')}, - {fieldname: 'actual_qty', label:__('Balance Qty (Stock ')} + {fieldname: 'percent_occupied', label: __('% Occupied')}, + {fieldname: 'actual_qty', label: __('Balance Qty (Stock ')} ] }, change: function(sort_by, sort_order) { @@ -90,14 +90,14 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { sort_order: 'desc', method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data', template: 'warehouse_capacity_summary' - }) + }); page.capacity_dashboard.before_refresh = function() { this.item_code = page.item_field.get_value(); this.warehouse = page.warehouse_field.get_value(); this.parent_warehouse = page.parent_warehouse_field.get_value(); this.company = page.company_field.get_value(); - } + }; page.capacity_dashboard.refresh(); @@ -105,16 +105,16 @@ frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) { page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() { var name = $(this).attr('data-name'); var field = page[doctype.toLowerCase() + '_field']; - if(field.get_value()===name) { - frappe.set_route('Form', doctype, name) + if (field.get_value()===name) { + frappe.set_route('Form', doctype, name); } else { field.set_input(name); page.capacity_dashboard.refresh(); } }); - } + }; setup_click('Item'); setup_click('Warehouse'); }); -} \ No newline at end of file +}; \ No newline at end of file From a5d8d3277560c794377a2449e534a1354dfba9a3 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Dec 2020 16:27:18 +0530 Subject: [PATCH 12/46] chore: Test and fixes - Tests as per new design flow - Fixed duplicate data bug in Warehouse Capacity Summary - Set Amount currently on applying rule in client side - Apply rules on server side before validate --- erpnext/public/js/controllers/transaction.js | 3 +- erpnext/stock/dashboard/item_dashboard.js | 2 +- .../purchase_receipt/purchase_receipt.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 2 + .../doctype/putaway_rule/test_putaway_rule.py | 135 +++++++++--------- .../stock/doctype/stock_entry/stock_entry.py | 2 +- 6 files changed, 72 insertions(+), 74 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 31efb6aa344..54634482b0a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2050,9 +2050,10 @@ erpnext.apply_putaway_rule = (frm) => { let items = result.message; items.forEach((row) => { - delete row["name"]; + delete row["name"]; // dont overwrite name from server side let child = frm.add_child("items"); Object.assign(child, row); + frm.script_manager.trigger("qty", child.doctype, child.name); }); frm.get_field("items").grid.refresh(); } diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 070589b3dc0..20415bc07e8 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -69,7 +69,7 @@ erpnext.stock.ItemDashboard = Class.extend({ // more this.content.find('.btn-more').on('click', function() { - me.start += this.page_length; + me.start += me.page_length; me.refresh(); }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4372bdcc590..d36d5159dbb 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -83,7 +83,7 @@ class PurchaseReceipt(BuyingController): } ]) - def before_save(self): + def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule if self.get("items") and self.apply_putaway_rule: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 9b8eeed1a12..7b3a83065b9 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -979,6 +979,7 @@ def make_purchase_receipt(**args): pr.currency = args.currency or "INR" pr.is_return = args.is_return pr.return_against = args.return_against + pr.apply_putaway_rule = args.apply_putaway_rule qty = args.qty or 5 received_qty = args.received_qty or qty rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty) @@ -994,6 +995,7 @@ def make_purchase_receipt(**args): "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", "rate": args.rate if args.rate != None else 50, "conversion_factor": args.conversion_factor or 1.0, + "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", "uom": uom, diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index 7b81784d5fa..17619e01bc1 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -9,8 +9,8 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt class TestPutawayRule(unittest.TestCase): def setUp(self): @@ -42,17 +42,15 @@ class TestPutawayRule(unittest.TestCase): rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=300, uom="Kg", priority=2) - po = create_purchase_order(item_code="_Rice", qty=300) - self.assertEqual(len(po.items), 1) - - pr = make_purchase_receipt(po.name) + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, + do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, warehouse_1) self.assertEqual(pr.items[1].qty, 100) self.assertEqual(pr.items[1].warehouse, warehouse_2) - po.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -70,10 +68,8 @@ class TestPutawayRule(unittest.TestCase): # out of 500 kg capacity, occupy 100 kg in warehouse_1 stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=100, basic_rate=50) - po = create_purchase_order(item_code="_Rice", qty=700) - self.assertEqual(len(po.items), 1) - - pr = make_purchase_receipt(po.name) + pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, + do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 500) # warehouse_2 has 500 kg free space, it is given priority @@ -82,8 +78,8 @@ class TestPutawayRule(unittest.TestCase): # warehouse_1 has 400 kg free space, it is given less priority self.assertEqual(pr.items[1].warehouse, warehouse_1) - po.cancel() stock_receipt.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -97,21 +93,14 @@ class TestPutawayRule(unittest.TestCase): rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=200, uom="Kg") - po = create_purchase_order(item_code="_Rice", qty=350) - self.assertEqual(len(po.items), 1) - - pr = make_purchase_receipt(po.name) - - self.assertEqual(len(pr.items), 3) + pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, warehouse_2) self.assertEqual(pr.items[1].qty, 100) self.assertEqual(pr.items[1].warehouse, warehouse_1) - # extra qty has no warehouse assigned - self.assertEqual(pr.items[2].qty, 50) - self.assertEqual(pr.items[2].warehouse, '') - - po.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -135,24 +124,19 @@ class TestPutawayRule(unittest.TestCase): uom="Bag") self.assertEqual(rule_2.stock_capacity, 4000) + # populate 'Rack 1' with 1 Bag, making the free space 2 Bags stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=1000, basic_rate=50) - po = create_purchase_order(item_code="_Rice", qty=6, do_not_save=True) - po.items[0].uom = "Bag" - po.save() - po.submit() - - self.assertEqual(po.items[0].stock_qty, 6000) - - pr = make_purchase_receipt(po.name) + pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", + conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 4) self.assertEqual(pr.items[0].warehouse, warehouse_2) self.assertEqual(pr.items[1].qty, 2) self.assertEqual(pr.items[1].warehouse, warehouse_1) - po.cancel() stock_receipt.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -180,24 +164,15 @@ class TestPutawayRule(unittest.TestCase): self.assertEqual(rule_2.stock_capacity, 500) # total capacity is 1500 Kg - po = create_purchase_order(item_code="_Rice", qty=2, do_not_save=True) - # PO for 2 Bags (2000 Kg) - po.items[0].uom = "Bag" - po.save() - po.submit() - - self.assertEqual(po.items[0].stock_qty, 2000) - - pr = make_purchase_receipt(po.name) - self.assertEqual(len(pr.items), 2) + pr = make_purchase_receipt(item_code="_Rice", qty=2, uom="Bag", stock_uom="Kg", + conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 1) self.assertEqual(pr.items[0].warehouse, warehouse_1) # leftover space was for 500 kg (0.5 Bag) # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned - self.assertEqual(pr.items[1].qty, 1) - self.assertEqual(pr.items[1].warehouse, '') - po.cancel() + pr.delete() rule_1.delete() rule_2.delete() @@ -208,38 +183,58 @@ class TestPutawayRule(unittest.TestCase): rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=100, - uom="Kg", priority=2) - # total capacity is 300 Kg + # total capacity is 200 Kg - po = create_purchase_order(item_code="_Rice", qty=200, rate=100, do_not_save=True) - po.append("items", { - "item_code":"_Rice", + pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, + do_not_submit=1) + pr.append("items", { + "item_code": "_Rice", "warehouse": "_Test Warehouse - _TC", - "qty": 300, - "rate": 120, - "schedule_date": add_days(nowdate(), 1), - }) - po.save() - po.submit() - # PO for 500 Kg (two rows of same item, different rates) - self.assertEqual(len(po.items), 2) - - pr = make_purchase_receipt(po.name) - self.assertEqual(len(pr.items), 3) - self.assertEqual(pr.items[0].qty, 200) + "qty": 200, + "uom": "Kg", + "stock_uom": "Kg", + "stock_qty": 200, + "received_qty": 200, + "rate": 100, + "conversion_factor": 1.0, + }) # same item entered again in PR but with different rate + pr.save() + self.assertEqual(len(pr.items), 2) + self.assertEqual(pr.items[0].qty, 100) self.assertEqual(pr.items[0].warehouse, warehouse_1) - # same rules applied to second item row + self.assertEqual(pr.items[0].putaway_rule, rule_1.name) + # same rule applied to second item row # with previous assignment considered - self.assertEqual(pr.items[1].qty, 100) - self.assertEqual(pr.items[1].warehouse, warehouse_2) - # unassigned 200 Kg - self.assertEqual(pr.items[2].qty, 200) - self.assertEqual(pr.items[2].warehouse, '') + self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 + self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].putaway_rule, rule_1.name) - po.cancel() + pr.delete() + rule_1.delete() + + def test_validate_over_receipt_in_warehouse(self): + """Test if overreceipt is blocked in the presence of putaway rules.""" + warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + uom="Kg") + + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, + do_not_submit=1) + self.assertEqual(len(pr.items), 1) + self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg + self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].putaway_rule, rule_1.name) + + # force overreceipt and disable apply putaway rule in PR + pr.items[0].qty = 300 + pr.items[0].stock_qty = 300 + pr.apply_putaway_rule = 0 + self.assertRaises(frappe.ValidationError, pr.save) + + pr.delete() rule_1.delete() - rule_2.delete() def create_putaway_rule(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f07039f5642..aed69e11ddf 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -42,7 +42,7 @@ class StockEntry(StockController): for item in self.get("items"): item.update(get_bin_details(item.item_code, item.s_warehouse)) - def before_save(self): + def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) From c47d38dc1820c3328b2e782c740fa10ad3471890 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Dec 2020 11:48:04 +0530 Subject: [PATCH 13/46] chore: Stock Entry Tests and fixes - Maintain item-warehouse wise rules for Stock Entry Material Transfer - Test cases for Stock Entry with more use cases - Sider fixes --- erpnext/public/js/controllers/transaction.js | 5 +- .../doctype/putaway_rule/putaway_rule.py | 38 +-- .../doctype/putaway_rule/test_putaway_rule.py | 233 ++++++++++++++---- .../stock/doctype/stock_entry/stock_entry.js | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 3 +- .../doctype/stock_entry/stock_entry_utils.py | 5 +- 6 files changed, 216 insertions(+), 70 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 54634482b0a..2dabb9a6474 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2030,7 +2030,7 @@ erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_ }); } -erpnext.apply_putaway_rule = (frm) => { +erpnext.apply_putaway_rule = (frm, purpose=null) => { if (!frm.doc.company) { frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); } @@ -2042,7 +2042,8 @@ erpnext.apply_putaway_rule = (frm) => { doctype: frm.doctype, items: frm.doc.items, company: frm.doc.company, - sync: true + sync: true, + purpose: purpose }, callback: (result) => { if (!result.exc && result.message) { diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 4ed4dafac77..56752583fa7 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -64,11 +64,14 @@ def get_putaway_capacity(rule): return free_space if free_space > 0 else 0 @frappe.whitelist() -def apply_putaway_rule(doctype, items, company, sync=None): +def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): """ Applies Putaway Rule on line items. - items: List of Purchase Receipt Item objects - company: Company in the Purchase Receipt + items: List of Purchase Receipt/Stock Entry Items + company: Company in the Purchase Receipt/Stock Entry + doctype: Doctype to apply rule on + purpose: Purpose of Stock Entry + sync (optional): Sync with client side only for client side calls """ if isinstance(items, string_types): items = json.loads(items) @@ -82,31 +85,36 @@ def apply_putaway_rule(doctype, items, company, sync=None): source_warehouse = item.get("s_warehouse") serial_nos = get_serial_nos(item.get("serial_no")) - item.conversion_factor = flt(item.conversion_factor) or 1 + item.conversion_factor = flt(item.conversion_factor) or 1.0 pending_qty, item_code = flt(item.qty), item.item_code pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) - if not pending_qty or not item_code: - updated_table = add_row(item, pending_qty, item.warehouse, updated_table) - continue - uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + if not pending_qty or not item_code: + updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) + continue + at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: - warehouse = item.warehouse + warehouse = source_warehouse or item.warehouse if at_capacity: - warehouse = '' # rules available, but no free space + # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) - updated_table = add_row(item, pending_qty, warehouse, updated_table) + else: + updated_table = add_row(item, pending_qty, warehouse, updated_table) continue - # maintain item wise rules, to handle if item is entered twice + # maintain item/item-warehouse wise rules, to handle if item is entered twice # in the table, due to different price, etc. - if not item_wise_rules[item_code]: - item_wise_rules[item_code] = rules + key = item_code + if doctype == "Stock Entry" and purpose == "Material Transfer" and source_warehouse: + key = (item_code, source_warehouse) - for rule in item_wise_rules[item_code]: + if not item_wise_rules[key]: + item_wise_rules[key] = rules + + for rule in item_wise_rules[key]: if pending_stock_qty > 0 and rule.free_space: stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty qty_to_allocate = stock_qty_to_allocate / item.conversion_factor diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index 17619e01bc1..86f7dc3e084 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -4,12 +4,11 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import add_days, nowdate from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt class TestPutawayRule(unittest.TestCase): @@ -27,6 +26,9 @@ class TestPutawayRule(unittest.TestCase): if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}): create_warehouse("Rack 2") + self.warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) + self.warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) + if not frappe.db.exists("UOM", "Bag"): new_uom = frappe.new_doc("UOM") new_uom.uom_name = "Bag" @@ -34,21 +36,18 @@ class TestPutawayRule(unittest.TestCase): def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=300, + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=300, uom="Kg", priority=2) pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) - self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].qty, 100) - self.assertEqual(pr.items[1].warehouse, warehouse_2) + self.assertEqual(pr.items[1].warehouse, self.warehouse_2) pr.delete() rule_1.delete() @@ -57,26 +56,23 @@ class TestPutawayRule(unittest.TestCase): def test_putaway_rules_with_same_priority(self): """Test if rule with more free space is applied, among two rules with same priority and capacity.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=500, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500, + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg") # out of 500 kg capacity, occupy 100 kg in warehouse_1 - stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=100, basic_rate=50) + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50) pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 500) # warehouse_2 has 500 kg free space, it is given priority - self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) self.assertEqual(pr.items[1].qty, 200) # warehouse_1 has 400 kg free space, it is given less priority - self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) stock_receipt.cancel() pr.delete() @@ -85,21 +81,20 @@ class TestPutawayRule(unittest.TestCase): def test_putaway_rules_with_insufficient_capacity(self): """Test if qty exceeding capacity, is handled.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=100, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=200, + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg") pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) - self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) self.assertEqual(pr.items[1].qty, 100) - self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) + # total 300 assigned, 50 unassigned + pr.delete() rule_1.delete() rule_2.delete() @@ -114,26 +109,23 @@ class TestPutawayRule(unittest.TestCase): }) item.save() - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=3, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag") self.assertEqual(rule_1.stock_capacity, 3000) - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=4, + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag") self.assertEqual(rule_2.stock_capacity, 4000) # populate 'Rack 1' with 1 Bag, making the free space 2 Bags - stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=1000, basic_rate=50) + stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50) pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 4) - self.assertEqual(pr.items[0].warehouse, warehouse_2) + self.assertEqual(pr.items[0].warehouse, self.warehouse_2) self.assertEqual(pr.items[1].qty, 2) - self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) stock_receipt.cancel() pr.delete() @@ -152,15 +144,12 @@ class TestPutawayRule(unittest.TestCase): frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - # Putaway Rule in different UOM - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=1, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag") self.assertEqual(rule_1.stock_capacity, 1000) # Putaway Rule in Stock UOM - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500) + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500) self.assertEqual(rule_2.stock_capacity, 500) # total capacity is 1500 Kg @@ -168,7 +157,7 @@ class TestPutawayRule(unittest.TestCase): conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 1) - self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) # leftover space was for 500 kg (0.5 Bag) # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned @@ -177,11 +166,8 @@ class TestPutawayRule(unittest.TestCase): rule_2.delete() def test_putaway_rules_with_reoccurring_item(self): - """Test rules on same item entered multiple times.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + """Test rules on same item entered multiple times with different rate.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") # total capacity is 200 Kg @@ -201,12 +187,12 @@ class TestPutawayRule(unittest.TestCase): pr.save() self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 100) - self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[0].putaway_rule, rule_1.name) # same rule applied to second item row # with previous assignment considered self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 - self.assertEqual(pr.items[1].warehouse, warehouse_1) + self.assertEqual(pr.items[1].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].putaway_rule, rule_1.name) pr.delete() @@ -214,17 +200,14 @@ class TestPutawayRule(unittest.TestCase): def test_validate_over_receipt_in_warehouse(self): """Test if overreceipt is blocked in the presence of putaway rules.""" - warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"}) - warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"}) - - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200, + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg") pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg - self.assertEqual(pr.items[0].warehouse, warehouse_1) + self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[0].putaway_rule, rule_1.name) # force overreceipt and disable apply putaway rule in PR @@ -236,6 +219,156 @@ class TestPutawayRule(unittest.TestCase): pr.delete() rule_1.delete() + def test_putaway_rule_on_stock_entry_material_transfer(self): + """Test if source warehouse is considered while applying rules.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") # higher priority + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, + uom="Kg", priority=2) + + stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_1, qty=200, + target="_Test Warehouse - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_submit=1) + + stock_entry_item = stock_entry.get("items")[0] + + # since source warehouse is Rack 1, rule 1 (for Rack 1) will be avoided + # even though it has more free space and higher priority + self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg + self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self): + """Test if reoccuring item is correctly considered.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, + uom="Kg") + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=600, + uom="Kg", priority=2) + + # create SE with first row having source warehouse as Rack 2 + stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_2, qty=200, + target="_Test Warehouse - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_submit=1) + + # Add rows with source warehouse as Rack 1 + stock_entry.extend("items", [ + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 100, + "basic_rate": 50, + "conversion_factor": 1.0, + "transfer_qty": 100 + }, + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 200, + "basic_rate": 60, + "conversion_factor": 1.0, + "transfer_qty": 200 + } + ]) + + stock_entry.save() + + # since source warehouse was Rack 2, exclude rule_2 + self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry.items[0].qty, 200) + self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) + + # since source warehouse was Rack 1, exclude rule_1 even though it has + # higher priority + self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[1].qty, 100) + self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) + + self.assertEqual(stock_entry.items[2].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[2].qty, 200) + self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_transfer_batch_serial_item(self): + """Test if batch and serial items are split correctly.""" + if not frappe.db.exists("Item", "Water Bottle"): + make_item("Water Bottle", { + "is_stock_item": 1, + "has_batch_no" : 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "BOTTL-.####", + "stock_uom": "Nos" + }) + + rule_1 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, + uom="Nos") + rule_2 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, + uom="Nos") + + make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle") + + pr = make_purchase_receipt(item_code="Water Bottle", qty=5, do_not_submit=1) + pr.items[0].batch_no = "BOTTL-BATCH-1" + pr.save() + pr.submit() + + serial_nos = frappe.get_list("Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}) + serial_nos = [d.name for d in serial_nos] + + stock_entry = make_stock_entry(item_code="Water Bottle", source="_Test Warehouse - _TC", qty=5, + target="Finished Goods - _TC", purpose="Material Transfer", + apply_putaway_rule=1, do_not_save=1) + stock_entry.items[0].batch_no = "BOTTL-BATCH-1" + stock_entry.items[0].serial_no = "\n".join(serial_nos) + stock_entry.save() + + self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry.items[0].qty, 3) + self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name) + self.assertEqual(stock_entry.items[0].serial_no, "\n".join(serial_nos[:3])) + self.assertEqual(stock_entry.items[0].batch_no, "BOTTL-BATCH-1") + + self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2) + self.assertEqual(stock_entry.items[1].qty, 2) + self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name) + self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:])) + self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") + + stock_entry.delete() + pr.cancel() + rule_1.delete() + rule_2.delete() + + def test_putaway_rule_on_stock_entry_material_receipt(self): + """Test if rules are applied in Stock Entry of type Receipt.""" + rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, + uom="Kg") # more capacity + rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, + uom="Kg") + + stock_entry = make_stock_entry(item_code="_Rice", qty=100, + target="_Test Warehouse - _TC", purpose="Material Receipt", + apply_putaway_rule=1, do_not_submit=1) + + stock_entry_item = stock_entry.get("items")[0] + + self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_1) + self.assertEqual(stock_entry_item.qty, 100) + self.assertEqual(stock_entry_item.putaway_rule, rule_1.name) + + stock_entry.delete() + rule_1.delete() + rule_2.delete() + def create_putaway_rule(**args): args = frappe._dict(args) putaway = frappe.new_doc("Putaway Rule") diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index fd920a585ac..5d9ea8daeeb 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -574,7 +574,7 @@ frappe.ui.form.on('Stock Entry', { }, apply_putaway_rule: function (frm) { - if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm); + if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose); } }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index aed69e11ddf..e37e1223bc6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -47,7 +47,8 @@ class StockEntry(StockController): apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) if self.get("items") and apply_rule: - apply_putaway_rule(self.doctype, self.get("items"), self.company) + apply_putaway_rule(self.doctype, self.get("items"), self.company, + purpose=self.purpose) def validate(self): self.pro_doc = frappe._dict() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b78c6be983f..b12a8547fea 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -53,6 +53,8 @@ def make_stock_entry(**args): args.target = args.to_warehouse if args.item_code: args.item = args.item_code + if args.apply_putaway_rule: + s.apply_putaway_rule = args.apply_putaway_rule if isinstance(args.qty, string_types): if '.' in args.qty: @@ -118,7 +120,8 @@ def make_stock_entry(**args): "t_warehouse": args.target, "qty": args.qty, "basic_rate": args.rate or args.basic_rate, - "conversion_factor": 1.0, + "conversion_factor": args.conversion_factor or 1.0, + "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), "serial_no": args.serial_no, 'batch_no': args.batch_no, 'cost_center': args.cost_center, From 46f3c65b003442ecfcdbcdc1d13422f872ea88bf Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Dec 2020 23:09:19 +0530 Subject: [PATCH 14/46] feat: Multi currency in landed cost voucher --- .../landed_cost_taxes_and_charges.json | 32 +++++++++- .../landed_cost_voucher.js | 58 +++++++++++++++++-- .../landed_cost_voucher.json | 26 ++++++++- .../landed_cost_voucher.py | 31 ++++++---- 4 files changed, 129 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 0cc243d4cb5..ae8747fc830 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -1,12 +1,16 @@ { + "actions": [], "creation": "2014-07-11 11:51:00.453717", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "expense_account", + "account_currency", + "exchange_rate", "description", "col_break3", + "base_amount", "amount" ], "fields": [ @@ -27,7 +31,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "Company:company:default_currency", + "options": "account_currency", "reqd": 1 }, { @@ -37,10 +41,34 @@ "label": "Expense Account", "options": "Account", "reqd": 1 + }, + { + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9" + }, + { + "fieldname": "base_amount", + "fieldtype": "Currency", + "label": "Base Amount", + "options": "Company:company:default_currency", + "read_only": 1, + "reqd": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-09-30 18:28:32.070655", + "links": [], + "modified": "2020-12-13 21:04:01.769989", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index 5de13525183..a5ac60c4f51 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -32,9 +32,8 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ this.frm.set_query("expense_account", "taxes", function() { return { - query: "erpnext.controllers.queries.tax_account_query", filters: { - "account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"], + "account_type": ['in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], "company": me.frm.doc.company } }; @@ -97,7 +96,7 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ set_total_taxes_and_charges: function() { var total_taxes_and_charges = 0.0; $.each(this.frm.doc.taxes || [], function(i, d) { - total_taxes_and_charges += flt(d.amount) + total_taxes_and_charges += flt(d.base_amount) }); cur_frm.set_value("total_taxes_and_charges", total_taxes_and_charges); }, @@ -134,7 +133,58 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ items_remove: () => { this.trigger('set_applicable_charges_for_item'); } - }); cur_frm.script_manager.make(erpnext.stock.LandedCostVoucher); + +frappe.ui.form.on('Landed Cost Voucher', { + set_account_currency: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.expense_account) { + frappe.db.get_value('Account', row.expense_account, 'account_currency', function(value) { + frappe.model.set_value(cdt, cdn, "account_currency", value.account_currency); + frm.events.set_exchange_rate(frm, cdt, cdn); + }); + } + }, + + set_exchange_rate: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + + if(row.account_currency == company_currency) { + row.exchange_rate = 1; + } else if (!row.exchange_rate || row.exchange_rate == 1) { + frappe.call({ + method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate", + args: { + posting_date: frm.doc.posting_date, + account: row.expense_account, + account_currency: row.account_currency, + company: frm.doc.company + }, + callback: function(r) { + if(r.message) { + frappe.model.set_value(cdt, cdn, "exchange_rate", r.message); + } + } + }) + } + }, + + set_base_amount: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, "base_amount", + flt(flt(row.amount)*row.exchange_rate, precision("base_amount", row))); + } +}) + +frappe.ui.form.on('Landed Cost Taxes and Charges', { + expense_account: function(frm, cdt, cdn) { + frm.events.set_account_currency(frm, cdt, cdn); + }, + + amount: function(frm, cdt, cdn) { + frm.events.set_base_amount(frm, cdt, cdn); + } +}); diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json index 01492807def..6a7994ba9e5 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2014-07-11 11:33:42.547339", "doctype": "DocType", @@ -7,6 +8,9 @@ "field_order": [ "naming_series", "company", + "column_break_2", + "posting_date", + "section_break_5", "purchase_receipts", "purchase_receipt_items", "get_items_from_purchase_receipts", @@ -86,7 +90,7 @@ { "fieldname": "total_taxes_and_charges", "fieldtype": "Currency", - "label": "Total Taxes and Charges", + "label": "Total Taxes and Charges (Company Currency)", "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 @@ -119,11 +123,29 @@ "fieldname": "landed_cost_help", "fieldtype": "HTML", "label": "Landed Cost Help" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hide_border": 1 } ], "icon": "icon-usd", + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-11-21 15:34:10.846093", + "links": [], + "modified": "2020-12-13 23:18:47.442466", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Voucher", 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 bc3d3266add..e39208991bd 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -9,6 +9,7 @@ from frappe.model.meta import get_field_precision from frappe.model.document import Document from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.accounts.doctype.account.account import get_account_currency +from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate class LandedCostVoucher(Document): def get_items_from_purchase_receipts(self): @@ -39,12 +40,13 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() + self.set_exchange_rate() + self.set_amounts_in_company_currency() if not self.get("items"): self.get_items_from_purchase_receipts() else: self.validate_applicable_charges_for_item() self.validate_purchase_receipts() - self.validate_expense_accounts() self.set_total_taxes_and_charges() def check_mandatory(self): @@ -73,16 +75,25 @@ class LandedCostVoucher(Document): frappe.throw(_("Row {0}: Cost center is required for an item {1}") .format(item.idx, item.item_code)) - def validate_expense_accounts(self): - company_currency = erpnext.get_company_currency(self.company) - for account in self.taxes: - if get_account_currency(account.expense_account) != company_currency: - frappe.throw(msg=_(""" Row {0}: Expense account currency should be same as company's default currency. - Please select expense account with account currency as {1}""") - .format(account.idx, frappe.bold(company_currency)), title=_("Invalid Account Currency")) - def set_total_taxes_and_charges(self): - self.total_taxes_and_charges = sum([flt(d.amount) for d in self.get("taxes")]) + self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")]) + + def set_exchange_rate(self): + company_currency = erpnext.get_company_currency(self.company) + for d in self.get('taxes'): + if d.account_currency == company_currency: + d.exchange_rate = 1 + elif not d.exchange_rate or d.exchange_rate == 1 or self.posting_date: + d.exchange_rate = get_exchange_rate(self.posting_date, account=d.expense_account, + account_currency=d.account_currency, company=self.company) + + if not d.exchange_rate: + frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) + + def set_amounts_in_company_currency(self): + for d in self.get('taxes'): + d.amount = flt(d.amount, d.precision("amount")) + d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) def validate_applicable_charges_for_item(self): based_on = self.distribute_charges_based_on.lower() From 5a2030c2f22e1af5eeedcab053978758826dd34b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Dec 2020 09:21:11 +0530 Subject: [PATCH 15/46] fix: Add server side methods and other fixes --- erpnext/controllers/accounts_controller.py | 2 +- .../landed_cost_voucher.js | 19 ++++--- .../landed_cost_voucher.py | 38 ++++++++++++-- .../test_landed_cost_voucher.py | 51 +++++++++++++++++-- .../purchase_receipt/purchase_receipt.py | 16 ++++-- 5 files changed, 106 insertions(+), 20 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 32c5d3a3b14..49c9f8c9d0f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -442,7 +442,7 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(gl_dict.account) if gl_dict.account and self.doctype not in ["Journal Entry", - "Period Closing Voucher", "Payment Entry"]: + "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice"]: self.validate_account_currency(gl_dict.account, account_currency) set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"), self.company_currency) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index a5ac60c4f51..82b52427041 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -96,9 +96,9 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ set_total_taxes_and_charges: function() { var total_taxes_and_charges = 0.0; $.each(this.frm.doc.taxes || [], function(i, d) { - total_taxes_and_charges += flt(d.base_amount) + total_taxes_and_charges += flt(d.base_amount); }); - cur_frm.set_value("total_taxes_and_charges", total_taxes_and_charges); + this.frm.set_value("total_taxes_and_charges", total_taxes_and_charges); }, set_applicable_charges_for_item: function() { @@ -148,11 +148,16 @@ frappe.ui.form.on('Landed Cost Voucher', { } }, + onload: function(frm) { + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + frm.set_currency_labels(["total_taxes_and_charges"], company_currency); + }, + set_exchange_rate: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; - if(row.account_currency == company_currency) { + if (row.account_currency == company_currency) { row.exchange_rate = 1; } else if (!row.exchange_rate || row.exchange_rate == 1) { frappe.call({ @@ -164,12 +169,12 @@ frappe.ui.form.on('Landed Cost Voucher', { company: frm.doc.company }, callback: function(r) { - if(r.message) { + if (r.message) { frappe.model.set_value(cdt, cdn, "exchange_rate", r.message); } } - }) - } + }); + }; }, set_base_amount: function(frm, cdt, cdn) { @@ -177,7 +182,7 @@ frappe.ui.form.on('Landed Cost Voucher', { frappe.model.set_value(cdt, cdn, "base_amount", flt(flt(row.amount)*row.exchange_rate, precision("base_amount", row))); } -}) +}); frappe.ui.form.on('Landed Cost Taxes and Charges', { expense_account: function(frm, cdt, cdn) { 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 e39208991bd..519077afa49 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -40,14 +40,17 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() + self.validate_purchase_receipts() + self.set_account_currency() self.set_exchange_rate() self.set_amounts_in_company_currency() + self.set_total_taxes_and_charges() if not self.get("items"): self.get_items_from_purchase_receipts() - else: - self.validate_applicable_charges_for_item() - self.validate_purchase_receipts() - self.set_total_taxes_and_charges() + + self.set_applicable_charges_on_item() + self.validate_applicable_charges_for_item() + def check_mandatory(self): if not self.get("purchase_receipts"): @@ -78,6 +81,33 @@ class LandedCostVoucher(Document): def set_total_taxes_and_charges(self): self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")]) + def set_applicable_charges_on_item(self): + if self.get('taxes'): + total_item_cost = 0.0 + total_charges = 0.0 + item_count = 0 + based_on_field = frappe.scrub(self.distribute_charges_based_on) + + for item in self.get('items'): + total_item_cost += item.get(based_on_field) + + for item in self.get('items'): + item.applicable_charges = flt(flt(item.get(based_on_field)) * flt(self.total_taxes_and_charges) / flt(total_item_cost), + item.precision('applicable_charges')) + total_charges += item.applicable_charges + item_count += 1 + + if total_charges != self.total_taxes_and_charges: + diff = self.total_taxes_and_charges - total_charges + self.get('items')[item_count - 1].applicable_charges += diff + + def set_account_currency(self): + company_currency = erpnext.get_company_currency(self.company) + for d in self.get('taxes'): + if not d.account_currency: + account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency') + d.account_currency = account_currency or company_currency + def set_exchange_rate(self): company_currency = erpnext.get_company_currency(self.company) for d in self.get('taxes'): 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 3f2c5daf669..79d306b5dbb 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 @@ -10,6 +10,7 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ import set_perpetual_inventory, get_gl_entries, test_records as pr_test_records, make_purchase_receipt from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.account.test_account import get_inventory_account +from erpnext.accounts.doctype.account.test_account import create_account class TestLandedCostVoucher(unittest.TestCase): def test_landed_cost_voucher(self): @@ -27,7 +28,7 @@ class TestLandedCostVoucher(unittest.TestCase): }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) - submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount") self.assertEqual(pr_lc_value, 25.0) @@ -89,7 +90,7 @@ class TestLandedCostVoucher(unittest.TestCase): }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) - submit_landed_cost_voucher("Purchase Invoice", pi.name, pi.company) + create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company) pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name}, "landed_cost_voucher_amount") @@ -137,7 +138,7 @@ class TestLandedCostVoucher(unittest.TestCase): serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate") - submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) @@ -160,7 +161,7 @@ class TestLandedCostVoucher(unittest.TestCase): }) pr.submit() - lcv = submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) + lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) self.assertEqual(lcv.items[0].applicable_charges, 41.07) self.assertEqual(lcv.items[2].applicable_charges, 41.08) @@ -206,6 +207,46 @@ class TestLandedCostVoucher(unittest.TestCase): self.assertEqual(pr.items[0].landed_cost_voucher_amount, 100) self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100) + def test_multi_currency_lcv(self): + ## Create USD Shipping charges_account + usd_shipping = create_account(account_name="Shipping Charges USD", + parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", + account_currency="USD") + + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", + supplier_warehouse = "Stores - TCP1") + pr.submit() + + lcv = make_landed_cost_voucher(company = pr.company, receipt_document_type = "Purchase Receipt", + receipt_document=pr.name, charges=100, do_not_save=True) + + lcv.append("taxes", { + "description": "Shipping Charges", + "expense_account": usd_shipping, + "amount": 10 + }) + + lcv.save() + lcv.submit() + pr.load_from_db() + + # Considering exchange rate from USD to INR as 62.9 + self.assertEqual(lcv.total_taxes_and_charges, 729) + self.assertEqual(pr.items[0].landed_cost_voucher_amount, 729) + + gl_entries = frappe.get_all("GL Entry", fields=["account", "credit", "credit_in_account_currency"], + filters={"voucher_no": pr.name, "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"])}) + + expected_gl_entries = { + "Shipping Charges USD - TCP1": [629, 10], + "Expenses Included In Valuation - TCP1": [100, 100] + } + + for entry in gl_entries: + amounts = expected_gl_entries.get(entry.account) + self.assertEqual(entry.credit, amounts[0]) + self.assertEqual(entry.credit_in_account_currency, amounts[1]) + def make_landed_cost_voucher(** args): args = frappe._dict(args) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) @@ -236,7 +277,7 @@ def make_landed_cost_voucher(** args): return lcv -def submit_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50): +def create_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50): ref_doc = frappe.get_doc(receipt_document_type, receipt_document) lcv = frappe.new_doc("Landed Cost Voucher") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 97e0fa738cd..2cb8d2b26da 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -279,12 +279,15 @@ class PurchaseReceipt(BuyingController): # Amount added through landed-cost-voucher if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): + account_currency = get_account_currency(account) gl_entries.append(self.get_gl_dict({ "account": account, + "account_currency": account_currency, "against": warehouse_account[d.warehouse]["account"], "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), "project": d.project }, item=d)) @@ -729,9 +732,16 @@ def get_item_account_wise_additional_cost(purchase_document): if item.receipt_document == purchase_document: for account in landed_cost_voucher_doc.taxes: item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, 0.0) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account] += \ + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(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 + 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 + return item_account_wise_cost From d6cd02d29d26bc814328ded38f12c090a27a6970 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Dec 2020 09:21:24 +0530 Subject: [PATCH 16/46] fix: Add tests --- erpnext/accounts/doctype/account/test_account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 0605d89a7e2..fbcaa6cc186 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -254,7 +254,8 @@ def create_account(**kwargs): account_name = kwargs.get('account_name'), account_type = kwargs.get('account_type'), parent_account = kwargs.get('parent_account'), - company = kwargs.get('company') + company = kwargs.get('company'), + account_currency = kwargs.get('account_currency') )) account.save() From 6f99c0850c3e40eff4a0c4f43ba850a1bacda620 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Dec 2020 12:19:58 +0530 Subject: [PATCH 17/46] fix: Linting Issues --- .../stock/doctype/landed_cost_voucher/landed_cost_voucher.js | 2 +- .../stock/doctype/landed_cost_voucher/landed_cost_voucher.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index 82b52427041..c817f05e5a2 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -174,7 +174,7 @@ frappe.ui.form.on('Landed Cost Voucher', { } } }); - }; + } }, set_base_amount: function(frm, cdt, cdn) { 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 519077afa49..ed545515957 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -193,8 +193,8 @@ class LandedCostVoucher(Document): docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, 'item_code': item.item_code }, fields=['name', 'docstatus']) if not docs or len(docs) != item.qty: - frappe.throw(_('There are not enough asset created or linked to {0}. \ - Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty)) + frappe.throw(_('There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document.').format( + item.receipt_document, item.qty)) if docs: for d in docs: if d.docstatus == 1: From 51892efd60e8cd86fde4d9167eca064f691d1b4f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Dec 2020 12:35:22 +0530 Subject: [PATCH 18/46] fix: Translation issues --- .../stock/doctype/landed_cost_voucher/landed_cost_voucher.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 ed545515957..17bfa8fbb02 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -198,8 +198,7 @@ class LandedCostVoucher(Document): if docs: for d in docs: if d.docstatus == 1: - frappe.throw(_('{2} {0} has submitted Assets.\ - Remove Item {1} from table to continue.').format( + frappe.throw(_('{2} {0} has submitted Assets. Remove Item {1} from table to continue.').format( item.receipt_document, item.item_code, item.receipt_document_type)) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): From e5e2ad646a762567115e0bb64e2d0e6acf7dd4c1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Dec 2020 19:53:07 +0530 Subject: [PATCH 19/46] fix: Test Cases --- .../doctype/purchase_invoice/purchase_invoice.py | 11 ++++++----- .../landed_cost_voucher/landed_cost_voucher.py | 8 ++++---- .../landed_cost_voucher/test_landed_cost_voucher.py | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b52678e8d3b..3c29561ad11 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -443,7 +443,7 @@ class PurchaseInvoice(BuyingController): else: self.stock_received_but_not_billed = None self.expenses_included_in_valuation = None - + self.negative_expense_to_be_booked = 0.0 gl_entries = [] @@ -457,7 +457,7 @@ class PurchaseInvoice(BuyingController): self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) - + gl_entries = merge_similar_entries(gl_entries) self.make_payment_gl_entries(gl_entries) @@ -582,7 +582,8 @@ class PurchaseInvoice(BuyingController): "against": item.expense_account, "cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), "project": item.project or self.project }, item=item)) @@ -999,10 +1000,10 @@ class PurchaseInvoice(BuyingController): self.delete_auto_created_batches() self.make_gl_entries_on_cancel() - + if self.update_stock == 1: self.repost_future_sle_and_gle() - + self.update_project() frappe.db.set(self, 'status', 'Cancelled') 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 2d10e6a4ed4..fee641e76c3 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -92,14 +92,14 @@ class LandedCostVoucher(Document): total_item_cost += item.get(based_on_field) for item in self.get('items'): - item.applicable_charges = flt(flt(item.get(based_on_field)) * flt(self.total_taxes_and_charges) / flt(total_item_cost), + item.applicable_charges = flt(flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), item.precision('applicable_charges')) total_charges += item.applicable_charges item_count += 1 - if total_charges != self.total_taxes_and_charges: - diff = self.total_taxes_and_charges - total_charges - self.get('items')[item_count - 1].applicable_charges += diff + if total_charges != self.total_taxes_and_charges: + diff = self.total_taxes_and_charges - total_charges + self.get('items')[item_count - 1].applicable_charges += diff def set_account_currency(self): company_currency = erpnext.get_company_currency(self.company) 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 cd4b33ece1b..144101c67d7 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 @@ -163,8 +163,8 @@ class TestLandedCostVoucher(unittest.TestCase): lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) - self.assertEqual(lcv.items[0].applicable_charges, 41.07) - self.assertEqual(lcv.items[2].applicable_charges, 41.08) + self.assertEqual(flt(lcv.items[0].applicable_charges, 2), 41.07) + self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08) def test_multiple_landed_cost_voucher_against_pr(self): pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", From bfc17e487cce0cdcb2aaee25c0f0e233773629ad Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Dec 2020 18:34:39 +0530 Subject: [PATCH 20/46] fix: Commonify code for Stock Entry --- erpnext/controllers/accounts_controller.py | 2 +- erpnext/controllers/taxes_and_totals.py | 33 ++++++++++ .../landed_cost_voucher.js | 65 ++----------------- .../landed_cost_voucher.py | 30 +-------- .../stock/doctype/stock_entry/stock_entry.js | 29 +++++---- .../stock/doctype/stock_entry/stock_entry.py | 27 +++++--- .../stock_entry_detail.json | 2 +- .../stock/landed_taxes_and_charges_common.js | 58 +++++++++++++++++ 8 files changed, 136 insertions(+), 110 deletions(-) create mode 100644 erpnext/stock/landed_taxes_and_charges_common.js diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 49c9f8c9d0f..6f688487548 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -442,7 +442,7 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(gl_dict.account) if gl_dict.account and self.doctype not in ["Journal Entry", - "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice"]: + "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice", "Stock Entry"]: self.validate_account_currency(gl_dict.account, account_currency) set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"), self.company_currency) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8dd2e5bacbd..76309f8799a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -10,6 +10,7 @@ from erpnext.controllers.accounts_controller import validate_conversion_rate, \ validate_taxes_and_charges, validate_inclusive_tax from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules +from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate class calculate_taxes_and_totals(object): def __init__(self, doc): @@ -758,3 +759,35 @@ def get_rounded_tax_amount(itemised_tax, precision): for taxes in itemised_tax.values(): for tax_account in taxes: taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) + +class init_landed_taxes_and_totals(object): + def __init__(self, doc): + self.doc = doc + self.tax_field = 'taxes' if self.doc.doctype == 'Landed Cost Voucher' else 'additional_costs' + self.set_account_currency() + self.set_exchange_rate() + self.set_amounts_in_company_currency() + + def set_account_currency(self): + company_currency = erpnext.get_company_currency(self.doc.company) + for d in self.doc.get(self.tax_field): + if not d.account_currency: + account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency') + d.account_currency = account_currency or company_currency + + def set_exchange_rate(self): + company_currency = erpnext.get_company_currency(self.doc.company) + for d in self.doc.get(self.tax_field): + if d.account_currency == company_currency: + d.exchange_rate = 1 + elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date: + d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account, + account_currency=d.account_currency, company=self.doc.company) + + if not d.exchange_rate: + frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) + + def set_amounts_in_company_currency(self): + for d in self.doc.get(self.tax_field): + d.amount = flt(d.amount, d.precision("amount")) + d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) \ No newline at end of file diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index c817f05e5a2..02ff8fa8587 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -1,6 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %}; frappe.provide("erpnext.stock"); @@ -29,19 +30,9 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ this.frm.add_fetch("receipt_document", "supplier", "supplier"); this.frm.add_fetch("receipt_document", "posting_date", "posting_date"); this.frm.add_fetch("receipt_document", "base_grand_total", "grand_total"); - - this.frm.set_query("expense_account", "taxes", function() { - return { - filters: { - "account_type": ['in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], - "company": me.frm.doc.company - } - }; - }); - }, - refresh: function(frm) { + refresh: function() { var help_content = `

    @@ -71,6 +62,11 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({
    `; set_field_options("landed_cost_help", help_content); + + if (this.frm.doc.company) { + let company_currency = frappe.get_doc(":Company", this.frm.doc.company).default_currency; + this.frm.set_currency_labels(["total_taxes_and_charges"], company_currency); + } }, get_items_from_purchase_receipts: function() { @@ -137,53 +133,6 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ cur_frm.script_manager.make(erpnext.stock.LandedCostVoucher); -frappe.ui.form.on('Landed Cost Voucher', { - set_account_currency: function(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - if (row.expense_account) { - frappe.db.get_value('Account', row.expense_account, 'account_currency', function(value) { - frappe.model.set_value(cdt, cdn, "account_currency", value.account_currency); - frm.events.set_exchange_rate(frm, cdt, cdn); - }); - } - }, - - onload: function(frm) { - let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; - frm.set_currency_labels(["total_taxes_and_charges"], company_currency); - }, - - set_exchange_rate: function(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; - - if (row.account_currency == company_currency) { - row.exchange_rate = 1; - } else if (!row.exchange_rate || row.exchange_rate == 1) { - frappe.call({ - method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate", - args: { - posting_date: frm.doc.posting_date, - account: row.expense_account, - account_currency: row.account_currency, - company: frm.doc.company - }, - callback: function(r) { - if (r.message) { - frappe.model.set_value(cdt, cdn, "exchange_rate", r.message); - } - } - }); - } - }, - - set_base_amount: function(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - frappe.model.set_value(cdt, cdn, "base_amount", - flt(flt(row.amount)*row.exchange_rate, precision("base_amount", row))); - } -}); - frappe.ui.form.on('Landed Cost Taxes and Charges', { expense_account: function(frm, cdt, cdn) { frm.events.set_account_currency(frm, cdt, cdn); 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 fee641e76c3..24bbc4046ba 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -9,7 +9,7 @@ from frappe.model.meta import get_field_precision from frappe.model.document import Document from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.accounts.doctype.account.account import get_account_currency -from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate +from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals class LandedCostVoucher(Document): def get_items_from_purchase_receipts(self): @@ -41,9 +41,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() self.validate_purchase_receipts() - self.set_account_currency() - self.set_exchange_rate() - self.set_amounts_in_company_currency() + init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): self.get_items_from_purchase_receipts() @@ -101,30 +99,6 @@ class LandedCostVoucher(Document): diff = self.total_taxes_and_charges - total_charges self.get('items')[item_count - 1].applicable_charges += diff - def set_account_currency(self): - company_currency = erpnext.get_company_currency(self.company) - for d in self.get('taxes'): - if not d.account_currency: - account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency') - d.account_currency = account_currency or company_currency - - def set_exchange_rate(self): - company_currency = erpnext.get_company_currency(self.company) - for d in self.get('taxes'): - if d.account_currency == company_currency: - d.exchange_rate = 1 - elif not d.exchange_rate or d.exchange_rate == 1 or self.posting_date: - d.exchange_rate = get_exchange_rate(self.posting_date, account=d.expense_account, - account_currency=d.account_currency, company=self.company) - - if not d.exchange_rate: - frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) - - def set_amounts_in_company_currency(self): - for d in self.get('taxes'): - d.amount = flt(d.amount, d.precision("amount")) - d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) - def validate_applicable_charges_for_item(self): based_on = self.distribute_charges_based_on.lower() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 98116ec1832..f3f5ac8b46e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1,6 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.stock"); +{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %}; + frappe.ui.form.on('Stock Entry', { setup: function(frm) { @@ -86,15 +88,6 @@ frappe.ui.form.on('Stock Entry', { } }); - frm.set_query("expense_account", "additional_costs", function() { - return { - query: "erpnext.controllers.queries.tax_account_query", - filters: { - "account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"], - "company": frm.doc.company - } - }; - }); frm.add_fetch("bom_no", "inspection_required", "inspection_required"); }, @@ -524,7 +517,7 @@ frappe.ui.form.on('Stock Entry', { }) ); } - + for (let i in frm.doc.items) { let item = frm.doc.items[i]; @@ -547,7 +540,7 @@ frappe.ui.form.on('Stock Entry', { calculate_total_additional_costs: function(frm) { const total_additional_costs = frappe.utils.sum( - (frm.doc.additional_costs || []).map(function(c) { return flt(c.amount); }) + (frm.doc.additional_costs || []).map(function(c) { return flt(c.base_amount); }) ); frm.set_value("total_additional_costs", @@ -716,8 +709,18 @@ var validate_sample_quantity = function(frm, cdt, cdn) { }; frappe.ui.form.on('Landed Cost Taxes and Charges', { - amount: function(frm) { - frm.events.calculate_amount(frm); + amount: function(frm, cdt, cdn) { + frm.events.set_base_amount(frm, cdt, cdn); + + // Adding this check because same table in used in LCV + // This causes an error if you try to post an LCV immediately after a Stock Entry + if (frm.doc.doctype == 'Stock Entry') { + frm.events.calculate_amount(frm); + } + }, + + expense_account: function(frm, cdt, cdn) { + frm.events.set_account_currency(frm, cdt, cdn); } }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index afdb54ceaa2..adaaff7b813 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -19,6 +19,7 @@ from frappe.model.mapper import get_mapped_doc from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError from erpnext.accounts.general_ledger import process_gl_map +from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals import json from six import string_types, itervalues, iteritems @@ -186,7 +187,7 @@ class StockEntry(StockController): and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1) amount = amount[0][0] if amount else 0 - additional_costs = frappe.db.sql(""" select ifnull(sum(sed.amount), 0) + additional_costs = frappe.db.sql(""" select ifnull(sum(sed.base_amount), 0) from `tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed where @@ -431,6 +432,7 @@ class StockEntry(StockController): def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate) + init_landed_taxes_and_totals(self) self.distribute_additional_costs() self.update_valuation_rate() self.set_total_incoming_outgoing_value() @@ -518,7 +520,7 @@ class StockEntry(StockController): if not any([d.item_code for d in self.items if d.t_warehouse]): self.additional_costs = [] - self.total_additional_costs = sum([flt(t.amount) for t in self.get("additional_costs")]) + self.total_additional_costs = sum([flt(t.base_amount) for t in self.get("additional_costs")]) if self.purpose in ("Repack", "Manufacture"): incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item]) @@ -698,7 +700,7 @@ class StockEntry(StockController): # SLE for target warehouse self.get_sle_for_target_warehouse(sl_entries, finished_item_row) - + # reverse sl entries if cancel if self.docstatus == 2: sl_entries.reverse() @@ -726,9 +728,9 @@ class StockEntry(StockController): sle.dependant_sle_voucher_detail_no = d.name elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse): sle.dependant_sle_voucher_detail_no = finished_item_row.name - + sl_entries.append(sle) - + def get_sle_for_target_warehouse(self, sl_entries, finished_item_row): for d in self.get('items'): if cstr(d.t_warehouse): @@ -758,13 +760,19 @@ class StockEntry(StockController): for d in self.get("items"): if d.t_warehouse: item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) - item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) + item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, { + "amount": 0.0, + "base_amount": 0.0 + }) multiply_based_on = d.basic_amount if total_basic_amount else d.qty - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \ flt(t.amount * multiply_based_on) / divide_based_on + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \ + flt(t.base_amount * multiply_based_on) / divide_based_on + if item_account_wise_additional_cost: for d in self.get("items"): for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})): @@ -775,7 +783,8 @@ class StockEntry(StockController): "against": d.expense_account, "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": amount + "credit_in_account_currency": flt(amount["amount"]), + "credit": flt(amount["base_amount"]) }, item=d)) gl_entries.append(self.get_gl_dict({ @@ -783,7 +792,7 @@ class StockEntry(StockController): "against": account, "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": -1 * amount # put it as negative credit instead of debit purposefully + "credit": -1 * amount['base_amount'] # put it as negative credit instead of debit purposefully }, item=d)) return process_gl_map(gl_entries) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 6fe60298eeb..b4c22842735 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -526,7 +526,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-23 17:55:03.384138", + "modified": "2020-09-25 17:55:03.384138", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/landed_taxes_and_charges_common.js b/erpnext/stock/landed_taxes_and_charges_common.js new file mode 100644 index 00000000000..8ad08808bc6 --- /dev/null +++ b/erpnext/stock/landed_taxes_and_charges_common.js @@ -0,0 +1,58 @@ +let document_list = ['Landed Cost Voucher', 'Stock Entry']; + +document_list.forEach((doctype) => { + frappe.ui.form.on(doctype, { + refresh: function(frm) { + let tax_field = frm.doc.doctype == 'Landed Cost Voucher' ? 'taxes' : 'additional_costs'; + frm.set_query("expense_account", tax_field, function() { + return { + filters: { + "account_type": ['in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], + "company": frm.doc.company + } + }; + }); + }, + + set_account_currency: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.expense_account) { + frappe.db.get_value('Account', row.expense_account, 'account_currency', function(value) { + frappe.model.set_value(cdt, cdn, "account_currency", value.account_currency); + frm.events.set_exchange_rate(frm, cdt, cdn); + }); + } + }, + + set_exchange_rate: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + + if (row.account_currency == company_currency) { + row.exchange_rate = 1; + } else if (!row.exchange_rate || row.exchange_rate == 1) { + frappe.call({ + method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate", + args: { + posting_date: frm.doc.posting_date, + account: row.expense_account, + account_currency: row.account_currency, + company: frm.doc.company + }, + callback: function(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, "exchange_rate", r.message); + } + } + }); + } + }, + + set_base_amount: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, "base_amount", + flt(flt(row.amount)*row.exchange_rate, precision("base_amount", row))); + } + }); +}); + From 4f8da4c31312d55036c19a160bec069bd3ffd8cd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 26 Dec 2020 11:37:51 +0530 Subject: [PATCH 21/46] fix: Test Cases --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 2 +- .../landed_cost_taxes_and_charges.json | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index c4412749080..fc849647d40 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -149,7 +149,7 @@ class GLEntry(Document): account_currency = get_account_currency(self.account) if not self.account_currency: - self.account_currency = company_currency + self.account_currency = account_currency or company_currency if account_currency != self.account_currency: frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}") diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 0861224de47..4fcdb4c10cc 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -41,16 +41,14 @@ "in_list_view": 1, "label": "Expense Account", "mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", - "options": "Account", - "reqd": 1 + "options": "Account" }, { "fieldname": "account_currency", "fieldtype": "Link", "label": "Account Currency", "options": "Currency", - "read_only": 1, - "reqd": 1 + "read_only": 1 }, { "fieldname": "exchange_rate", @@ -63,14 +61,13 @@ "fieldtype": "Currency", "label": "Base Amount", "options": "Company:company:default_currency", - "read_only": 1, - "reqd": 1 + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-04 00:22:14.373312", + "modified": "2020-12-26 01:07:23.233604", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", From 7c300859ed11ae860adcb90b8bde403097c289f6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 26 Dec 2020 20:14:51 +0530 Subject: [PATCH 22/46] fix: Test Cases --- erpnext/controllers/accounts_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cfb2949e7f1..7ac4cfeef7d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -110,12 +110,12 @@ class AccountsController(TransactionBase): self.set_inter_company_account() validate_regional(self) - + validate_einvoice_fields(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) - + def before_cancel(self): validate_einvoice_fields(self) @@ -450,6 +450,8 @@ class AccountsController(TransactionBase): if gl_dict.account and self.doctype not in ["Journal Entry", "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice", "Stock Entry"]: self.validate_account_currency(gl_dict.account, account_currency) + + if gl_dict.account and self.doctype not in ["Journal Entry", "Period Closing Voucher", "Payment Entry"]: set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"), self.company_currency) From b8aeb9ea88bc62d04945af6936b5320bba41470c Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Jan 2021 12:16:25 +0530 Subject: [PATCH 23/46] fix: Indentation and missing semi-colon - Fix incorrect indentation after merge conflict - Sider --- erpnext/stock/dashboard/item_dashboard.js | 2 +- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 20415bc07e8..d3c442d3039 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -163,7 +163,7 @@ erpnext.stock.ItemDashboard = Class.extend({ max_count: max_count, can_write:can_write, show_item: show_item || false - } + }; }, get_capacity_dashboard_data: function(data) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1cb71c2db64..ecdd396f3ad 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -87,7 +87,7 @@ class StockEntry(StockController): self.validate_serialized_batch() self.set_actual_qty() self.calculate_rate_and_amount() - self.validate_putaway_capacity() + self.validate_putaway_capacity() def on_submit(self): self.update_stock_ledger() From 957615bec83e6d4c13eb5dd72fade8c3a3d6ed5b Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 18 Jan 2021 23:47:24 +0530 Subject: [PATCH 24/46] fix: Stricter validations - Validation for overreceipt on Purchase Invoice, Stock Entry, Purchase Receipt & Stock Reconciliation - Every incoming stock transaction must be checked to avoid overcapacity - However application of rule and splitting only on certain doctypes - Validate capacity < stock balance on save in putaway rule, irrespective --- erpnext/controllers/stock_controller.py | 66 ++++++++++++------- .../purchase_receipt/purchase_receipt.py | 4 +- .../doctype/putaway_rule/putaway_rule.py | 4 +- .../stock_reconciliation.py | 1 + 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index af26d7b6689..e0fcf47365f 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -396,38 +396,54 @@ class StockController(AccountsController): def validate_putaway_capacity(self): # if over receipt is attempted while 'apply putaway rule' is disabled # and if rule was applied on the transaction, validate it. - from erpnext.stock.doctype.putaway_rule.putaway_rule import get_putaway_capacity - valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry") - rule_applied = any(item.get("putaway_rule") for item in self.get("items")) + from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity + valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice", + "Stock Reconciliation") - if valid_doctype and rule_applied and not self.apply_putaway_rule: + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: + valid_doctype = False + + if valid_doctype: rule_map = defaultdict(dict) for item in self.get("items"): - if item.get("putaway_rule"): - rule = item.get("putaway_rule") - disabled = frappe.db.get_value("Putaway Rule", rule, "disable") - if disabled: return # dont validate for disabled rule - stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) - warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" - if not rule_map[rule]: - rule_map[rule]["warehouse"] = item.get(warehouse_field) - rule_map[rule]["item"] = item.get("item_code") - rule_map[rule]["qty_put"] = 0 - rule_map[rule]["capacity"] = get_putaway_capacity(rule) - rule_map[rule]["qty_put"] += flt(stock_qty) + warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" + rule = frappe.db.get_value("Putaway Rule", + { + "item_code": item.get("item_code"), + "warehouse": item.get(warehouse_field) + }, + ["name", "disable"], as_dict=True) + if rule: + if rule.get("disabled"): continue # dont validate for disabled rule + + if self.doctype == "Stock Reconciliation": + stock_qty = flt(item.qty) + else: + stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) + + rule_name = rule.get("name") + if not rule_map[rule_name]: + rule_map[rule_name]["warehouse"] = item.get(warehouse_field) + rule_map[rule_name]["item"] = item.get("item_code") + rule_map[rule_name]["qty_put"] = 0 + rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name) + rule_map[rule_name]["qty_put"] += flt(stock_qty) for rule, values in rule_map.items(): if flt(values["qty_put"]) > flt(values["capacity"]): - message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ - .format( - frappe.bold(values["qty_put"]), frappe.bold(values["item"]), - frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) - ) - message += "

    " - rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) - message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) + message = self.prepare_over_receipt_message(rule, values) frappe.throw(msg=message, title=_("Over Receipt")) - return rule_map + + def prepare_over_receipt_message(self, rule, values): + message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \ + .format( + frappe.bold(values["qty_put"]), frappe.bold(values["item"]), + frappe.bold(values["warehouse"]), frappe.bold(values["capacity"]) + ) + message += "

    " + rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) + message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) + return message def repost_future_sle_and_gle(self): args = frappe._dict({ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 5e6a3f22b5e..61c531067ca 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -86,7 +86,7 @@ class PurchaseReceipt(BuyingController): def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule - if self.get("items") and self.apply_putaway_rule: + if self.get("items") and self.apply_putaway_rule and not self.get("is_return"): apply_putaway_rule(self.doctype, self.get("items"), self.company) def validate(self): @@ -415,7 +415,7 @@ class PurchaseReceipt(BuyingController): if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + "\n".join(warehouse_with_no_account)) - + return process_gl_map(gl_entries) def get_asset_gl_entry(self, gl_entries): diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 56752583fa7..ea26caced04 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -44,7 +44,7 @@ class PutawayRule(Document): stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) - if flt(self.stock_capacity) < flt(balance_qty) and self.get('__islocal'): + if flt(self.stock_capacity) < flt(balance_qty): frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") .format(self.item_code, frappe.bold(balance_qty), stock_uom), title=_("Insufficient Capacity")) @@ -56,7 +56,7 @@ class PutawayRule(Document): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) @frappe.whitelist() -def get_putaway_capacity(rule): +def get_available_putaway_capacity(rule): stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, ["stock_capacity", "item_code", "warehouse"]) balance_qty = get_stock_balance(item_code, warehouse, nowdate()) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 5b40292ea8f..f0a90f9754b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -30,6 +30,7 @@ class StockReconciliation(StockController): self.validate_data() self.validate_expense_account() self.set_total_qty_and_amount() + self.validate_putaway_capacity() if self._action=="submit": self.make_batches('warehouse') From c908deea619dc7aa504e8d8814ca094eac6655c5 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 22 Jan 2021 16:10:01 +0530 Subject: [PATCH 25/46] fix: Patch to Add Naming Series to old projects --- erpnext/patches.txt | 1 + .../add_naming_series_to_old_projects.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 erpnext/patches/v13_0/add_naming_series_to_old_projects.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 62e9b5cb6ac..2f5643b65db 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -746,3 +746,4 @@ erpnext.patches.v13_0.update_returned_qty_in_pr_dn erpnext.patches.v13_0.update_project_template_tasks erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.convert_qi_parameter_to_link_field +erpnext.patches.v13_0.add_naming_series_to_old_projects diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py new file mode 100644 index 00000000000..f74f5410366 --- /dev/null +++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals +import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter, delete_property_setter + +def execute(): + frappe.reload_doc("projects", "doctype", "project") + projects = frappe.db.get_all("Project", + fields=["name", "naming_series", "modified"], + filters={ + "naming_series": ["is", "not set"] + }, + order_by="timestamp(modified) asc") + + # disable set only once as the old docs must be saved + # (to bypass 'Cant change naming series' validation on save) + make_property_setter("Project", "naming_series", "set_only_once", 0, "Check") + + for entry in projects: + # need to save the doc so that users can edit old projects + doc = frappe.get_doc("Project", entry.name) + if not doc.naming_series: + doc.naming_series = "PROJ-.####" + doc.save() + + delete_property_setter("Project", "set_only_once", "naming_series") + frappe.db.commit() From 7ba60570e71528c8e5c9b3ca1fa0e8a386908e72 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 22 Jan 2021 16:55:06 +0530 Subject: [PATCH 26/46] fix: Spaces to Tabs --- erpnext/patches/v13_0/add_naming_series_to_old_projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py index f74f5410366..79b67533ed6 100644 --- a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py +++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py @@ -17,8 +17,8 @@ def execute(): for entry in projects: # need to save the doc so that users can edit old projects - doc = frappe.get_doc("Project", entry.name) - if not doc.naming_series: + doc = frappe.get_doc("Project", entry.name) + if not doc.naming_series: doc.naming_series = "PROJ-.####" doc.save() From b0ef8fb7c5bada147af65a479eb64c674f3e2dd7 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 25 Jan 2021 21:00:24 +0530 Subject: [PATCH 27/46] fix: Always show QI Template in Item --- erpnext/stock/doctype/item/item.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index d07b3dc4fef..fcf7c2608ef 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -106,9 +106,9 @@ "item_tax_section_break", "taxes", "inspection_criteria", + "quality_inspection_template", "inspection_required_before_purchase", "inspection_required_before_delivery", - "quality_inspection_template", "manufacturing", "default_bom", "is_sub_contracted_item", @@ -814,7 +814,6 @@ "label": "Inspection Required before Delivery" }, { - "depends_on": "eval:(doc.inspection_required_before_purchase || doc.inspection_required_before_delivery)", "fieldname": "quality_inspection_template", "fieldtype": "Link", "label": "Quality Inspection Template", @@ -1069,7 +1068,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2020-08-07 14:24:58.384992", + "modified": "2021-01-25 20:49:50.222976", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1131,4 +1130,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} +} \ No newline at end of file From 1991ba7910933e79d3693871cadff463023fa871 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 25 Jan 2021 16:52:10 +0100 Subject: [PATCH 28/46] feat: temporary against account (#24086) * feat: add temporaray against account nmber to datev settings * feat: use temporary against account in datev report * test: add temp against account number to test data * test: include temp against aaccount in filters --- .../datev_settings/datev_settings.json | 18 ++++++++---- erpnext/regional/report/datev/datev.py | 29 +++++-------------- erpnext/regional/report/datev/test_datev.py | 6 ++-- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json index 713e8e34ef6..f60de4c8af4 100644 --- a/erpnext/regional/doctype/datev_settings/datev_settings.json +++ b/erpnext/regional/doctype/datev_settings/datev_settings.json @@ -7,13 +7,14 @@ "engine": "InnoDB", "field_order": [ "client", - "account_number_length", - "column_break_2", "client_number", - "section_break_4", + "column_break_2", + "consultant_number", "consultant", + "section_break_4", + "account_number_length", "column_break_6", - "consultant_number" + "temporary_against_account_number" ], "fields": [ { @@ -66,10 +67,17 @@ "fieldtype": "Int", "label": "Account Number Length", "reqd": 1 + }, + { + "allow_in_quick_entry": 1, + "fieldname": "temporary_against_account_number", + "fieldtype": "Data", + "label": "Temporary Against Account Number", + "reqd": 1 } ], "links": [], - "modified": "2020-11-05 17:52:11.674329", + "modified": "2020-11-19 19:00:09.088816", "modified_by": "Administrator", "module": "Regional", "name": "DATEV Settings", diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 1e39c57786c..cbc94789874 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -96,6 +96,8 @@ def execute(filters=None): """Entry point for frappe.""" data = [] if filters and validate(filters): + fn = 'temporary_against_account_number' + filters[fn] = frappe.get_value('DATEV Settings', filters.get('company'), fn) data = get_transactions(filters, as_dict=0) return COLUMNS, data @@ -156,11 +158,11 @@ def get_transactions(filters, as_dict=1): case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', /* account number or, if empty, party account number */ - coalesce(acc.account_number, acc_pa.account_number) as 'Konto', + acc.account_number as 'Konto', /* against number or, if empty, party against number */ - coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', - + %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', + gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', LEFT(gl.remarks, 60) as 'Buchungstext', @@ -171,27 +173,10 @@ def get_transactions(filters, as_dict=1): FROM `tabGL Entry` gl - /* Statistisches Konto (Debitoren/Kreditoren) */ - left join `tabParty Account` pa - on gl.against = pa.parent - and gl.company = pa.company - /* Kontonummer */ left join `tabAccount` acc on gl.account = acc.name - /* Gegenkonto-Nummer */ - left join `tabAccount` acc_against - on gl.against = acc_against.name - - /* Statistische Kontonummer */ - left join `tabAccount` acc_pa - on pa.account = acc_pa.name - - /* Statistische Gegenkonto-Nummer */ - left join `tabAccount` acc_against_pa - on pa.account = acc_against_pa.name - WHERE gl.company = %(company)s AND DATE(gl.posting_date) >= %(from_date)s AND DATE(gl.posting_date) <= %(to_date)s @@ -347,7 +332,9 @@ def download_datev_csv(filters): coa = frappe.get_value('Company', company, 'chart_of_accounts') filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') - filters['account_number_length'] = frappe.get_value('DATEV Settings', company, 'account_number_length') + datev_settings = frappe.get_doc('DATEV Settings', company) + filters['account_number_length'] = datev_settings.account_number_length + filters['temporary_against_account_number'] = datev_settings.temporary_against_account_number transactions = get_transactions(filters) account_names = get_account_names(filters) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index 9529923a73b..59b878e94a5 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -126,7 +126,8 @@ def make_datev_settings(company): "doctype": "DATEV Settings", "client": company.name, "client_number": "12345", - "consultant_number": "67890" + "consultant_number": "67890", + "temporary_against_account_number": "9999" }).insert() @@ -137,7 +138,8 @@ class TestDatev(TestCase): self.filters = { "company": self.company.name, "from_date": today(), - "to_date": today() + "to_date": today(), + "temporary_against_account_number": "9999" } make_datev_settings(self.company) From 7a4db2214ae1ff639074a4656e32bad353ccc8cd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 26 Jan 2021 10:10:35 +0530 Subject: [PATCH 29/46] fix: Division by 0 error in security exposure reports --- .../applicant_wise_loan_security_exposure.py | 30 ++++++++++--------- .../loan_security_exposure.py | 27 +++++++++-------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index f280402ce9b..ab586bc09c4 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -43,22 +43,24 @@ def get_data(filters): currency = erpnext.get_company_currency(filters.get('company')) for key, qty in iteritems(pledge_values): - row = {} - current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) - valid_upto = loan_security_details.get(key[1], {}).get('valid_upto') + if qty: + row = {} + current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) + valid_upto = loan_security_details.get(key[1], {}).get('valid_upto') - row.update(loan_security_details.get(key[1])) - row.update({ - 'applicant_type': applicant_type_map.get(key[0]), - 'applicant_name': key[0], - 'total_qty': qty, - 'current_value': current_value, - 'price_valid_upto': valid_upto, - 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2), - 'currency': currency - }) + row.update(loan_security_details.get(key[1])) + row.update({ + 'applicant_type': applicant_type_map.get(key[0]), + 'applicant_name': key[0], + 'total_qty': qty, + 'current_value': current_value, + 'price_valid_upto': valid_upto, + 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \ + else 0.0, + 'currency': currency + }) - data.append(row) + data.append(row) return data diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index ff88052df5b..adc8013c686 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -40,21 +40,22 @@ def get_data(filters): currency = erpnext.get_company_currency(filters.get('company')) for security, value in iteritems(current_pledges): - row = {} - current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0)) - valid_upto = loan_security_details.get(security, {}).get('valid_upto') + if value.get('qty'): + row = {} + current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0)) + valid_upto = loan_security_details.get(security, {}).get('valid_upto') - row.update(loan_security_details.get(security)) - row.update({ - 'total_qty': value.get('qty'), - 'current_value': current_value, - 'price_valid_upto': valid_upto, - 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), - 'pledged_applicant_count': value.get('applicant_count'), - 'currency': currency - }) + row.update(loan_security_details.get(security)) + row.update({ + 'total_qty': value.get('qty'), + 'current_value': current_value, + 'price_valid_upto': valid_upto, + 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), + 'pledged_applicant_count': value.get('applicant_count'), + 'currency': currency + }) - data.append(row) + data.append(row) return data From a0d61524c186b07c0fd0ccaf4423087b385f717c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 26 Jan 2021 17:05:00 +0530 Subject: [PATCH 30/46] fix: Add distribute manually option in LCV --- .../landed_cost_item/landed_cost_item.json | 57 ++++++++++---- .../landed_cost_voucher.js | 30 ++++---- .../landed_cost_voucher.json | 76 ++++++++++++++----- .../landed_cost_voucher.py | 8 +- .../purchase_receipt/purchase_receipt.py | 8 +- .../stock/landed_taxes_and_charges_common.js | 4 + 6 files changed, 133 insertions(+), 50 deletions(-) diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json index b24d621c317..c77b993167a 100644 --- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json +++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2013-02-22 01:28:02", "doctype": "DocType", "document_type": "Document", @@ -29,6 +30,8 @@ "options": "Item", "read_only": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -41,6 +44,8 @@ "print_width": "300px", "read_only": 1, "reqd": 1, + "show_days": 1, + "show_seconds": 1, "width": "120px" }, { @@ -50,7 +55,9 @@ "no_copy": 1, "options": "Purchase Invoice\nPurchase Receipt", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "receipt_document", @@ -59,25 +66,33 @@ "no_copy": 1, "options": "receipt_document_type", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, "label": "Qty", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rate", "fieldtype": "Currency", "label": "Rate", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amount", @@ -88,14 +103,19 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "applicable_charges", "fieldtype": "Currency", "in_list_view": 1, "label": "Applicable Charges", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only_depends_on": "eval:parent.distribute_charges_based_on != 'Distribute Manually'", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipt_item", @@ -104,22 +124,30 @@ "label": "Purchase Receipt Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions" + "label": "Accounting Dimensions", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -128,12 +156,15 @@ "fieldtype": "Check", "hidden": 1, "label": "Is Fixed Asset", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "idx": 1, "istable": 1, - "modified": "2020-09-18 17:26:09.703215", + "links": [], + "modified": "2021-01-25 23:09:23.322282", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Item", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index 02ff8fa8587..1abbc35334f 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -101,25 +101,27 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ var me = this; if(this.frm.doc.taxes.length) { - var total_item_cost = 0.0; var based_on = this.frm.doc.distribute_charges_based_on.toLowerCase(); - $.each(this.frm.doc.items || [], function(i, d) { - total_item_cost += flt(d[based_on]) - }); - var total_charges = 0.0; - $.each(this.frm.doc.items || [], function(i, item) { - item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost) - item.applicable_charges = flt(item.applicable_charges, precision("applicable_charges", item)) - total_charges += item.applicable_charges - }); + if (based_on != 'distribute manually') { + $.each(this.frm.doc.items || [], function(i, d) { + total_item_cost += flt(d[based_on]) + }); - if (total_charges != this.frm.doc.total_taxes_and_charges){ - var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges) - this.frm.doc.items.slice(-1)[0].applicable_charges += diff + var total_charges = 0.0; + $.each(this.frm.doc.items || [], function(i, item) { + item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost) + item.applicable_charges = flt(item.applicable_charges, precision("applicable_charges", item)) + total_charges += item.applicable_charges + }); + + if (total_charges != this.frm.doc.total_taxes_and_charges){ + var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges) + this.frm.doc.items.slice(-1)[0].applicable_charges += diff + } + refresh_field("items"); } - refresh_field("items"); } }, distribute_charges_based_on: function (frm) { diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json index 6a7994ba9e5..059f925184b 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -34,7 +34,9 @@ "options": "MAT-LCV-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -44,24 +46,32 @@ "label": "Company", "options": "Company", "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipts", "fieldtype": "Table", "label": "Purchase Receipts", "options": "Landed Cost Purchase Receipt", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "purchase_receipt_items", "fieldtype": "Section Break", - "label": "Purchase Receipt Items" + "label": "Purchase Receipt Items", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "get_items_from_purchase_receipts", "fieldtype": "Button", - "label": "Get Items From Purchase Receipts" + "label": "Get Items From Purchase Receipts", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "items", @@ -69,23 +79,31 @@ "label": "Purchase Receipt Items", "no_copy": 1, "options": "Landed Cost Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_break1", "fieldtype": "Section Break", - "label": "Applicable Charges" + "label": "Applicable Charges", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", "fieldtype": "Table", "label": "Taxes and Charges", "options": "Landed Cost Taxes and Charges", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_9", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -93,18 +111,24 @@ "label": "Total Taxes and Charges (Company Currency)", "options": "Company:company:default_currency", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "distribute_charges_based_on", "fieldtype": "Select", "label": "Distribute Charges Based On", - "options": "Qty\nAmount", - "reqd": 1 + "options": "Qty\nAmount\nDistribute Manually", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -113,39 +137,51 @@ "no_copy": 1, "options": "Landed Cost Voucher", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_break2", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "landed_cost_help", "fieldtype": "HTML", - "label": "Landed Cost Help" + "label": "Landed Cost Help", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", "fieldname": "posting_date", "fieldtype": "Date", "label": "Posting Date", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_5", "fieldtype": "Section Break", - "hide_border": 1 + "hide_border": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "icon-usd", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-12-13 23:18:47.442466", + "modified": "2021-01-25 23:07:30.468423", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Voucher", 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 24bbc4046ba..69a8bf19d34 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -80,7 +80,7 @@ class LandedCostVoucher(Document): self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")]) def set_applicable_charges_on_item(self): - if self.get('taxes'): + if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually': total_item_cost = 0.0 total_charges = 0.0 item_count = 0 @@ -102,7 +102,11 @@ class LandedCostVoucher(Document): def validate_applicable_charges_for_item(self): based_on = self.distribute_charges_based_on.lower() - total = sum([flt(d.get(based_on)) for d in self.get("items")]) + if based_on != 'distribute manually': + total = sum([flt(d.get(based_on)) for d in self.get("items")]) + else: + # consider for proportion while distributing manually + total = sum([flt(d.get('applicable_charges')) for d in self.get("items")]) if not total: frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index fd9f5b851ea..550c849c5d1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -731,7 +731,13 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) - based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) + + #Use amount field for total item cost for manually cost distributed LCVs + if landed_cost_voucher_doc.distribute_charges_based_on == 'Distribute Manually': + based_on_field = 'amount' + else: + based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) + total_item_cost = 0 for item in landed_cost_voucher_doc.items: diff --git a/erpnext/stock/landed_taxes_and_charges_common.js b/erpnext/stock/landed_taxes_and_charges_common.js index 8ad08808bc6..f3f61963a88 100644 --- a/erpnext/stock/landed_taxes_and_charges_common.js +++ b/erpnext/stock/landed_taxes_and_charges_common.js @@ -30,7 +30,9 @@ document_list.forEach((doctype) => { if (row.account_currency == company_currency) { row.exchange_rate = 1; + frm.set_df_property('taxes', 'hidden', 1, row.name, 'exchange_rate'); } else if (!row.exchange_rate || row.exchange_rate == 1) { + frm.set_df_property('taxes', 'hidden', 0, row.name, 'exchange_rate'); frappe.call({ method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate", args: { @@ -46,6 +48,8 @@ document_list.forEach((doctype) => { } }); } + + frm.refresh_field('taxes'); }, set_base_amount: function(frm, cdt, cdn) { From dade7a45839beb8995c3f0390ad09c8f165bedc7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 26 Jan 2021 21:11:55 +0530 Subject: [PATCH 31/46] fix: Translation Syntax --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 1dd235dc64f..b0a864f76cd 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -159,8 +159,8 @@ class GLEntry(Document): if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \ and self.cost_center and _check_is_group(): - frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot - be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) + frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format( + self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) def validate_party(self): validate_party_frozen_disabled(self.party_type, self.party) From 5ba2f5835595691a84c45883dc14fea38f21d8a0 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 27 Jan 2021 12:34:08 +0530 Subject: [PATCH 32/46] fix: e_invoice print format not showing other charges (#24474) --- erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index 9827e00b71b..8eef2adce3e 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -152,7 +152,7 @@ {{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }} {{ frappe.utils.fmt_money(0, None, "INR") }} {{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }} - {{ frappe.utils.fmt_money(0, None, "INR") }} + {{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }} {{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }} {{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }} From 907e2990d064957b336717a77d2013ac5718c564 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 27 Jan 2021 15:34:42 +0530 Subject: [PATCH 33/46] fix: remove function to get earning deduction components (#24465) * fix: remove function to get earning deduction components * fix: slider --- .../additional_salary/additional_salary.js | 3 +-- .../employee_incentive/employee_incentive.js | 20 ++++++++++--------- .../salary_structure/salary_structure.js | 2 -- .../salary_structure/salary_structure.py | 19 ------------------ 4 files changed, 12 insertions(+), 32 deletions(-) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index d20c98c0984..d1ed91fac70 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -53,8 +53,7 @@ frappe.ui.form.on('Additional Salary', { if (!frm.doc.company) return; frm.set_query("salary_component", function() { return { - query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {type: "earning", company: frm.doc.company} + filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} }; }); }, diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js index 182ce0f83a6..b2809b164a0 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js @@ -10,15 +10,7 @@ frappe.ui.form.on('Employee Incentive', { } }; }); - - if (!frm.doc.company) return; - frm.set_query("salary_component", function() { - return { - query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", - filters: {type: "earning", company: frm.doc.company} - }; - }); - + frm.trigger('set_earning_component'); }, employee: function(frm) { @@ -45,11 +37,21 @@ frappe.ui.form.on('Employee Incentive', { callback: function(data) { if (data.message) { frm.set_value("company", data.message.company); + frm.trigger('set_earning_component'); } } }); }, + set_earning_component: function(frm) { + if (!frm.doc.company) return; + frm.set_query("salary_component", function() { + return { + filters: {type: "earning", company: frm.doc.company} + }; + }); + }, + get_employee_currency: function(frm) { frappe.call({ method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index 6c7b382264f..1378bf0b913 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -58,13 +58,11 @@ frappe.ui.form.on('Salary Structure', { if(!frm.doc.company) return; frm.set_query("salary_component", "earnings", function() { return { - query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", filters: {type: "earning", company: frm.doc.company} }; }); frm.set_query("salary_component", "deductions", function() { return { - query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components", filters: {type: "deduction", company: frm.doc.company} }; }); diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index e71803172c5..17120815504 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -207,22 +207,3 @@ def get_employees(salary_structure): return list(set([d.employee for d in employees])) -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters): - if len(filters) < 2: - return {} - - return frappe.db.sql(""" - select t1.salary_component - from `tabSalary Component` t1, `tabSalary Component Account` t2 - where (t1.name = t2.parent - and t1.type = %(type)s - and t2.company = %(company)s) - or (t1.type = %(type)s - and t1.statistical_component = 1) - order by salary_component - """,{ - "type": filters['type'], - "company": filters['company'] - }) From bc370b3ee586d97c2fd5c9db74a0718d4fff2d10 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 28 Jan 2021 11:44:26 +0530 Subject: [PATCH 34/46] chore: Set Stock Entry Form Indicators in setup - Makes it easier to override via customisations - Style consistency with other forms that set indicator in setup as well --- .../stock/doctype/stock_entry/stock_entry.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index bd2fce8bef1..cb6cab3341a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -5,6 +5,14 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Stock Entry', { setup: function(frm) { + frm.set_indicator_formatter('item_code', function(doc) { + if (!doc.s_warehouse) { + return 'blue'; + } else { + return (doc.qty<=doc.actual_qty) ? 'green' : 'orange' + } + }); + frm.set_query('work_order', function() { return { filters: [ @@ -779,15 +787,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } } - this.frm.set_indicator_formatter('item_code', - function(doc) { - if (!doc.s_warehouse) { - return 'blue'; - } else { - return (doc.qty<=doc.actual_qty) ? "green" : "orange" - } - }) - this.frm.add_fetch("purchase_order", "supplier", "supplier"); frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' } From 27bac2aaecfc831d899903f886a1feb3de357b91 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 28 Jan 2021 12:04:14 +0530 Subject: [PATCH 35/46] fix: Missing semi-colon --- erpnext/stock/doctype/stock_entry/stock_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index cb6cab3341a..bc1d089899f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -9,7 +9,7 @@ frappe.ui.form.on('Stock Entry', { if (!doc.s_warehouse) { return 'blue'; } else { - return (doc.qty<=doc.actual_qty) ? 'green' : 'orange' + return (doc.qty<=doc.actual_qty) ? 'green' : 'orange'; } }); From bd9bdc5797d859988460a0e359205a12b89fe207 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 28 Jan 2021 12:05:57 +0530 Subject: [PATCH 36/46] feat: Batch wise item pricing (#24470) * feat: Batch wise item pricing * fix: Rate for items with no batch * fix: Add test case for batch pricing * fix: Sider Issues * fix: Add item filter for batch --- erpnext/public/js/controllers/buying.js | 4 + erpnext/public/js/controllers/transaction.js | 6 + erpnext/selling/sales_common.js | 4 + erpnext/stock/doctype/batch/test_batch.py | 70 ++++++++++- .../stock/doctype/item_price/item_price.js | 8 ++ .../stock/doctype/item_price/item_price.json | 116 ++++++++++++++---- .../stock/doctype/item_price/item_price.py | 5 +- erpnext/stock/get_item_details.py | 5 +- 8 files changed, 187 insertions(+), 31 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 0c6bcad7219..a2a723dd774 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -195,6 +195,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ this._super(doc, cdt, cdn); }, + batch_no: function(doc, cdt, cdn) { + this._super(doc, cdt, cdn); + }, + received_qty: function(doc, cdt, cdn) { this.calculate_accepted_qty(doc, cdt, cdn) }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 26280998f9c..77318332d25 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1099,6 +1099,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + batch_no: function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + this.apply_price_list(item, true); + }, + toggle_conversion_factor: function(item) { // toggle read only property for conversion factor field if the uom and stock uom are same if(this.frm.get_field('items').grid.fields_map.conversion_factor) { @@ -1403,6 +1408,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ "pricing_rules": d.pricing_rules, "warehouse": d.warehouse, "serial_no": d.serial_no, + "batch_no": d.batch_no, "price_list_rate": d.price_list_rate, "conversion_factor": d.conversion_factor || 1.0 }); diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 7f00fca8f05..ce084646e15 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -399,6 +399,10 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ } }, + batch_no: function(doc, cdt, cdn) { + this._super(doc, cdt, cdn); + }, + qty: function(doc, cdt, cdn) { this._super(doc, cdt, cdn); diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index e41f1a8aaaf..97f85bafd95 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -8,6 +8,8 @@ import unittest from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no from frappe.utils import cint, flt +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.stock.get_item_details import get_item_details class TestBatch(unittest.TestCase): def test_item_has_batch_enabled(self): @@ -182,7 +184,7 @@ class TestBatch(unittest.TestCase): stock_entry.cancel() current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) self.assertEqual(current_batch_qty, existing_batch_qty) - + @classmethod def make_new_batch_and_entry(cls, item_name, batch_name, warehouse): '''Make a new stock entry for given target warehouse and batch name of item''' @@ -252,6 +254,72 @@ class TestBatch(unittest.TestCase): return batch + def test_batch_wise_item_price(self): + if not frappe.db.get_value('Item', '_Test Batch Price Item'): + frappe.get_doc({ + 'doctype': 'Item', + 'is_stock_item': 1, + 'item_code': '_Test Batch Price Item', + 'item_group': 'Products', + 'has_batch_no': 1, + 'create_new_batch': 1 + }).insert(ignore_permissions=True) + + batch1 = create_batch('_Test Batch Price Item', 200, 1) + batch2 = create_batch('_Test Batch Price Item', 300, 1) + batch3 = create_batch('_Test Batch Price Item', 400, 0) + + args = frappe._dict({ + "item_code": "_Test Batch Price Item", + "company": "_Test Company with perpetual inventory", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "customer": "_Test Customer", + "name": None + }) + + #test price for batch1 + args.update({'batch_no': batch1}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 200) + + #test price for batch2 + args.update({'batch_no': batch2}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 300) + + #test price for batch3 + args.update({'batch_no': batch3}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 400) + +def create_batch(item_code, rate, create_item_price_for_batch): + pi = make_purchase_invoice(company="_Test Company with perpetual inventory", + warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, + expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) + + batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) + + if not create_item_price_for_batch: + create_price_list_for_batch(item_code, None, rate) + else: + create_price_list_for_batch(item_code, batch, rate) + + return batch + +def create_price_list_for_batch(item_code, batch, rate): + frappe.get_doc({ + 'doctype': 'Item Price', + 'item_code': '_Test Batch Price Item', + 'price_list': '_Test Price List', + 'batch_no': batch, + 'price_list_rate': rate + }).insert() + def make_new_batch(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index 2729f4b15e2..e4db0480db0 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -15,5 +15,13 @@ frappe.ui.form.on("Item Price", { frm.set_df_property("bulk_import_help", "options", '' + __("Import in Bulk") + ''); + + frm.set_query('batch_no', function() { + return { + filters: { + 'item': frm.doc.item_code + } + } + }); } }); diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 5f62381f8b3..83177b372ad 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -18,6 +18,7 @@ "price_list", "customer", "supplier", + "batch_no", "column_break_3", "buying", "selling", @@ -47,31 +48,41 @@ "oldfieldtype": "Select", "options": "Item", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM" + "options": "UOM", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "description": "Quantity that must be bought or sold per UOM", "fieldname": "packing_unit", "fieldtype": "Int", - "label": "Packing Unit" + "label": "Packing Unit", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, "label": "Item Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "item_code.brand", @@ -79,19 +90,25 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Brand", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_description", "fieldtype": "Text", "label": "Item Description", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_details", "fieldtype": "Section Break", "label": "Price List", - "options": "fa fa-tags" + "options": "fa fa-tags", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list", @@ -100,7 +117,9 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -108,37 +127,49 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer" + "options": "Customer", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.buying == 1", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", - "options": "Supplier" + "options": "Supplier", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "buying", "fieldtype": "Check", "label": "Buying", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "selling", "fieldtype": "Check", "label": "Selling", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_details", "fieldtype": "Section Break", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -146,11 +177,15 @@ "fieldtype": "Link", "label": "Currency", "options": "Currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_br_1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_rate", @@ -162,53 +197,80 @@ "oldfieldname": "ref_rate", "oldfieldtype": "Currency", "options": "currency", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_15", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From" + "label": "Valid From", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days" + "label": "Lead Time in days", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_18", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Upto", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_24", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "note", "fieldtype": "Text", - "label": "Note" + "label": "Note", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "reference", "fieldtype": "Data", "in_list_view": 1, - "label": "Reference" + "label": "Reference", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-flag", "idx": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-06 22:31:32.943475", + "modified": "2020-12-08 18:12:15.395772", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index bed5ea9ab66..e82a19b0dc0 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -54,7 +54,8 @@ class ItemPrice(Document): "valid_upto", "packing_unit", "customer", - "supplier",]: + "supplier", + "batch_no"]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: @@ -68,7 +69,7 @@ class ItemPrice(Document): self.as_dict(),) if price_list_rate: - frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) + frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) def before_save(self): if self.selling: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bf45251c9d8..dfe8fea67b9 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -674,6 +674,8 @@ def get_item_price(args, item_code, ignore_party=False): and price_list=%(price_list)s and ifnull(uom, '') in ('', %(uom)s)""" + conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)" + if not ignore_party: if args.get("customer"): conditions += " and customer=%(customer)s" @@ -692,7 +694,7 @@ def get_item_price(args, item_code, ignore_party=False): return frappe.db.sql(""" select name, price_list_rate, uom from `tabItem Price` {conditions} - order by valid_from desc, uom desc """.format(conditions=conditions), args) + order by valid_from desc, batch_no desc, uom desc """.format(conditions=conditions), args) def get_price_list_rate_for(args, item_code): """ @@ -711,6 +713,7 @@ def get_price_list_rate_for(args, item_code): "uom": args.get('uom'), "transaction_date": args.get('transaction_date'), "posting_date": args.get('posting_date'), + "batch_no": args.get('batch_no') } item_price_data = 0 From 16bcd01f6da00016130183c8552ddd3256a0ca70 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 28 Jan 2021 12:09:35 +0530 Subject: [PATCH 37/46] feat: Provision to disable serial no and batch selector (#24398) * feat: feat: Provision to disbale serial no and batch selector * fix: Sider issues * fix: Linting issues --- erpnext/public/js/controllers/transaction.js | 12 +++++++++++- .../stock/doctype/stock_settings/stock_settings.json | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 77318332d25..1d1f18b1a0d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -589,11 +589,21 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) .then((r) => { if (r.message && - (r.message.has_batch_no || r.message.has_serial_no)) { + (r.message.has_batch_no || r.message.has_serial_no)) { frappe.flags.hide_serial_batch_dialog = false; } }); }, + () => { + // check if batch serial selector is disabled or not + if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) + return frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector') + .then((value) => { + if (value) { + frappe.flags.hide_serial_batch_dialog = true; + } + }); + }, () => { if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) { var d = locals[cdt][cdn]; diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 3ff396ba77e..84af57b48dd 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -16,6 +16,7 @@ "action_if_quality_inspection_is_not_submitted", "show_barcode_field", "clean_description_html", + "disable_serial_no_and_batch_selector", "section_break_7", "auto_insert_price_list_rate_if_missing", "allow_negative_stock", @@ -227,6 +228,12 @@ "fieldname": "control_historical_stock_transactions_section", "fieldtype": "Section Break", "label": "Control Historical Stock Transactions" + }, + { + "default": "0", + "fieldname": "disable_serial_no_and_batch_selector", + "fieldtype": "Check", + "label": "Disable Serial No And Batch Selector" } ], "icon": "icon-cog", @@ -234,7 +241,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-12-29 12:53:31.162247", + "modified": "2021-01-18 13:15:38.352796", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From f6f4376463456c980fe8a380609484f11e577689 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 28 Jan 2021 07:46:26 +0100 Subject: [PATCH 38/46] feat: remove german sales invoice validation (#24441) This can be better achieved via Customize Form (mandatory and mandatory_depends_on) --- erpnext/hooks.py | 3 -- .../regional/germany/accounts_controller.py | 53 ------------------- .../germany/test_accounts_controller.py | 12 ----- 3 files changed, 68 deletions(-) delete mode 100644 erpnext/regional/germany/accounts_controller.py delete mode 100644 erpnext/regional/germany/test_accounts_controller.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 14377e1b994..1c20555b827 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -416,9 +416,6 @@ regional_overrides = { 'Italy': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate', - }, - 'Germany': { - 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.germany.accounts_controller.validate_regional', } } user_privacy_documents = [ diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py deleted file mode 100644 index 7f76493608e..00000000000 --- a/erpnext/regional/germany/accounts_controller.py +++ /dev/null @@ -1,53 +0,0 @@ -import frappe -from frappe import _ -from frappe import msgprint - - -REQUIRED_FIELDS = { - "Sales Invoice": [ - { - "field_name": "company_address", - "regulation": "§ 14 Abs. 4 Nr. 1 UStG" - }, - { - "field_name": "company_tax_id", - "regulation": "§ 14 Abs. 4 Nr. 2 UStG" - }, - { - "field_name": "taxes", - "regulation": "§ 14 Abs. 4 Nr. 8 UStG" - }, - { - "field_name": "customer_address", - "regulation": "§ 14 Abs. 4 Nr. 1 UStG", - "condition": "base_grand_total > 250" - } - ] -} - - -def validate_regional(doc): - """Check if required fields for this document are present.""" - required_fields = REQUIRED_FIELDS.get(doc.doctype) - if not required_fields: - return - - meta = frappe.get_meta(doc.doctype) - field_map = {field.fieldname: field.label for field in meta.fields} - - for field in required_fields: - condition = field.get("condition") - if condition and not frappe.safe_eval(condition, doc.as_dict()): - continue - - field_name = field.get("field_name") - regulation = field.get("regulation") - if field_name and not doc.get(field_name): - missing(field_map.get(field_name), regulation) - - -def missing(field_label, regulation): - """Notify the user that a required field is missing.""" - translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501 - formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation) - msgprint(formatted_msg) diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py deleted file mode 100644 index 8bd378c971f..00000000000 --- a/erpnext/regional/germany/test_accounts_controller.py +++ /dev/null @@ -1,12 +0,0 @@ -import frappe -import unittest -from erpnext.regional.germany.accounts_controller import validate_regional - - -class TestAccountsController(unittest.TestCase): - - def setUp(self): - self.sales_invoice = frappe.get_last_doc('Sales Invoice') - - def test_validate_regional(self): - validate_regional(self.sales_invoice) From e5529ad461e5f57c972c880371308e4e8db0f62a Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 28 Jan 2021 12:22:39 +0530 Subject: [PATCH 39/46] fix: validate tax template for tax category (#24402) * fix: validate tax template for tax category * Update sales_taxes_and_charges_template.py --- .../sales_taxes_and_charges_template.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index b46de6c85bb..429a9f3591d 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -34,6 +34,9 @@ def valdiate_taxes_and_charges_template(doc): validate_disabled(doc) + # Validate with existing taxes and charges template for unique tax category + validate_for_tax_category(doc) + for tax in doc.get("taxes"): validate_taxes_and_charges(tax) validate_inclusive_tax(tax, doc) @@ -41,3 +44,7 @@ def valdiate_taxes_and_charges_template(doc): def validate_disabled(doc): if doc.is_default and doc.disabled: frappe.throw(_("Disabled template must not be default template")) + +def validate_for_tax_category(doc): + if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}): + frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) From 5756bf50e6d1d41cfbf7b028ee59baa7d48ebf3c Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Thu, 28 Jan 2021 12:24:39 +0530 Subject: [PATCH 40/46] fix: removed all day event from shift assignment calendar (#24397) Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- .../shift_assignment/shift_assignment.py | 48 ++++++++++++++----- .../shift_assignment_calendar.js | 10 +--- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 2c385e80f46..ab65260c091 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -88,7 +88,7 @@ def get_events(start, end, filters=None): def add_assignments(events, start, end, conditions=None): query = """select name, start_date, end_date, employee_name, - employee, docstatus + employee, docstatus, shift_type from `tabShift Assignment` where start_date >= %(start_date)s or end_date <= %(end_date)s @@ -97,18 +97,40 @@ def add_assignments(events, start, end, conditions=None): if conditions: query += conditions - for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True): - e = { - "name": d.name, - "doctype": "Shift Assignment", - "start_date": d.start_date, - "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + ": "+ \ - cstr(d.shift_type), - "docstatus": d.docstatus - } - if e not in events: - events.append(e) + records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True) + shift_timing_map = get_shift_type_timing([d.shift_type for d in records]) + + for d in records: + daily_event_start = d.start_date + daily_event_end = d.end_date if d.end_date else getdate() + delta = timedelta(days=1) + while daily_event_start <= daily_event_end: + start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time'] + end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time'] + daily_event_start += delta + e = { + "name": d.name, + "doctype": "Shift Assignment", + "start_date": start_timing, + "end_date": end_timing, + "title": cstr(d.employee_name) + ": "+ \ + cstr(d.shift_type), + "docstatus": d.docstatus, + "allDay": 0 + } + if e not in events: + events.append(e) + + return events + +def get_shift_type_timing(shift_types): + shift_timing_map = {} + data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time']) + + for d in data: + shift_timing_map[d.name] = d + + return shift_timing_map def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None): diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js index 17a986deb21..bb692e1402e 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js @@ -6,14 +6,8 @@ frappe.views.calendar["Shift Assignment"] = { "start": "start_date", "end": "end_date", "id": "name", - "docstatus": 1 - }, - options: { - header: { - left: 'prev,next today', - center: 'title', - right: 'month' - } + "docstatus": 1, + "allDay": "allDay", }, get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events" } \ No newline at end of file From 3914aca647f25592dffe66acb3d35b41a11f2a0c Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 28 Jan 2021 12:25:15 +0530 Subject: [PATCH 41/46] fix: check for tax rate (#24376) --- erpnext/regional/report/gstr_1/gstr_1.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index ad3de5f398d..96dc3f728d9 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -255,15 +255,16 @@ class Gstr1Report(object): for item_code, tax_amounts in item_wise_tax_detail.items(): tax_rate = tax_amounts[0] - if cgst_or_sgst: - tax_rate *= 2 - if parent not in self.cgst_sgst_invoices: - self.cgst_sgst_invoices.append(parent) + if tax_rate: + if cgst_or_sgst: + tax_rate *= 2 + if parent not in self.cgst_sgst_invoices: + self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) - if item_code not in rate_based_dict: - rate_based_dict.append(item_code) + rate_based_dict = self.items_based_on_tax_rate\ + .setdefault(parent, {}).setdefault(tax_rate, []) + if item_code not in rate_based_dict: + rate_based_dict.append(item_code) except ValueError: continue if unidentified_gst_accounts: From 8e55677f10fac3002983a4fbf9160dd5a9e53cb0 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 28 Jan 2021 12:26:45 +0530 Subject: [PATCH 42/46] feat: deleting account & stock entries on deletion of transaction (#24298) * feat: control for deleting account & stock entries on deletion of a transaction * chore: change label & fieldname Co-authored-by: Nabin Hait --- .../doctype/accounts_settings/accounts_settings.json | 11 +++++++++-- erpnext/controllers/accounts_controller.py | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 41f9ce030a1..a3c29b6d640 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -21,6 +21,7 @@ "book_asset_depreciation_entry_automatically", "add_taxes_from_item_tax_template", "automatically_fetch_payment_terms", + "delete_linked_ledger_entries", "deferred_accounting_settings_section", "automatically_process_deferred_accounting_entry", "book_deferred_entries_based_on", @@ -219,6 +220,12 @@ "fieldtype": "Select", "label": "Book Deferred Entries Based On", "options": "Days\nMonths" + }, + { + "default": "0", + "fieldname": "delete_linked_ledger_entries", + "fieldtype": "Check", + "label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction" } ], "icon": "icon-cog", @@ -226,7 +233,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 11:32:52.268826", + "modified": "2021-01-05 13:04:00.118892", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -254,4 +261,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0f1aa23064c..b9bfbaf0bd3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -118,6 +118,12 @@ class AccountsController(TransactionBase): def before_cancel(self): validate_einvoice_fields(self) + + def on_trash(self): + # delete sl and gl entries on deletion of transaction + if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'): + frappe.db.sql("delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)) + frappe.db.sql("delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)) def validate_deferred_start_and_end_date(self): for d in self.items: From 9f8cbe91013c49126c8be3b4164343fd1ee6eab9 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 28 Jan 2021 12:29:52 +0530 Subject: [PATCH 43/46] Material request wrong status issue (#24019) * fix: material request status fix * fix: changing precision to global defaults for material request status * fix: adding back the removed flt * fix: sider issues fixed Co-authored-by: pateljannat --- .../doctype/purchase_order/purchase_order.js | 2 +- .../request_for_quotation.js | 4 ++-- .../supplier_quotation/supplier_quotation.js | 2 +- .../material_request/material_request_list.js | 15 ++++++++------- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 14534ef1b75..83efa66f6b0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -381,7 +381,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index e537771eaf2..b76c3784a47 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -224,7 +224,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) @@ -280,7 +280,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99] + per_ordered: ["<", 100] } }); dialog.hide(); diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index a3b2085400e..a0187b0a824 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -44,7 +44,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext material_request_type: "Purchase", docstatus: 1, status: ["!=", "Stopped"], - per_ordered: ["<", 99.99], + per_ordered: ["<", 100], company: me.frm.doc.company } }) diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index 0d7095875c6..de7a3d05bf5 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -1,9 +1,10 @@ frappe.listview_settings['Material Request'] = { add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"], get_indicator: function(doc) { - if(doc.status=="Stopped") { + var precision = frappe.defaults.get_default("float_precision"); + if (doc.status=="Stopped") { return [__("Stopped"), "red", "status,=,Stopped"]; - } else if(doc.transfer_status && doc.docstatus != 2) { + } else if (doc.transfer_status && doc.docstatus != 2) { if (doc.transfer_status == "Not Started") { return [__("Not Started"), "orange"]; } else if (doc.transfer_status == "In Transit") { @@ -11,14 +12,14 @@ frappe.listview_settings['Material Request'] = { } else if (doc.transfer_status == "Completed") { return [__("Completed"), "green"]; } - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 0) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 0) { return [__("Pending"), "orange", "per_ordered,=,0"]; - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) < 100) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) < 100) { return [__("Partially ordered"), "yellow", "per_ordered,<,100"]; - } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 100) { - if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) < 100 && flt(doc.per_received, 2) > 0) { + } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 100) { + if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) < 100 && flt(doc.per_received, precision) > 0) { return [__("Partially Received"), "yellow", "per_received,<,100"]; - } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) == 100) { + } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) { return [__("Received"), "green", "per_received,=,100"]; } else if (doc.material_request_type == "Purchase") { return [__("Ordered"), "green", "per_ordered,=,100"]; From f7ea340181acd3fdde2c185a8d1c36fb75e04e6e Mon Sep 17 00:00:00 2001 From: Mohamed Almubarak <59777434+madar2020@users.noreply.github.com> Date: Thu, 28 Jan 2021 10:02:13 +0300 Subject: [PATCH 44/46] fix(template): cards on second row overlaps the one before (#23454) * Update macros.html fix second row of cards overlapps with the one before * Update macros.html fix(template): second row of cards overlaps with the one before * Update macros.html fix(template): second row of cards overlaps with the one before --- erpnext/templates/includes/macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index ea6b00fc58a..5d8ee5cab6d 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -40,7 +40,7 @@
    {% if card.image %} -
    +
    {% endif %}
    {{ card.title }}
    From b4be2922173ed74a04a02cfb5d1fdd5976459f85 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 28 Jan 2021 13:09:56 +0530 Subject: [PATCH 45/46] fix: Item valuation for internal stock transfers (#24200) * fix: Item valuation for internal stocktransfers * fix: Consider conversion factor for invoices * fix: Add missing set warehouse fields * fix: Add validations and other fixes * fix: Fixes in flow * fix: Internal sales item link in Purchase Invoice * fix: Debugging * fix: Debug travis * fix: Remove commented code * fix: Rate forcing in sales order * fix: Unreallized profit in Sales Register * fix: Item wise gst sales register fix * fix: From warehouse in Purchase Order * fix: Target field in Sales Invoice * fix: remove self * fix: GST Purchasse register and other fixes * fix: Add shipping_address to no field map * fix: Ref doc map * fix: Test Cases * fix: address mapping between sales and purchase document * fix: Import Error * fix: Tax updation for internal invoices * fix: Purchase Order and Invoice linking * fix: Internal Party validation fix * fix: validation * fix(india): GST Taxes for intra state transfer * fix(india): GST Taxes for intra state transfer Co-authored-by: Nabin Hait --- .../purchase_invoice/purchase_invoice.js | 5 +- .../purchase_invoice/purchase_invoice.json | 30 ++-- .../purchase_invoice/purchase_invoice.py | 54 +++--- .../purchase_invoice_item.json | 26 ++- .../doctype/sales_invoice/sales_invoice.js | 19 +-- .../doctype/sales_invoice/sales_invoice.json | 15 +- .../doctype/sales_invoice/sales_invoice.py | 157 ++++++++++++++++-- .../sales_invoice/test_sales_invoice.py | 114 ++++++++----- .../sales_invoice_item.json | 5 +- .../item_wise_purchase_register.py | 4 +- .../item_wise_sales_register.py | 3 +- .../purchase_register/purchase_register.py | 41 ++++- .../report/sales_register/sales_register.py | 59 ++++++- .../doctype/purchase_order/purchase_order.js | 20 +-- .../purchase_order/purchase_order.json | 19 ++- erpnext/buying/doctype/supplier/supplier.py | 5 +- erpnext/controllers/accounts_controller.py | 22 ++- erpnext/controllers/buying_controller.py | 52 +++++- erpnext/controllers/selling_controller.py | 28 +++- erpnext/controllers/stock_controller.py | 30 +++- erpnext/public/js/controllers/transaction.js | 49 +++++- erpnext/public/js/utils/party.js | 6 + erpnext/regional/india/taxes.js | 6 +- erpnext/regional/india/utils.py | 2 +- erpnext/selling/doctype/customer/customer.py | 5 +- .../doctype/sales_order/sales_order.js | 23 +-- .../doctype/sales_order/sales_order.json | 23 ++- .../doctype/delivery_note/delivery_note.js | 29 ++-- .../doctype/delivery_note/delivery_note.json | 38 +++-- .../doctype/delivery_note/delivery_note.py | 31 +++- .../delivery_note_item.json | 7 +- .../purchase_receipt/purchase_receipt.json | 22 ++- .../purchase_receipt_item.json | 14 +- erpnext/stock/stock_ledger.py | 29 ++-- 34 files changed, 749 insertions(+), 243 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 4a952a30a26..06aa20bfc5a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -275,8 +275,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ supplier: function() { var me = this; - if(this.frm.updating_party_details) + + // Do not update if inter company reference is there as the details will already be updated + if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference) return; + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index c64ffd878c4..451c9368816 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -57,8 +57,8 @@ "set_warehouse", "rejected_warehouse", "col_break_warehouse", + "set_from_warehouse", "is_subcontracted", - "supplier_warehouse", "items_section", "update_stock", "scan_barcode", @@ -515,6 +515,7 @@ }, { "depends_on": "update_stock", + "description": "Sets 'Accepted Warehouse' in each row of the items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "label": "Set Accepted Warehouse", @@ -543,17 +544,6 @@ "options": "No\nYes", "print_hide": 1 }, - { - "depends_on": "eval:doc.is_subcontracted==\"Yes\"", - "fieldname": "supplier_warehouse", - "fieldtype": "Link", - "label": "Supplier Warehouse", - "no_copy": 1, - "options": "Warehouse", - "print_hide": 1, - "print_width": "50px", - "width": "50px" - }, { "fieldname": "items_section", "fieldtype": "Section Break", @@ -1232,7 +1222,9 @@ "fieldname": "inter_company_invoice_reference", "fieldtype": "Link", "label": "Inter Company Invoice Reference", + "no_copy": 1, "options": "Sales Invoice", + "print_hide": 1, "read_only": 1 }, { @@ -1356,13 +1348,25 @@ "fieldtype": "Link", "label": "Represents Company", "options": "Company" + }, + { + "depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)", + "description": "Sets 'From Warehouse' in each row of the items table.", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "50px", + "width": "50px" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-12-11 12:46:12.796378", + "modified": "2020-12-26 20:49:03.305063", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b52678e8d3b..56feb573c27 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -443,7 +443,7 @@ class PurchaseInvoice(BuyingController): else: self.stock_received_but_not_billed = None self.expenses_included_in_valuation = None - + self.negative_expense_to_be_booked = 0.0 gl_entries = [] @@ -457,7 +457,7 @@ class PurchaseInvoice(BuyingController): self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) - + gl_entries = merge_similar_entries(gl_entries) self.make_payment_gl_entries(gl_entries) @@ -480,7 +480,7 @@ class PurchaseInvoice(BuyingController): grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total if grand_total and not self.is_internal_transfer(): - # Didnot use base_grand_total to book rounding loss gle + # Did not use base_grand_total to book rounding loss gle grand_total_in_company_currency = flt(grand_total * self.conversion_rate, self.precision("grand_total")) gl_entries.append( @@ -511,8 +511,8 @@ class PurchaseInvoice(BuyingController): voucher_wise_stock_value = {} if self.update_stock: for d in frappe.get_all('Stock Ledger Entry', - fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): - voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) + fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): + voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) valuation_tax_accounts = [d.account_head for d in self.get("taxes") if d.category in ('Valuation', 'Total and Valuation') @@ -563,16 +563,17 @@ class PurchaseInvoice(BuyingController): ) else: - gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": warehouse_debit_amount, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) + if not self.is_internal_transfer(): + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": warehouse_debit_amount, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) # Amount added through landed-cost-voucher if landed_cost_entries: @@ -624,13 +625,14 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: expense_account = service_received_but_not_billed_account - gl_entries.append(self.get_gl_dict({ - "account": expense_account, - "against": self.supplier, - "debit": amount, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item)) + if not self.is_internal_transfer(): + gl_entries.append(self.get_gl_dict({ + "account": expense_account, + "against": self.supplier, + "debit": amount, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item)) # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: @@ -795,10 +797,10 @@ class PurchaseInvoice(BuyingController): # Stock ledger value is not matching with the warehouse amount if (self.update_stock and voucher_wise_stock_value.get(item.name) and - warehouse_debit_amount != flt(voucher_wise_stock_value.get(item.name), net_amt_precision)): + warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)): cost_of_goods_sold_account = self.get_company_default("default_expense_account") - stock_amount = flt(voucher_wise_stock_value.get(item.name), net_amt_precision) + stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) stock_adjustment_amt = warehouse_debit_amount - stock_amount gl_entries.append( @@ -999,10 +1001,10 @@ class PurchaseInvoice(BuyingController): self.delete_auto_created_batches() self.make_gl_entries_on_cancel() - + if self.update_stock == 1: self.repost_future_sle_and_gle() - + self.update_project() frappe.db.set(self, 'status', 'Cancelled') diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index f6d76e50502..1f7853dbf71 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -87,6 +88,7 @@ "po_detail", "purchase_receipt", "pr_detail", + "sales_invoice_item", "item_weight_details", "weight_per_unit", "total_weight", @@ -553,8 +555,8 @@ "fieldtype": "Link", "hidden": 1, "label": "Brand", - "print_hide": 1, - "options": "Brand" + "options": "Brand", + "print_hide": 1 }, { "fetch_from": "item_code.item_group", @@ -562,9 +564,9 @@ "fieldname": "item_group", "fieldtype": "Link", "label": "Item Group", + "options": "Item Group", "print_hide": 1, - "read_only": 1, - "options": "Item Group" + "read_only": 1 }, { "description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges", @@ -759,10 +761,11 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_internal_supplier && parent.update_stock", "fieldname": "from_warehouse", "fieldtype": "Link", "ignore_user_permissions": 1, - "label": "Supplier Warehouse", + "label": "From Warehouse", "options": "Warehouse" }, { @@ -779,11 +782,20 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "sales_invoice_item", + "fieldtype": "Data", + "label": "Sales Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2020-08-20 11:48:01.398356", + "links": [], + "modified": "2020-12-26 17:20:36.415791", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -791,4 +803,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 72199a92aa2..f2a62cdacd1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -130,16 +130,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.set_default_print_format(); if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { - frappe.model.with_doc("Customer", me.frm.doc.customer, function() { - var customer = frappe.model.get_doc("Customer", me.frm.doc.customer); - var internal = customer.is_internal_customer; - var disabled = customer.disabled; - if (internal == 1 && disabled == 0) { - me.frm.add_custom_button("Inter Company Invoice", function() { - me.make_inter_company_invoice(); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Invoice" : + "Inter Company Purchase Invoice"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_invoice(); + }, __('Create')); + } } }, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 6799fb986aa..447cee42a75 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -60,6 +60,8 @@ "ignore_pricing_rule", "sec_warehouse", "set_warehouse", + "column_break_55", + "set_target_warehouse", "items_section", "update_stock", "scan_barcode", @@ -1969,13 +1971,24 @@ "label": "Represents Company", "options": "Company", "read_only": 1 + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.is_internal_customer && doc.update_stock", + "fieldname": "set_target_warehouse", + "fieldtype": "Link", + "label": "Set Target Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-12-11 12:48:31.769958", + "modified": "2020-12-25 22:57:32.555067", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 566734e7d14..7116a6a62b0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -6,7 +6,7 @@ import frappe, erpnext import frappe.defaults from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate from frappe import _, msgprint, throw -from erpnext.accounts.party import get_party_account, get_due_date +from erpnext.accounts.party import get_party_account, get_due_date, get_party_details from frappe.model.mapper import get_mapped_doc from erpnext.controllers.selling_controller import SellingController from erpnext.accounts.utils import get_account_currency @@ -21,6 +21,8 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points from erpnext.accounts.deferred_revenue import validate_service_stop_date +from frappe.model.utils import get_fetch_values +from frappe.contacts.doctype.address.address import get_address_display from erpnext.healthcare.utils import manage_invoice_submit_cancel @@ -1534,7 +1536,7 @@ def validate_inter_company_transaction(doc, doctype): details = get_inter_company_details(doc, doctype) price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1}) - if not valid_price_list: + if not valid_price_list and not doc.is_internal_transfer(): frappe.throw(_("Selected Price List should have buying and selling fields checked.")) party = details.get("party") @@ -1557,6 +1559,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if doctype in ["Sales Invoice", "Sales Order"]: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" + target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item" source_document_warehouse_field = 'target_warehouse' target_document_warehouse_field = 'from_warehouse' else: @@ -1570,6 +1573,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def set_missing_values(source, target): target.run_method("set_missing_values") + set_purchase_references(target) def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name @@ -1577,19 +1581,38 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency') target_doc.company = details.get("company") target_doc.supplier = details.get("party") + target_doc.is_internal_supplier = 1 + target_doc.ignore_pricing_rule = 1 target_doc.buying_price_list = source_doc.selling_price_list + # Invert Addresses + update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) + update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + if currency: target_doc.currency = currency + + update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address) + else: currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency') target_doc.company = details.get("company") target_doc.customer = details.get("party") target_doc.selling_price_list = source_doc.buying_price_list + update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) + update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) + update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + if currency: target_doc.currency = currency + update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.customer_address, + company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + item_field_map = { "doctype": target_doctype + " Item", "field_no_map": [ @@ -1597,25 +1620,33 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "expense_account", "cost_center", "warehouse" - ] + ], + "field_map": { + 'rate': 'rate', + } } - if source_doc.get('update_stock'): - item_field_map.update({ - 'field_map': { - source_document_warehouse_field: target_document_warehouse_field, - 'batch_no': 'batch_no', - 'serial_no': 'serial_no' - } + if doctype in ["Sales Invoice", "Sales Order"]: + item_field_map["field_map"].update({ + "name": target_detail_field, }) + if source_doc.get('update_stock'): + item_field_map["field_map"].update({ + source_document_warehouse_field: target_document_warehouse_field, + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' + }) doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, "postprocess": update_details, + "set_target_warehouse": "set_from_warehouse", "field_no_map": [ - "taxes_and_charges" + "taxes_and_charges", + "set_warehouse", + "shipping_address" ] }, doctype +" Item": item_field_map @@ -1624,6 +1655,110 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): return doclist +def set_purchase_references(doc): + # add internal PO or PR links if any + if doc.is_internal_transfer(): + if doc.doctype == 'Purchase Receipt': + so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) + + if so_item_map: + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + + update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map) + + elif doc.doctype == 'Purchase Invoice': + dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference) + # First check for Purchase receipt + if list(dn_item_map.values()): + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item') + + update_pi_items(doc, 'pr_detail', 'purchase_receipt', + dn_item_map, pd_item_map, parent_child_map, warehouse_map) + + if list(so_item_map.values()): + pd_item_map, parent_child_map, warehouse_map = \ + get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + + update_pi_items(doc, 'po_detail', 'purchase_order', + so_item_map, pd_item_map, parent_child_map, warehouse_map) + +def update_pi_items(doc, detail_field, parent_field, sales_item_map, + purchase_item_map, parent_child_map, warehouse_map): + for item in doc.get('items'): + item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item))) + item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) + if doc.update_stock: + item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + +def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): + for item in doc.get('items'): + item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) + item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) + item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + +def get_delivery_note_details(internal_reference): + so_item_map = {} + + si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'], + filters={'parent': internal_reference}) + + for d in si_item_details: + so_item_map.setdefault(d.name, d.so_detail) + + return so_item_map + +def get_sales_invoice_details(internal_reference): + dn_item_map = {} + so_item_map = {} + + si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail', + 'dn_detail'], filters={'parent': internal_reference}) + + for d in si_item_details: + if d.dn_detail: + dn_item_map.setdefault(d.name, d.dn_detail) + if d.so_detail: + so_item_map.setdefault(d.name, d.so_detail) + + return dn_item_map, so_item_map + +def get_pd_details(doctype, sd_detail_map, sd_detail_field): + pd_item_map = {} + accepted_warehouse_map = {} + parent_child_map = {} + + pd_item_details = frappe.get_all(doctype, + fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))}) + + for d in pd_item_details: + pd_item_map.setdefault(d.get(sd_detail_field), d.name) + parent_child_map.setdefault(d.get(sd_detail_field), d.parent) + accepted_warehouse_map.setdefault(d.get(sd_detail_field), d.warehouse) + + return pd_item_map, parent_child_map, accepted_warehouse_map + +def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None, + company_address=None, shipping_address_name=None, master_doctype=None): + # Update Party Details + party_details = get_party_details(party=party, party_type=party_type, company=company, + doctype=doctype, party_address=party_address, company_address=company_address, + shipping_address=shipping_address_name) + + # Update taxes and charges if any + doc.taxes_and_charges = party_details.get('taxes_and_charges') + doc.set('taxes', party_details.get('taxes')) + +def update_address(doc, address_field, address_display_field, address_name): + doc.set(address_field, address_name) + fetch_values = get_fetch_values(doc.doctype, address_field, address_name) + + for key, value in fetch_values.items(): + doc.set(key, value) + + doc.set(address_display_field, get_address_display(doc.get(address_field))) + @frappe.whitelist() def get_loyalty_programs(customer): ''' sets applicable loyalty program to the customer or returns a list of applicable programs ''' diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3a6dbeb51c2..e94e2cdd957 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -22,6 +22,7 @@ from erpnext.regional.india.utils import get_ewb_data from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice +from erpnext.stock.utils import get_incoming_rate class TestSalesInvoice(unittest.TestCase): def make(self): @@ -688,7 +689,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue(gle) def test_pos_gl_entry_with_perpetual_inventory(self): - make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") @@ -745,7 +746,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(pos_return.get('payments')[0].amount, -1000) def test_pos_change_amount(self): - make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", + make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", @@ -1770,59 +1771,82 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") - # def test_internal_transfer_gl_entry(self): - # ## Create internal transfer account - # account = create_account(account_name="Unrealized Profit", - # parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") + def test_internal_transfer_gl_entry(self): + ## Create internal transfer account + account = create_account(account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") - # frappe.db.set_value('Company', '_Test Company with perpetual inventory', - # 'unrealized_profit_loss_account', account) + frappe.db.set_value('Company', '_Test Company with perpetual inventory', + 'unrealized_profit_loss_account', account) - # customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", - # "_Test Company with perpetual inventory") + customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") - # create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", - # "_Test Company with perpetual inventory") + create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory") - # si = create_sales_invoice( - # company = "_Test Company with perpetual inventory", - # customer = customer, - # debit_to = "Debtors - TCP1", - # warehouse = "Stores - TCP1", - # income_account = "Sales - TCP1", - # expense_account = "Cost of Goods Sold - TCP1", - # cost_center = "Main - TCP1", - # currency = "INR", - # do_not_save = 1 - # ) + si = create_sales_invoice( + company = "_Test Company with perpetual inventory", + customer = customer, + debit_to = "Debtors - TCP1", + warehouse = "Stores - TCP1", + income_account = "Sales - TCP1", + expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", + currency = "INR", + do_not_save = 1 + ) - # si.selling_price_list = "_Test Price List Rest of the World" - # si.update_stock = 1 - # si.items[0].target_warehouse = 'Work In Progress - TCP1' - # add_taxes(si) - # si.save() - # si.submit() + si.selling_price_list = "_Test Price List Rest of the World" + si.update_stock = 1 + si.items[0].target_warehouse = 'Work In Progress - TCP1' + add_taxes(si) + si.save() - # target_doc = make_inter_company_transaction("Sales Invoice", si.name) - # target_doc.company = '_Test Company with perpetual inventory' - # target_doc.items[0].warehouse = 'Finished Goods - TCP1' - # add_taxes(target_doc) - # target_doc.save() - # target_doc.submit() + rate = 0.0 + for d in si.get('items'): + rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": si.posting_date, + "posting_time": si.posting_time, + "qty": -1 * flt(d.get('stock_qty')), + "serial_no": d.serial_no, + "company": si.company, + "voucher_type": 'Sales Invoice', + "voucher_no": si.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) - # si_gl_entries = [ - # ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], - # ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] - # ] + rate = flt(rate, 2) - # check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + si.submit() - # pi_gl_entries = [ - # ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], - # ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] - # ] + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + target_doc.company = '_Test Company with perpetual inventory' + target_doc.items[0].warehouse = 'Finished Goods - TCP1' + add_taxes(target_doc) + target_doc.save() + target_doc.submit() - # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + tax_amount = flt(rate * (12/100), 2) + si_gl_entries = [ + ["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()], + ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()] + ] + + check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) + + pi_gl_entries = [ + ["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()], + ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()] + ] + + # Sale and Purchase both should be at valuation rate + self.assertEqual(si.items[0].rate, rate) + self.assertEqual(target_doc.items[0].rate, rate) + + check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) def test_eway_bill_json(self): si = make_sales_invoice_for_ewaybill() diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 36950757989..7a98afff364 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -565,11 +565,12 @@ "print_hide": 1 }, { + "depends_on": "eval: parent.is_internal_customer && parent.update_stock", "fieldname": "target_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Customer Warehouse (Optional)", + "label": "Target Warehouse", "no_copy": 1, "options": "Warehouse", "print_hide": 1 @@ -815,7 +816,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-23 19:59:04.879322", + "modified": "2020-12-26 17:25:04.090630", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index eeb5140bbe2..cb4d9b43dbd 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -49,7 +49,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum elif d.po_detail: purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, [])) - expense_account = d.expense_account or aii_account_map.get(d.company) + expense_account = d.unrealized_profit_loss_account or d.expense_account \ + or aii_account_map.get(d.company) row = { 'item_code': d.item_code, @@ -315,6 +316,7 @@ def get_items(filters, additional_query_columns): `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, + `tabPurchase Invoice`.unrealized_profit_loss_account, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index f54ceb0d2f5..998003ac698 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum 'company': d.company, 'sales_order': d.sales_order, 'delivery_note': d.delivery_note, - 'income_account': d.income_account, + 'income_account': d.unrealized_profit_loss_account or d.income_account, 'cost_center': d.cost_center, 'stock_qty': d.stock_qty, 'stock_uom': d.stock_uom @@ -379,6 +379,7 @@ def get_items(filters, additional_query_columns): select `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, + `tabSales Invoice`.unrealized_profit_loss_account, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 9399e707390..8ac749d6290 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -14,13 +14,15 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if not filters: filters = {} invoice_list = get_invoices(filters, additional_query_columns) - columns, expense_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns) + columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts \ + = get_columns(invoice_list, additional_table_columns) if not invoice_list: msgprint(_("No record found")) return columns, invoice_list invoice_expense_map = get_invoice_expense_map(invoice_list) + internal_invoice_map = get_internal_invoice_map(invoice_list) invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) @@ -52,10 +54,17 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum # map expense values base_net_total = 0 for expense_acc in expense_accounts: - expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc)) + if inv.is_internal_supplier and inv.company == inv.represents_company: + expense_amount = 0 + else: + expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc)) base_net_total += expense_amount row.append(expense_amount) + # Add amount in unrealized account + for account in unrealized_profit_loss_accounts: + row.append(flt(internal_invoice_map.get((inv.name, account)))) + # net total row.append(base_net_total or inv.base_net_total) @@ -96,7 +105,8 @@ def get_columns(invoice_list, additional_table_columns): "width": 80 } ] - expense_accounts = tax_accounts = expense_columns = tax_columns = [] + expense_accounts = tax_accounts = expense_columns = tax_columns = unrealized_profit_loss_accounts = \ + unrealized_profit_loss_account_columns = [] if invoice_list: expense_accounts = frappe.db.sql_list("""select distinct expense_account @@ -112,17 +122,25 @@ def get_columns(invoice_list, additional_table_columns): and parent in (%s) order by account_head""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account + from `tabPurchase Invoice` where docstatus = 1 and name in (%s) + and ifnull(unrealized_profit_loss_account, '') != '' + order by unrealized_profit_loss_account""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts] + unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts] + for account in tax_accounts: if account not in expense_accounts: tax_columns.append(account + ":Currency/currency:120") - columns = columns + expense_columns + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ + columns = columns + expense_columns + unrealized_profit_loss_account_columns + \ + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ [_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120", _("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"] - return columns, expense_accounts, tax_accounts + return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts def get_conditions(filters): conditions = "" @@ -199,6 +217,19 @@ def get_invoice_expense_map(invoice_list): return invoice_expense_map +def get_internal_invoice_map(invoice_list): + unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, + base_net_total as amount from `tabPurchase Invoice` where name in (%s) + and is_internal_supplier = 1 and company = represents_company""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + + internal_invoice_map = {} + for d in unrealized_amount_details: + if d.unrealized_profit_loss_account: + internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount) + + return internal_invoice_map + def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): tax_details = frappe.db.sql(""" select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index b6e61b13069..cb2c98b64ae 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -15,13 +15,14 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No if not filters: filters = frappe._dict({}) invoice_list = get_invoices(filters, additional_query_columns) - columns, income_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns) + columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(invoice_list, additional_table_columns) if not invoice_list: msgprint(_("No record found")) return columns, invoice_list invoice_income_map = get_invoice_income_map(invoice_list) + internal_invoice_map = get_internal_invoice_map(invoice_list) invoice_income_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts) #Cost Center & Warehouse Map @@ -70,12 +71,22 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No # map income values base_net_total = 0 for income_acc in income_accounts: - income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) + if inv.is_internal_customer and inv.company == inv.represents_company: + income_amount = 0 + else: + income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) + base_net_total += income_amount row.update({ frappe.scrub(income_acc): income_amount }) + # Add amount in unrealized account + for account in unrealized_profit_loss_accounts: + row.update({ + frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account))) + }) + # net total row.update({'net_total': base_net_total or inv.base_net_total}) @@ -230,6 +241,8 @@ def get_columns(invoice_list, additional_table_columns): tax_accounts = [] income_columns = [] tax_columns = [] + unrealized_profit_loss_accounts = [] + unrealized_profit_loss_account_columns = [] if invoice_list: income_accounts = frappe.db.sql_list("""select distinct income_account @@ -243,12 +256,18 @@ def get_columns(invoice_list, additional_table_columns): and parent in (%s) order by account_head""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account + from `tabSales Invoice` where docstatus = 1 and name in (%s) + and ifnull(unrealized_profit_loss_account, '') != '' + order by unrealized_profit_loss_account""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + for account in income_accounts: income_columns.append({ "label": account, "fieldname": frappe.scrub(account), "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }) @@ -258,15 +277,24 @@ def get_columns(invoice_list, additional_table_columns): "label": account, "fieldname": frappe.scrub(account), "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }) + for account in unrealized_profit_loss_accounts: + unrealized_profit_loss_account_columns.append({ + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120 + }) + net_total_column = [{ "label": _("Net Total"), "fieldname": "net_total", "fieldtype": "Currency", - "options": 'currency', + "options": "currency", "width": 120 }] @@ -301,9 +329,10 @@ def get_columns(invoice_list, additional_table_columns): } ] - columns = columns + income_columns + net_total_column + tax_columns + total_columns + columns = columns + income_columns + unrealized_profit_loss_account_columns + \ + net_total_column + tax_columns + total_columns - return columns, income_accounts, tax_accounts + return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts def get_conditions(filters): conditions = "" @@ -368,7 +397,8 @@ def get_invoices(filters, additional_query_columns): return frappe.db.sql(""" select name, posting_date, debit_to, project, customer, customer_name, owner, remarks, territory, tax_id, customer_group, - base_net_total, base_grand_total, base_rounded_total, outstanding_amount {0} + base_net_total, base_grand_total, base_rounded_total, outstanding_amount, + is_internal_customer, represents_company, company {0} from `tabSales Invoice` where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') % conditions, filters, as_dict=1) @@ -385,6 +415,19 @@ def get_invoice_income_map(invoice_list): return invoice_income_map +def get_internal_invoice_map(invoice_list): + unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, + base_net_total as amount from `tabSales Invoice` where name in (%s) + and is_internal_customer = 1 and company = represents_company""" % + ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + + internal_invoice_map = {} + for d in unrealized_amount_details: + if d.unrealized_profit_loss_account: + internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount) + + return internal_invoice_map + def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): tax_details = frappe.db.sql("""select parent, account_head, sum(base_tax_amount_after_discount_amount) as tax_amount diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 83efa66f6b0..dd0f0658485 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -164,16 +164,16 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if (doc.docstatus === 1 && !doc.inter_company_order_reference) { let me = this; - frappe.model.with_doc("Supplier", me.frm.doc.supplier, () => { - let supplier = frappe.model.get_doc("Supplier", me.frm.doc.supplier); - let internal = supplier.is_internal_supplier; - let disabled = supplier.disabled; - if (internal === 1 && disabled === 0) { - me.frm.add_custom_button("Inter Company Order", function() { - me.make_inter_company_order(me.frm); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_supplier; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Sales Order" : + "Inter Company Sales Order"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_order(me.frm); + }, __('Create')); + } + } } diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 75da71ceff8..ee2beea67f9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -134,6 +134,8 @@ "ref_sq", "column_break_74", "party_account_currency", + "is_internal_supplier", + "represents_company", "inter_company_order_reference" ], "fields": [ @@ -1101,13 +1103,28 @@ { "fieldname": "items_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "supplier.is_internal_supplier", + "fieldname": "is_internal_supplier", + "fieldtype": "Check", + "label": "Is Internal Supplier" + }, + { + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-12-03 16:46:44.229351", + "modified": "2021-01-20 22:07:23.487138", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 0ee9d180d99..edeb135d951 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -52,7 +52,10 @@ class Supplier(TransactionBase): self.validate_internal_supplier() def validate_internal_supplier(self): - if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"): + internal_supplier = frappe.db.get_value("Supplier", + {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + + if internal_supplier: frappe.throw(_("Internal Supplier for company {0} already exists").format( frappe.bold(self.represents_company))) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b9bfbaf0bd3..eaa80800c69 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -75,6 +75,9 @@ class AccountsController(TransactionBase): self.ensure_supplier_is_not_blocked() self.validate_date_with_fiscal_year() + self.validate_inter_company_reference() + + self.set_incoming_rate() if self.meta.get_field("currency"): self.calculate_taxes_and_totals() @@ -110,12 +113,12 @@ class AccountsController(TransactionBase): self.set_inter_company_account() validate_regional(self) - + validate_einvoice_fields(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) - + def before_cancel(self): validate_einvoice_fields(self) @@ -212,6 +215,17 @@ class AccountsController(TransactionBase): validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company, self.meta.get_label(date_field), self) + def validate_inter_company_reference(self): + if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): + return + + if self.is_internal_transfer(): + if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference') + or self.get('inter_company_order_reference')): + msg = _("Internal Sale or Delivery Reference missing. ") + msg += _("Please create purchase from internal sale or delivery document itself") + frappe.throw(msg, title=_("Internal Sales Reference Missing")) + def validate_due_date(self): if self.get('is_pos'): return @@ -968,9 +982,9 @@ class AccountsController(TransactionBase): It will an internal transfer if its an internal customer and representation company is same as billing company """ - if self.doctype == 'Sales Invoice': + if self.doctype in ('Sales Invoice', 'Delivery Note', 'Sales Order'): internal_party_field = 'is_internal_customer' - else: + elif self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'): internal_party_field = 'is_internal_supplier' if self.get(internal_party_field) and (self.represents_company == self.company): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 4dee375e5a2..ab1f02779be 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -44,7 +44,6 @@ class BuyingController(StockController): self.validate_items() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() - self.update_tax_category_for_internal_transfer() self.validate_warehouse() self.validate_from_warehouse() self.set_supplier_address() @@ -100,11 +99,6 @@ class BuyingController(StockController): msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') self.update_tax_category(msg) - def update_tax_category_for_internal_transfer(self): - if self.doctype == 'Purchase Invoice' and self.is_internal_transfer(): - msg = _('Tax Category has been changed to "Total" as its an internal purchase.') - self.update_tax_category(msg) - def update_tax_category(self, msg): tax_for_valuation = [d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]] @@ -224,6 +218,48 @@ class BuyingController(StockController): else: item.valuation_rate = 0.0 + def set_incoming_rate(self): + if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"): + return + + ref_doctype_map = { + "Purchase Order": "Sales Order Item", + "Purchase Receipt": "Delivery Note Item", + "Purchase Invoice": "Sales Invoice Item", + } + + ref_doctype = ref_doctype_map.get(self.doctype) + items = self.get("items") + for d in items: + if not cint(self.get("is_return")): + # Get outgoing rate based on original item cost based on valuation method + + if not d.get(frappe.scrub(ref_doctype)): + outgoing_rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.get('from_warehouse'), + "posting_date": self.get('posting_date') or self.get('transation_date'), + "posting_time": self.get('posting_time'), + "qty": -1 * flt(d.get('stock_qty')), + "serial_no": d.get('serial_no'), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) + + rate = flt(outgoing_rate * d.conversion_factor, d.precision('rate')) + else: + rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), 'rate') + + if self.is_internal_transfer(): + if rate != d.rate: + d.rate = rate + d.discount_percentage = 0 + d.discount_amount = 0 + frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") + .format(d.idx), alert=1) + def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): supplied_items_cost = 0.0 for d in self.get("supplied_items"): @@ -243,7 +279,7 @@ class BuyingController(StockController): d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount")) supplied_items_cost += flt(d.amount) - + return supplied_items_cost def validate_for_subcontracting(self): @@ -559,6 +595,8 @@ class BuyingController(StockController): from_warehouse_sle = self.get_sl_entries(d, { "actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, + "outgoing_rate": d.rate, + "recalculate_rate": 1, "dependant_sle_voucher_detail_no": d.name }) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 812021f5c86..e085048f99b 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint, flt, cstr, comma_or, get_link_to_form +from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime from frappe import _, throw from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.utils import get_incoming_rate @@ -49,7 +49,6 @@ class SellingController(StockController): self.set_customer_address() self.validate_for_duplicate_items() self.validate_target_warehouse() - self.set_incoming_rate() def set_missing_values(self, for_validate=False): @@ -191,7 +190,7 @@ class SellingController(StockController): for it in self.get("items"): if not it.item_code: continue - + last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1) if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom): @@ -312,7 +311,7 @@ class SellingController(StockController): sales_order.update_reserved_qty(so_item_rows) def set_incoming_rate(self): - if self.doctype not in ("Delivery Note", "Sales Invoice"): + if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"): return items = self.get("items") + (self.get("packed_items") or []) @@ -322,15 +321,26 @@ class SellingController(StockController): d.incoming_rate = get_incoming_rate({ "item_code": d.item_code, "warehouse": d.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1*flt(d.qty), - "serial_no": d.serial_no, + "posting_date": self.get('posting_date') or self.get('transaction_date'), + "posting_time": self.get('posting_time') or nowtime(), + "qty": -1 * flt(d.get('stock_qty') or d.get('actual_qty')), + "serial_no": d.get('serial_no'), "company": self.company, "voucher_type": self.doctype, "voucher_no": self.name, "allow_zero_valuation": d.get("allow_zero_valuation") }, raise_error_if_no_rate=False) + + # For internal transfers use incoming rate as the valuation rate + if self.is_internal_transfer(): + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 + d.discount_amount = 0 + frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") + .format(d.idx), alert=1) + elif self.get("return_against"): # Get incoming rate of return entry from reference document # based on original item cost as per valuation method @@ -391,7 +401,7 @@ class SellingController(StockController): }) if item_row.warehouse: sle.dependant_sle_voucher_detail_no = item_row.name - + return sle def set_po_nos(self, for_validate=False): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e0fcf47365f..4b5e3479706 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -24,6 +24,7 @@ class StockController(AccountsController): self.validate_inspection() self.validate_serialized_batch() self.validate_customer_provided_item() + self.validate_internal_transfer() self.validate_putaway_capacity() def make_gl_entries(self, gl_entries=None, from_repost=False): @@ -74,6 +75,7 @@ class StockController(AccountsController): warehouse_with_no_account = [] precision = frappe.get_precision("GL Entry", "debit_in_account_currency") for item_row in voucher_details: + sle_list = sle_map.get(item_row.name) if sle_list: for sle in sle_list: @@ -218,7 +220,7 @@ class StockController(AccountsController): """, (self.doctype, self.name), as_dict=True) for sle in stock_ledger_entries: - stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) + stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle) return stock_ledger def make_batches(self, warehouse_field): @@ -393,6 +395,32 @@ class StockController(AccountsController): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 + def validate_internal_transfer(self): + if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ + and self.is_internal_transfer(): + self.validate_in_transit_warehouses() + self.validate_multi_currency() + self.validate_packed_items() + + def validate_in_transit_warehouses(self): + if (self.doctype == 'Sales Invoice' and self.get('update_stock')) or self.doctype == 'Delivery Note': + for item in self.get('items'): + if not item.target_warehouse: + frappe.throw(_("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx)) + + if (self.doctype == 'Purchase Invoice' and self.get('update_stock')) or self.doctype == 'Purchase Receipt': + for item in self.get('items'): + if not item.from_warehouse: + frappe.throw(_("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx)) + + def validate_multi_currency(self): + if self.currency != self.company_currency: + frappe.throw(_("Internal transfers can only be done in company's default currency")) + + def validate_packed_items(self): + if self.doctype in ('Sales Invoice', 'Delivery Note Item') and self.get('packed_items'): + frappe.throw(_("Packed Items cannot be transferred internally")) + def validate_putaway_capacity(self): # if over receipt is attempted while 'apply putaway rule' is disabled # and if rule was applied on the transaction, validate it. diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1d1f18b1a0d..9627600a17a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -105,10 +105,18 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.ui.form.on(this.frm.doctype + " Item", { items_add: function(frm, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); - if(!item.warehouse && frm.doc.set_warehouse) { + if (!item.warehouse && frm.doc.set_warehouse) { item.warehouse = frm.doc.set_warehouse; } + if (!item.target_warehouse && frm.doc.set_target_warehouse) { + item.target_warehouse = frm.doc.set_target_warehouse; + } + + if (!item.from_warehouse && frm.doc.set_from_warehouse) { + item.from_warehouse = frm.doc.set_from_warehouse; + } + erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items'); } }); @@ -227,6 +235,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }; + this.frm.trigger('set_default_internal_warehouse'); + return frappe.run_serially([ () => set_value('currency', currency), () => set_value('price_list_currency', currency), @@ -658,7 +668,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ args: item_args }, callback: function(r) { - frappe.model.set_value(item.doctype, item.name, 'rate', r.message); + frappe.model.set_value(item.doctype, item.name, 'rate', r.message * item.conversion_factor); } }); }, @@ -724,6 +734,31 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.calculate_taxes_and_totals(false); }, + update_stock: function() { + this.frm.trigger('set_default_internal_warehouse'); + }, + + set_default_internal_warehouse: function() { + let me = this; + if ((this.frm.doc.doctype === 'Sales Invoice' && me.frm.doc.update_stock) + || this.frm.doc.doctype == 'Delivery Note') { + if (this.frm.doc.is_internal_customer && this.frm.doc.company === this.frm.doc.represents_company) { + frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) { + me.frm.set_value('set_target_warehouse', value.default_in_transit_warehouse); + }); + } + } + + if ((this.frm.doc.doctype === 'Purchase Invoice' && me.frm.doc.update_stock) + || this.frm.doc.doctype == 'Purchase Receipt') { + if (this.frm.doc.is_internal_supplier && this.frm.doc.company === this.frm.doc.represents_company) { + frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) { + me.frm.set_value('set_from_warehouse', value.default_in_transit_warehouse); + }); + } + } + }, + company: function() { var me = this; var set_pricing = function() { @@ -810,7 +845,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ set_party_account(set_pricing); - }) + }); // Get default company billing address in Purchase Invoice, Order and Receipt frappe.call({ @@ -1977,6 +2012,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse); }, + set_target_warehouse: function() { + this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse); + }, + + set_from_warehouse: function() { + this.autofill_warehouse(this.frm.doc.items, "from_warehouse", this.frm.doc.set_from_warehouse); + }, + autofill_warehouse : function (child_table, warehouse_field, warehouse) { if (warehouse && child_table && child_table.length) { let doctype = child_table[0].doctype; diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 770704e595b..808dd5add05 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -276,6 +276,12 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { erpnext.utils.get_shipping_address = function(frm, callback){ if (frm.doc.company) { + if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + frm.doc.internal_order_reference)) { + if (callback) { + return callback(); + } + } frappe.call({ method: "erpnext.accounts.custom.address.get_shipping_address", args: { diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 87baece65d3..f09d3d08ad2 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -40,14 +40,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => { callback: function(r) { if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); + frm.set_value('taxes', r.message.taxes); frm.set_value('place_of_supply', r.message.place_of_supply); - } else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) { - frm.set_value('taxes_and_charges', ''); - frm.set_value('taxes', []); } } }); } }); -}; +} diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0d8263835d2..e89885f3805 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -171,7 +171,7 @@ def get_regional_address_details(party_details, doctype, company): if is_internal_transfer(party_details, doctype): party_details.taxes_and_charges = '' - party_details.taxes = '' + party_details.taxes = [] return party_details if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 29214ee06d9..bf8b7fc128a 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -84,7 +84,10 @@ class Customer(TransactionBase): frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) def validate_internal_customer(self): - if self.is_internal_customer and frappe.db.get_value('Customer', {"represents_company": self.represents_company}, "name"): + internal_customer = frappe.db.get_value("Customer", + {"is_internal_customer": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + + if internal_customer: frappe.throw(_("Internal Customer for company {0} already exists").format( frappe.bold(self.represents_company))) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index d4fb07cc275..78f9df9588a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -171,8 +171,10 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create')); } - // make purchase order + // Make Purchase Order + if (!this.frm.doc.is_internal_customer) { this.frm.add_custom_button(__('Purchase Order'), () => this.make_purchase_order(), __('Create')); + } // maintenance if(flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) { @@ -193,16 +195,15 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( if (doc.docstatus === 1 && !doc.inter_company_order_reference) { let me = this; - frappe.model.with_doc("Customer", me.frm.doc.customer, () => { - let customer = frappe.model.get_doc("Customer", me.frm.doc.customer); - let internal = customer.is_internal_customer; - let disabled = customer.disabled; - if (internal === 1 && disabled === 0) { - me.frm.add_custom_button("Inter Company Order", function() { - me.make_inter_company_order(); - }, __('Create')); - } - }); + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Order" : + "Inter Company Purchase Order"; + + me.frm.add_custom_button(button_label, function() { + me.make_inter_company_order(); + }, __('Create')); + } } } // payment request diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 3d64ac3780e..0a5c6651ba3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -107,6 +107,8 @@ "tc_name", "terms", "more_info", + "is_internal_customer", + "represents_company", "inter_company_order_reference", "project", "party_account_currency", @@ -1103,7 +1105,8 @@ "hide_days": 1, "hide_seconds": 1, "label": "Inter Company Order Reference", - "options": "Purchase Order" + "options": "Purchase Order", + "read_only": 1 }, { "description": "Track this Sales Order against any Project", @@ -1455,13 +1458,29 @@ "hide_seconds": 1, "label": "Skip Delivery Note", "print_hide": 1 + }, + { + "default": "0", + "fetch_from": "customer.is_internal_customer", + "fieldname": "is_internal_customer", + "fieldtype": "Check", + "label": "Is Internal Customer", + "read_only": 1 + }, + { + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:59:18.628077", + "modified": "2021-01-20 23:40:39.929296", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ee18042fdd5..334bdeac9d3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -95,13 +95,19 @@ frappe.ui.form.on("Delivery Note", { frm.page.set_inner_btn_group_as_primary(__('Create')); } - if (frm.doc.docstatus === 1 && frm.doc.is_internal_customer && !frm.doc.inter_company_reference) { - frm.add_custom_button(__('Purchase Receipt'), function() { - frappe.model.open_mapped_doc({ - method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', - frm: frm, - }) - }, __('Create')); + if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) { + let internal = me.frm.doc.is_internal_customer; + if (internal) { + let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" : + "Inter Company Purchase Receipt"; + + me.frm.add_custom_button(button_label, function() { + frappe.model.open_mapped_doc({ + method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', + frm: frm, + }); + }, __('Create')); + } } } }); @@ -297,15 +303,6 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } }) }, - - to_warehouse: function() { - let packed_items_table = this.frm.doc["packed_items"]; - this.autofill_warehouse(this.frm.doc["items"], "target_warehouse", this.frm.doc.to_warehouse); - if (packed_items_table && packed_items_table.length) { - this.autofill_warehouse(packed_items_table, "target_warehouse", this.frm.doc.to_warehouse); - } - } - }); $.extend(cur_frm.cscript, new erpnext.stock.DeliveryNoteController({frm: cur_frm})); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index c9f8d0810e3..f595aade917 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -53,7 +53,7 @@ "sec_warehouse", "set_warehouse", "col_break_warehouse", - "to_warehouse", + "set_target_warehouse", "items_section", "scan_barcode", "items", @@ -117,6 +117,7 @@ "source", "column_break5", "is_internal_customer", + "represents_company", "inter_company_reference", "per_billed", "customer_group", @@ -502,18 +503,6 @@ "fieldname": "col_break_warehouse", "fieldtype": "Column Break" }, - { - "description": "Required only for sample item.", - "fieldname": "to_warehouse", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "To Warehouse", - "no_copy": 1, - "oldfieldname": "to_warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "print_hide": 1 - }, { "fieldname": "items_section", "fieldtype": "Section Break", @@ -1261,13 +1250,34 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.is_internal_customer", + "fieldname": "set_target_warehouse", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Set Target Warehouse", + "no_copy": 1, + "oldfieldname": "to_warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "print_hide": 1 + }, + { + "description": "Company which internal customer represents.", + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-11-30 12:54:45.407289", + "modified": "2020-12-26 17:07:59.194403", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index a30cadf0a04..fa5a7fbe719 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -664,7 +664,8 @@ def make_inter_company_purchase_receipt(source_name, target_doc=None): return make_inter_company_transaction("Delivery Note", source_name, target_doc) def make_inter_company_transaction(doctype, source_name, target_doc=None): - from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_transaction, get_inter_company_details + from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_transaction, + get_inter_company_details, update_address, update_taxes, set_purchase_references) if doctype == 'Delivery Note': source_doc = frappe.get_doc(doctype, source_name) @@ -682,6 +683,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def set_missing_values(source, target): target.run_method("set_missing_values") + set_purchase_references(target) if target.doctype == 'Purchase Receipt': master_doctype = 'Purchase Taxes and Charges Template' @@ -697,21 +699,35 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if target_doc.doctype == 'Purchase Receipt': target_doc.company = details.get("company") target_doc.supplier = details.get("party") - target_doc.supplier_address = source_doc.company_address - target_doc.shipping_address = source_doc.shipping_address_name or source_doc.customer_address target_doc.buying_price_list = source_doc.selling_price_list target_doc.is_internal_supplier = 1 target_doc.inter_company_reference = source_doc.name + + # Invert the address on target doc creation + update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) + update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + + update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address) else: target_doc.company = details.get("company") target_doc.customer = details.get("party") target_doc.company_address = source_doc.supplier_address - target_doc.shipping_address_name = source_doc.shipping_address target_doc.selling_price_list = source_doc.buying_price_list target_doc.is_internal_customer = 1 target_doc.inter_company_reference = source_doc.name - doclist = get_mapped_doc(doctype, source_name, { + # Invert the address on target doc creation + update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) + update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) + update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + + update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, + doctype=target_doc.doctype, party_address=target_doc.customer_address, + company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + + doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, "postprocess": update_details, @@ -722,7 +738,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): doctype +" Item": { "doctype": target_doctype + " Item", "field_map": { - source_document_warehouse_field: target_document_warehouse_field + source_document_warehouse_field: target_document_warehouse_field, + 'name': 'delivery_note_item', + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' }, "field_no_map": [ "warehouse" 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 4bbf3de5940..9de088df0ee 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -458,7 +458,7 @@ "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "From Warehouse", + "label": "Warehouse", "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", @@ -467,11 +467,12 @@ "width": "100px" }, { + "depends_on": "eval:parent.is_internal_customer", "fieldname": "target_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Customer Warehouse (Optional)", + "label": "Target Warehouse", "no_copy": 1, "options": "Warehouse", "print_hide": 1 @@ -748,7 +749,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-07 19:59:27.119856", + "modified": "2020-12-26 17:31:27.029803", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 55f0f0cb269..32d349f3031 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -48,6 +48,7 @@ "set_warehouse", "rejected_warehouse", "col_break_warehouse", + "set_from_warehouse", "is_subcontracted", "supplier_warehouse", "items_section", @@ -115,6 +116,7 @@ "per_returned", "is_internal_supplier", "inter_company_reference", + "represents_company", "subscription_detail", "auto_repeat", "printing_settings", @@ -1087,7 +1089,9 @@ "fieldname": "inter_company_reference", "fieldtype": "Link", "label": "Inter Company Reference", + "no_copy": 1, "options": "Delivery Note", + "print_hide": 1, "read_only": 1 }, { @@ -1121,13 +1125,29 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.is_internal_supplier", + "description": "Sets 'From Warehouse' in each row of the items table.", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "options": "Warehouse" + }, + { + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-12-08 18:31:32.234503", + "modified": "2020-12-26 20:49:39.106049", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 662e50c6931..e99119202e7 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -76,6 +76,7 @@ "purchase_order_item", "material_request_item", "purchase_receipt_item", + "delivery_note_item", "putaway_rule", "section_break_45", "allow_zero_valuation_rate", @@ -819,11 +820,12 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_internal_supplier", "fieldname": "from_warehouse", "fieldtype": "Link", "hidden": 1, "ignore_user_permissions": 1, - "label": "Supplier Warehouse", + "label": "From Warehouse", "options": "Warehouse" }, { @@ -871,12 +873,20 @@ "fieldtype": "Float", "label": "Received Qty in Stock UOM", "print_hide": 1 + }, + { + "fieldname": "delivery_note_item", + "fieldtype": "Data", + "label": "Delivery Note Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-09 10:00:38.204294", + "modified": "2020-12-26 16:50:56.479347", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5b9ada0ee56..2b2a7a202dd 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -41,7 +41,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation": sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) - + args = sle_doc.as_dict() update_bin(args, allow_negative_stock, via_landed_cost_voucher) @@ -65,7 +65,7 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=False, via_landed_cost_voucher=False): if not args and voucher_type and voucher_no: args = get_args_for_voucher(voucher_type, voucher_no) - + distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] i = 0 @@ -80,7 +80,7 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat for item_wh, new_sle in iteritems(obj.new_items): if item_wh not in distinct_item_warehouses: args.append(new_sle) - + i += 1 def get_args_for_voucher(voucher_type, voucher_no): @@ -127,7 +127,7 @@ class update_entries_after(object): self.initialize_previous_data(self.args) self.build() - + def get_precision(self): company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency") self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), @@ -213,13 +213,13 @@ class update_entries_after(object): # includes current entry! args = self.data[self.args.warehouse].previous_sle \ or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse}) - + return list(self.get_sle_after_datetime(args)) def get_dependent_entries_to_fix(self, entries_to_fix, sle): dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name) - + if not dependant_sle: return elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: @@ -251,7 +251,7 @@ class update_entries_after(object): # Get dynamic incoming/outgoing rate self.get_dynamic_incoming_outgoing_rate(sle) - + if sle.serial_no: self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) @@ -329,7 +329,7 @@ class update_entries_after(object): rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, voucher_detail_no=sle.voucher_detail_no) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): - rate_field = "valuation_rate" + rate_field = "valuation_rate" else: rate_field = "incoming_rate" @@ -344,7 +344,7 @@ class update_entries_after(object): ref_doctype = "Packed Item" else: ref_doctype = "Purchase Receipt Item Supplied" - + rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, rate_field) @@ -374,7 +374,7 @@ class update_entries_after(object): stock_entry.db_update() for d in stock_entry.items: d.db_update() - + def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate): # Update item's incoming rate on transaction item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code") @@ -487,7 +487,6 @@ class update_entries_after(object): self.wh_data.valuation_rate = new_stock_value / new_stock_qty else: self.wh_data.valuation_rate = sle.outgoing_rate - else: if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate: self.wh_data.valuation_rate = sle.outgoing_rate @@ -631,7 +630,7 @@ class update_entries_after(object): frappe.throw(message, NegativeStockError, title='Insufficient Stock') else: raise NegativeStockError(message) - + def update_bin(self): # update bin for each warehouse for warehouse, data in iteritems(self.data): @@ -766,7 +765,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): frappe.db.sql(""" update `tabStock Ledger Entry` set qty_after_transaction = qty_after_transaction + {qty} - where + where item_code = %(item_code)s and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s @@ -794,7 +793,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): frappe.get_desk_link('Warehouse', args.warehouse), sle[0]["posting_date"], sle[0]["posting_time"], frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) - + frappe.throw(message, NegativeStockError, title='Insufficient Stock') def get_future_sle_with_negative_qty(args): @@ -803,7 +802,7 @@ def get_future_sle_with_negative_qty(args): qty_after_transaction, posting_date, posting_time, voucher_type, voucher_no from `tabStock Ledger Entry` - where + where item_code = %(item_code)s and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s From 0d9a8ae4d2b8517faa0b05b79dda2828198712ed Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Thu, 28 Jan 2021 17:32:57 +0530 Subject: [PATCH 46/46] feat: shopping portal changes (#24445) * feat: shopping portal changes * fix: review changes * review changes --- erpnext/shopping_cart/cart.py | 7 +++++++ erpnext/templates/pages/cart.html | 3 +++ 2 files changed, 10 insertions(+) diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index c2549fe7dd4..c2d743b2a93 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -180,6 +180,13 @@ def create_lead_for_item_inquiry(lead, subject, message): lead_doc.update(lead) lead_doc.set('lead_owner', '') + if not frappe.db.exists('Lead Source', 'Product Inquiry'): + frappe.get_doc({ + 'doctype': 'Lead Source', + 'source_name' : 'Product Inquiry' + }).insert(ignore_permissions=True) + lead_doc.set('source', 'Product Inquiry') + try: lead_doc.save(ignore_permissions=True) except frappe.exceptions.DuplicateEntryError: diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html index 3033d1587d6..876eaea8e6a 100644 --- a/erpnext/templates/pages/cart.html +++ b/erpnext/templates/pages/cart.html @@ -47,6 +47,9 @@ {% if doc.items %}
    + + {{ _("Continue Shopping") }} + {% if cart_settings.enable_checkout %}