From 2813e5ee2956f4667ecb9e02cb3a824c0fb7a0bd Mon Sep 17 00:00:00 2001 From: ruthra Date: Mon, 13 Dec 2021 12:26:23 +0530 Subject: [PATCH 01/35] feat: new column 'Time taken to Deliver' in sales order analysis --- .../sales_order_analysis/sales_order_analysis.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 82e5d0ce57d..f1edca4cef3 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -61,6 +61,7 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, + IF((SELECT pending_qty) = 0, DATEDIFF(Max(dn.posting_date), so.transaction_date), 0) as time_taken_to_deliver, IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, @@ -70,9 +71,13 @@ def get_data(conditions, filters): so.company, soi.name FROM `tabSales Order` so, - `tabSales Order Item` soi + (`tabSales Order Item` soi LEFT JOIN `tabSales Invoice Item` sii - ON sii.so_detail = soi.name and sii.docstatus = 1 + ON sii.so_detail = soi.name and sii.docstatus = 1) + LEFT JOIN `tabDelivery Note Item` dni + on dni.so_detail = soi.name + RIGHT JOIN `tabDelivery Note` dn + on dni.parent = dn.name and dn.docstatus = 1 WHERE soi.parent = so.name and so.status not in ('Stopped', 'Closed', 'On Hold') @@ -259,6 +264,12 @@ def get_columns(filters): "fieldname": "delay", "fieldtype": "Data", "width": 100 + }, + { + "label": _("Time Taken to Deliver"), + "fieldname": "time_taken_to_deliver", + "fieldtype": "Data", + "width": 100 } ]) if not filters.get("group_by_so"): From 9232f7599870b755c23a7f179306c6eed93f60dc Mon Sep 17 00:00:00 2001 From: ruthra Date: Tue, 14 Dec 2021 19:45:09 +0530 Subject: [PATCH 02/35] refactor: change field to duration and fetch elapsed seconds --- .../report/sales_order_analysis/sales_order_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index f1edca4cef3..0c0acc76e39 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -61,7 +61,7 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, - IF((SELECT pending_qty) = 0, DATEDIFF(Max(dn.posting_date), so.transaction_date), 0) as time_taken_to_deliver, + IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver, IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, @@ -268,7 +268,7 @@ def get_columns(filters): { "label": _("Time Taken to Deliver"), "fieldname": "time_taken_to_deliver", - "fieldtype": "Data", + "fieldtype": "Duration", "width": 100 } ]) From 0f43792dbbc2ef8f4d5e2f288b61034049d09dae Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 20 Dec 2021 21:33:59 +0530 Subject: [PATCH 03/35] fix: Stock Ageing Report - Negative Opening Stock - Consider negative opening stock in logic and neutralise it with +ve stock - minor code refactor: class for FIFOSlots to generate chronological FIFO queue --- .../stock/report/stock_ageing/stock_ageing.py | 323 +++++++++++------- .../report/stock_balance/stock_balance.py | 4 +- ...rehouse_wise_item_balance_age_and_value.py | 4 +- 3 files changed, 206 insertions(+), 125 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 0ebe4f903f1..0136007ca42 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -3,6 +3,7 @@ from operator import itemgetter +from typing import Dict, List, Tuple, Union import frappe from frappe import _ @@ -10,19 +11,29 @@ from frappe.utils import cint, date_diff, flt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +Filters = frappe._dict -def execute(filters=None): - columns = get_columns(filters) - item_details = get_fifo_queue(filters) +def execute(filters: Filters =None) -> Tuple: to_date = filters["to_date"] - _func = itemgetter(1) + columns = get_columns(filters) + item_details = FIFOSlots(filters).generate() + data = format_report_data(filters, item_details, to_date) + + chart_data = get_chart_data(data, filters) + + return columns, data, None, chart_data + +def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]: + "Returns ordered, formatted data with ranges." + _func = itemgetter(1) data = [] + for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 + details = item_dict["details"] fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) - details = item_dict["details"] if not fifo_queue: continue @@ -31,23 +42,22 @@ def execute(filters=None): latest_age = date_diff(to_date, fifo_queue[-1][1]) range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict) - row = [details.name, details.item_name, - details.description, details.item_group, details.brand] + row = [details.name, details.item_name, details.description, + details.item_group, details.brand] if filters.get("show_warehouse_wise_stock"): row.append(details.warehouse) row.extend([item_dict.get("total_qty"), average_age, range1, range2, range3, above_range3, - earliest_age, latest_age, details.stock_uom]) + earliest_age, latest_age, + details.stock_uom]) data.append(row) - chart_data = get_chart_data(data, filters) + return data - return columns, data, None, chart_data - -def get_average_age(fifo_queue, to_date): +def get_average_age(fifo_queue: List, to_date: str) -> float: batch_age = age_qty = total_qty = 0.0 for batch in fifo_queue: batch_age = date_diff(to_date, batch[1]) @@ -61,7 +71,7 @@ def get_average_age(fifo_queue, to_date): return flt(age_qty / total_qty, 2) if total_qty else 0.0 -def get_range_age(filters, fifo_queue, to_date, item_dict): +def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: range1 = range2 = range3 = above_range3 = 0.0 for item in fifo_queue: @@ -79,7 +89,7 @@ def get_range_age(filters, fifo_queue, to_date, item_dict): return range1, range2, range3, above_range3 -def get_columns(filters): +def get_columns(filters: Filters) -> List[Dict]: range_columns = [] setup_ageing_columns(filters, range_columns) columns = [ @@ -164,106 +174,7 @@ def get_columns(filters): return columns -def get_fifo_queue(filters, sle=None): - item_details = {} - transferred_item_details = {} - serial_no_batch_purchase_details = {} - - if sle == None: - sle = get_stock_ledger_entries(filters) - - for d in sle: - key = (d.name, d.warehouse) if filters.get('show_warehouse_wise_stock') else d.name - item_details.setdefault(key, {"details": d, "fifo_queue": []}) - fifo_queue = item_details[key]["fifo_queue"] - - transferred_item_key = (d.voucher_no, d.name, d.warehouse) - transferred_item_details.setdefault(transferred_item_key, []) - - if d.voucher_type == "Stock Reconciliation": - d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0)) - - serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else [] - - if d.actual_qty > 0: - if transferred_item_details.get(transferred_item_key): - batch = transferred_item_details[transferred_item_key][0] - fifo_queue.append(batch) - transferred_item_details[transferred_item_key].pop(0) - else: - if serial_no_list: - for serial_no in serial_no_list: - if serial_no_batch_purchase_details.get(serial_no): - fifo_queue.append([serial_no, serial_no_batch_purchase_details.get(serial_no)]) - else: - serial_no_batch_purchase_details.setdefault(serial_no, d.posting_date) - fifo_queue.append([serial_no, d.posting_date]) - else: - fifo_queue.append([d.actual_qty, d.posting_date]) - else: - if serial_no_list: - fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_no_list] - else: - qty_to_pop = abs(d.actual_qty) - while qty_to_pop: - batch = fifo_queue[0] if fifo_queue else [0, None] - if 0 < flt(batch[0]) <= qty_to_pop: - # if batch qty > 0 - # not enough or exactly same qty in current batch, clear batch - qty_to_pop -= flt(batch[0]) - transferred_item_details[transferred_item_key].append(fifo_queue.pop(0)) - else: - # all from current batch - batch[0] = flt(batch[0]) - qty_to_pop - transferred_item_details[transferred_item_key].append([qty_to_pop, batch[1]]) - qty_to_pop = 0 - - item_details[key]["qty_after_transaction"] = d.qty_after_transaction - - if "total_qty" not in item_details[key]: - item_details[key]["total_qty"] = d.actual_qty - else: - item_details[key]["total_qty"] += d.actual_qty - - item_details[key]["has_serial_no"] = d.has_serial_no - - return item_details - -def get_stock_ledger_entries(filters): - return frappe.db.sql("""select - item.name, item.item_name, item_group, brand, description, item.stock_uom, item.has_serial_no, - actual_qty, posting_date, voucher_type, voucher_no, serial_no, batch_no, qty_after_transaction, warehouse - from `tabStock Ledger Entry` sle, - (select name, item_name, description, stock_uom, brand, item_group, has_serial_no - from `tabItem` {item_conditions}) item - where item_code = item.name and - company = %(company)s and - posting_date <= %(to_date)s and - is_cancelled != 1 - {sle_conditions} - order by posting_date, posting_time, sle.creation, actual_qty""" #nosec - .format(item_conditions=get_item_conditions(filters), - sle_conditions=get_sle_conditions(filters)), filters, as_dict=True) - -def get_item_conditions(filters): - conditions = [] - if filters.get("item_code"): - conditions.append("item_code=%(item_code)s") - if filters.get("brand"): - conditions.append("brand=%(brand)s") - - return "where {}".format(" and ".join(conditions)) if conditions else "" - -def get_sle_conditions(filters): - conditions = [] - if filters.get("warehouse"): - lft, rgt = frappe.db.get_value('Warehouse', filters.get("warehouse"), ['lft', 'rgt']) - conditions.append("""warehouse in (select wh.name from `tabWarehouse` wh - where wh.lft >= {0} and rgt <= {1})""".format(lft, rgt)) - - return "and {}".format(" and ".join(conditions)) if conditions else "" - -def get_chart_data(data, filters): +def get_chart_data(data: List, filters: Filters) -> Dict: if not data: return [] @@ -294,17 +205,187 @@ def get_chart_data(data, filters): "type" : "bar" } -def setup_ageing_columns(filters, range_columns): - for i, label in enumerate(["0-{range1}".format(range1=filters["range1"]), - "{range1}-{range2}".format(range1=cint(filters["range1"])+ 1, range2=filters["range2"]), - "{range2}-{range3}".format(range2=cint(filters["range2"])+ 1, range3=filters["range3"]), - "{range3}-{above}".format(range3=cint(filters["range3"])+ 1, above=_("Above"))]): - add_column(range_columns, label="Age ("+ label +")", fieldname='range' + str(i+1)) +def setup_ageing_columns(filters: Filters, range_columns: List): + ranges = [ + f"0 - {filters['range1']}", + f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}", + f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}", + f"{cint(filters['range3']) + 1} - {_('Above')}" + ] + for i, label in enumerate(ranges): + fieldname = 'range' + str(i+1) + add_column(range_columns, label=f"Age ({label})",fieldname=fieldname) -def add_column(range_columns, label, fieldname, fieldtype='Float', width=140): +def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str ='Float', width: int =140): range_columns.append(dict( label=label, fieldname=fieldname, fieldtype=fieldtype, width=width )) + + +class FIFOSlots: + "Returns FIFO computed slots of inwarded stock as per date." + + def __init__(self, filters: Dict =None , sle: List =None): + self.item_details = {} + self.transferred_item_details = {} + self.serial_no_batch_purchase_details = {} + self.filters = filters + self.sle = sle + + def generate(self) -> Dict: + """ + Returns dict of the foll.g structure: + Key = Item A / (Item A, Warehouse A) + Key: { + 'details' -> Dict: ** item details **, + 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, + consumed/updated and maintained via FIFO. ** + } + """ + if self.sle == None: + self.sle = self.__get_stock_ledger_entries() + + for d in self.sle: + key, fifo_queue, transferred_item_key = self.__init_key_stores(d) + + if d.voucher_type == "Stock Reconciliation": + prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) + d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) + + serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] + + if d.actual_qty > 0: + self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) + else: + self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos) + + self.__update_balances(d, key) + + return self.item_details + + def __init_key_stores(self, row: Dict) -> Tuple: + "Initialise keys and FIFO Queue." + + key = (row.name, row.warehouse) if self.filters.get('show_warehouse_wise_stock') else row.name + self.item_details.setdefault(key, {"details": row, "fifo_queue": []}) + fifo_queue = self.item_details[key]["fifo_queue"] + + transferred_item_key = (row.voucher_no, row.name, row.warehouse) + self.transferred_item_details.setdefault(transferred_item_key, []) + + return key, fifo_queue, transferred_item_key + + def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): + "Update FIFO Queue on inward stock." + + if self.transferred_item_details.get(transfer_key): + # inward/outward from same voucher, item & warehouse + slot = self.transferred_item_details[transfer_key].pop(0) + fifo_queue.append(slot) + else: + if not serial_nos: + if fifo_queue and fifo_queue[0][0] < 0: + # neutralize negative stock by adding positive stock + fifo_queue[0][0] += flt(row.actual_qty) + fifo_queue[0][1] = row.posting_date + else: + fifo_queue.append([row.actual_qty, row.posting_date]) + return + + for serial_no in serial_nos: + if self.serial_no_batch_purchase_details.get(serial_no): + fifo_queue.append([serial_no, self.serial_no_batch_purchase_details.get(serial_no)]) + else: + self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date) + fifo_queue.append([serial_no, row.posting_date]) + + def __compute_outgoing_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): + "Update FIFO Queue on outward stock." + if serial_nos: + fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos] + return + + qty_to_pop = abs(row.actual_qty) + while qty_to_pop: + slot = fifo_queue[0] if fifo_queue else [0, None] + if 0 < flt(slot[0]) <= qty_to_pop: + # qty to pop >= slot qty + # if +ve and not enough or exactly same balance in current slot, consume whole slot + qty_to_pop -= flt(slot[0]) + self.transferred_item_details[transfer_key].append(fifo_queue.pop(0)) + elif not fifo_queue: + # negative stock, no balance but qty yet to consume + fifo_queue.append([-(qty_to_pop), row.posting_date]) + self.transferred_item_details[transfer_key].append([row.actual_qty, row.posting_date]) + qty_to_pop = 0 + else: + # qty to pop < slot qty, ample balance + # consume actual_qty from first slot + slot[0] = flt(slot[0]) - qty_to_pop + self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]]) + qty_to_pop = 0 + + def __update_balances(self, row: Dict, key: Union[Tuple, str]): + self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction + + if "total_qty" not in self.item_details[key]: + self.item_details[key]["total_qty"] = row.actual_qty + else: + self.item_details[key]["total_qty"] += row.actual_qty + + self.item_details[key]["has_serial_no"] = row.has_serial_no + + def __get_stock_ledger_entries(self) -> List[Dict]: + return frappe.db.sql(""" + select + item.name, item.item_name, item_group, brand, description, + item.stock_uom, item.has_serial_no, + actual_qty, posting_date, voucher_type, voucher_no, + serial_no, batch_no, qty_after_transaction, warehouse + from + `tabStock Ledger Entry` sle, + ( + select name, item_name, description, stock_uom, + brand, item_group, has_serial_no + from `tabItem` {item_conditions} + ) item + where + item_code = item.name and + company = %(company)s and + posting_date <= %(to_date)s and + is_cancelled != 1 + {sle_conditions} + order by posting_date, posting_time, sle.creation, actual_qty""" #nosec + .format( + item_conditions=self.__get_item_conditions(), + sle_conditions=self.__get_sle_conditions() + ), + self.filters, + as_dict=True + ) + + def __get_item_conditions(self) -> str: + conditions = [] + if self.filters.get("item_code"): + conditions.append("item_code=%(item_code)s") + if self.filters.get("brand"): + conditions.append("brand=%(brand)s") + + return "where {}".format(" and ".join(conditions)) if conditions else "" + + def __get_sle_conditions(self) -> str: + conditions = [] + + if self.filters.get("warehouse"): + lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ['lft', 'rgt']) + conditions.append(""" + warehouse in ( + select wh.name from `tabWarehouse` wh + where wh.lft >= {0} and rgt <= {1} + ) + """.format(lft, rgt)) + + return "and {}".format(" and ".join(conditions)) if conditions else "" \ No newline at end of file diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 3c7b26bb1b6..c0dc3e89105 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -9,7 +9,7 @@ from frappe import _ from frappe.utils import cint, date_diff, flt, getdate import erpnext -from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, get_fifo_queue +from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, FIFOSlots from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress @@ -33,7 +33,7 @@ def execute(filters=None): if filters.get('show_stock_ageing_data'): filters['show_warehouse_wise_stock'] = True - item_wise_fifo_queue = get_fifo_queue(filters, sle) + item_wise_fifo_queue = FIFOSlots().generate(filters, sle) # if no stock ledger entry found return if not sle: diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index 4d1491b1b55..bd4e235e147 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -9,7 +9,7 @@ import frappe from frappe import _ from frappe.utils import flt -from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, get_fifo_queue +from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, FIFOSlots from erpnext.stock.report.stock_balance.stock_balance import ( get_item_details, get_item_warehouse_map, @@ -33,7 +33,7 @@ def execute(filters=None): item_map = get_item_details(items, sle, filters) iwb_map = get_item_warehouse_map(filters, sle) warehouse_list = get_warehouse_list(filters) - item_ageing = get_fifo_queue(filters) + item_ageing = FIFOSlots().generate(filters) data = [] item_balance = {} item_value = {} From 8951a5c2675bf9903b7b1fb3a4d241ca96456474 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 20 Dec 2021 21:53:47 +0530 Subject: [PATCH 04/35] fix: Linter (imports) --- erpnext/stock/report/stock_balance/stock_balance.py | 2 +- .../warehouse_wise_item_balance_age_and_value.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index c0dc3e89105..44d52f74bdd 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -9,7 +9,7 @@ from frappe import _ from frappe.utils import cint, date_diff, flt, getdate import erpnext -from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, FIFOSlots +from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index bd4e235e147..e5003ee0250 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -9,7 +9,7 @@ import frappe from frappe import _ from frappe.utils import flt -from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, FIFOSlots +from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age from erpnext.stock.report.stock_balance.stock_balance import ( get_item_details, get_item_warehouse_map, From 24a35c69c00565ecd6bf91120a9544c0fb7d5ef4 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 21 Dec 2021 12:32:55 +0530 Subject: [PATCH 05/35] fix: Sider and Server side test - args passed to wrong function - missing space around '=' --- erpnext/stock/report/stock_ageing/stock_ageing.py | 15 ++++++++------- .../stock/report/stock_balance/stock_balance.py | 2 +- .../warehouse_wise_item_balance_age_and_value.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 0136007ca42..75e235ac05c 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -13,7 +13,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict -def execute(filters: Filters =None) -> Tuple: +def execute(filters: Filters = None) -> Tuple: to_date = filters["to_date"] columns = get_columns(filters) @@ -213,10 +213,10 @@ def setup_ageing_columns(filters: Filters, range_columns: List): f"{cint(filters['range3']) + 1} - {_('Above')}" ] for i, label in enumerate(ranges): - fieldname = 'range' + str(i+1) - add_column(range_columns, label=f"Age ({label})",fieldname=fieldname) + fieldname = 'range' + str(i+1) + add_column(range_columns, label=f"Age ({label})",fieldname=fieldname) -def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str ='Float', width: int =140): +def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str = 'Float', width: int = 140): range_columns.append(dict( label=label, fieldname=fieldname, @@ -228,7 +228,7 @@ def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str =' class FIFOSlots: "Returns FIFO computed slots of inwarded stock as per date." - def __init__(self, filters: Dict =None , sle: List =None): + def __init__(self, filters: Dict = None , sle: List = None): self.item_details = {} self.transferred_item_details = {} self.serial_no_batch_purchase_details = {} @@ -245,7 +245,7 @@ class FIFOSlots: consumed/updated and maintained via FIFO. ** } """ - if self.sle == None: + if self.sle is None: self.sle = self.__get_stock_ledger_entries() for d in self.sle: @@ -358,7 +358,8 @@ class FIFOSlots: posting_date <= %(to_date)s and is_cancelled != 1 {sle_conditions} - order by posting_date, posting_time, sle.creation, actual_qty""" #nosec + order by posting_date, posting_time, sle.creation, actual_qty + """ #nosec .format( item_conditions=self.__get_item_conditions(), sle_conditions=self.__get_sle_conditions() diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 44d52f74bdd..b4f43a7fef1 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -33,7 +33,7 @@ def execute(filters=None): if filters.get('show_stock_ageing_data'): filters['show_warehouse_wise_stock'] = True - item_wise_fifo_queue = FIFOSlots().generate(filters, sle) + item_wise_fifo_queue = FIFOSlots(filters, sle).generate() # if no stock ledger entry found return if not sle: diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index e5003ee0250..22bdb891988 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -33,7 +33,7 @@ def execute(filters=None): item_map = get_item_details(items, sle, filters) iwb_map = get_item_warehouse_map(filters, sle) warehouse_list = get_warehouse_list(filters) - item_ageing = FIFOSlots().generate(filters) + item_ageing = FIFOSlots(filters).generate() data = [] item_balance = {} item_value = {} From 12a65eef8a75ae8350eb7487dff2738fc489f2ab Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 17 Dec 2021 15:59:21 +0530 Subject: [PATCH 06/35] fix: Is Reverse Charge check in Tax Category (cherry picked from commit b33fd6acc769dbfaa43c665c19f378e8e041d010) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 7 ++++- .../v13_0/update_tax_category_for_rcm.py | 31 +++++++++++++++++++ erpnext/regional/india/setup.py | 4 ++- erpnext/regional/india/utils.py | 5 +-- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v13_0/update_tax_category_for_rcm.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d9cedab52ac..8e74daa051e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -316,5 +316,10 @@ erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.migrate_crm_settings erpnext.patches.v13_0.rename_ksa_qr_field +<<<<<<< HEAD erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 -erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template \ No newline at end of file +erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template +======= +erpnext.patches.v13_0.disable_ksa_print_format_for_others +erpnext.patches.v13_0.update_tax_category_for_rcm #1 +>>>>>>> b33fd6acc7 (fix: Is Reverse Charge check in Tax Category) diff --git a/erpnext/patches/v13_0/update_tax_category_for_rcm.py b/erpnext/patches/v13_0/update_tax_category_for_rcm.py new file mode 100644 index 00000000000..7af2366bf0a --- /dev/null +++ b/erpnext/patches/v13_0/update_tax_category_for_rcm.py @@ -0,0 +1,31 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + +from erpnext.regional.india import states + + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + create_custom_fields({ + 'Tax Category': [ + dict(fieldname='is_inter_state', label='Is Inter State', + fieldtype='Check', insert_after='disabled', print_hide=1), + dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check', + insert_after='is_inter_state', print_hide=1), + dict(fieldname='tax_category_column_break', fieldtype='Column Break', + insert_after='is_reverse_charge'), + dict(fieldname='gst_state', label='Source State', fieldtype='Select', + options='\n'.join(states), insert_after='company') + ] + }, update=True) + + tax_category = frappe.qb.DocType("Tax Category") + + frappe.qb.update(tax_category).set( + tax_category.is_reverse_charge, 1 + ).where( + tax_category.name.isin(['Reverse Charge Out-State', 'Reverse Charge In-State']) + ).run() \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 58654240285..c0dcb70b92c 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -277,8 +277,10 @@ def get_custom_fields(): inter_state_gst_field = [ dict(fieldname='is_inter_state', label='Is Inter State', fieldtype='Check', insert_after='disabled', print_hide=1), + dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check', + insert_after='is_inter_state', print_hide=1), dict(fieldname='tax_category_column_break', fieldtype='Column Break', - insert_after='is_inter_state'), + insert_after='is_reverse_charge'), dict(fieldname='gst_state', label='Source State', fieldtype='Select', options='\n'.join(states), insert_after='company') ] diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fd3ec3c08ce..215b483c7a5 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -67,7 +67,8 @@ def validate_pan_for_india(doc, method): frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN.")) def validate_tax_category(doc, method): - if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}): + if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state, + 'is_reverse_charge': doc.is_reverse_charge}): if doc.is_inter_state: frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state)) else: @@ -264,7 +265,7 @@ def get_tax_template_based_on_category(master_doctype, company, party_details): def get_tax_template(master_doctype, company, is_inter_state, state_code): tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], - filters = {'is_inter_state': is_inter_state}) + filters = {'is_inter_state': is_inter_state, 'is_reverse_charge': 0}) default_tax = '' From cbef04fdedc6766e2a4373c98b5c4b16bebbfa25 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 17 Dec 2021 16:02:40 +0530 Subject: [PATCH 07/35] chore: Remove patch comment (cherry picked from commit 7c1bfe6b46ac3deedacfa666d0695b53b86ec3f6) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8e74daa051e..407d68384ba 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -321,5 +321,9 @@ erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template ======= erpnext.patches.v13_0.disable_ksa_print_format_for_others +<<<<<<< HEAD erpnext.patches.v13_0.update_tax_category_for_rcm #1 >>>>>>> b33fd6acc7 (fix: Is Reverse Charge check in Tax Category) +======= +erpnext.patches.v13_0.update_tax_category_for_rcm +>>>>>>> 7c1bfe6b46 (chore: Remove patch comment) From 98ac47caa4720ba87b593df0c97c84ec115327a3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 21 Dec 2021 12:54:40 +0530 Subject: [PATCH 08/35] fix: Add is reverse charge in country wise tax (cherry picked from commit 7e912db4b13ee2b1b88a68fc6110eb527a0375d5) --- erpnext/setup/setup_wizard/data/country_wise_tax.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 14b79510c12..91e8eff89fd 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1178,11 +1178,13 @@ { "title": "Reverse Charge In-State", "is_inter_state": 0, + "is_reverse_charge": 1, "gst_state": "" }, { "title": "Reverse Charge Out-State", "is_inter_state": 1, + "is_reverse_charge": 1, "gst_state": "" }, { From 1ed30ee7c7f2f270989ab65ed79e60f06d0a48d0 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Tue, 21 Dec 2021 13:59:53 +0530 Subject: [PATCH 09/35] fix: Reset value_after_depreciation on reversing journal entry during Asset return --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 64712b550f2..321b45323fe 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1049,6 +1049,8 @@ class SalesInvoice(SellingController): frappe.flags.is_reverse_depr_entry = False asset.flags.ignore_validate_update_after_submit = True schedule.journal_entry = None + depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry) + asset.finance_books[0].value_after_depreciation += depreciation_amount asset.save() def get_posting_date_of_sales_invoice(self): @@ -1071,6 +1073,12 @@ class SalesInvoice(SellingController): return False + def get_depreciation_amount_in_je(self, journal_entry): + if journal_entry.accounts[0].debit_in_account_currency: + return journal_entry.accounts[0].debit_in_account_currency + else: + return journal_entry.accounts[0].credit_in_account_currency + @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): From 076cb408db6577d3901e339d298064760f47edb0 Mon Sep 17 00:00:00 2001 From: Development for People <47140294+developmentforpeople@users.noreply.github.com> Date: Tue, 21 Dec 2021 10:18:31 +0000 Subject: [PATCH 10/35] fix: missed colon (#28979) --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 9ceb6267a70..1d11f20ab74 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -374,7 +374,7 @@ scheduler_events = { "erpnext.selling.doctype.quotation.quotation.set_expired_status", "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email", - "erpnext.non_profit.doctype.membership.membership.set_expired_status" + "erpnext.non_profit.doctype.membership.membership.set_expired_status", "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder" ], "daily_long": [ From db7aef2fef14a09053dfffe443b9099dae21c202 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 21 Dec 2021 17:39:27 +0530 Subject: [PATCH 11/35] style: better labels for SLE fields (#28988) * style: better labels for SLE fields * style: rename stock queue field [skip ci] Co-Authored-by: marination --- .../stock_ledger_entry/stock_ledger_entry.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 2651407d16f..46ce9debf3b 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -150,7 +150,7 @@ "fieldtype": "Float", "in_filter": 1, "in_list_view": 1, - "label": "Actual Quantity", + "label": "Qty Change", "oldfieldname": "actual_qty", "oldfieldtype": "Currency", "print_width": "150px", @@ -189,7 +189,7 @@ "fieldname": "qty_after_transaction", "fieldtype": "Float", "in_filter": 1, - "label": "Actual Qty After Transaction", + "label": "Qty After Transaction", "oldfieldname": "bin_aqat", "oldfieldtype": "Currency", "print_width": "150px", @@ -210,7 +210,7 @@ { "fieldname": "stock_value", "fieldtype": "Currency", - "label": "Stock Value", + "label": "Balance Stock Value", "oldfieldname": "stock_value", "oldfieldtype": "Currency", "options": "Company:company:default_currency", @@ -219,14 +219,14 @@ { "fieldname": "stock_value_difference", "fieldtype": "Currency", - "label": "Stock Value Difference", + "label": "Change in Stock Value", "options": "Company:company:default_currency", "read_only": 1 }, { "fieldname": "stock_queue", "fieldtype": "Text", - "label": "Stock Queue (FIFO)", + "label": "FIFO Stock Queue (qty, rate)", "oldfieldname": "fcfs_stack", "oldfieldtype": "Text", "print_hide": 1, @@ -317,10 +317,11 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-10-08 13:42:51.857631", + "modified": "2021-12-21 06:25:30.040801", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -338,5 +339,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" -} + "sort_order": "DESC", + "states": [] +} \ No newline at end of file From 8dc1ce12d4d3590b4496d5aa3f25e9ee89abba89 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 22 Dec 2021 12:12:24 +0530 Subject: [PATCH 12/35] chore: simplify bug report form --- .github/ISSUE_TEMPLATE/bug_report.yaml | 27 ++++---------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index a6e16a03d8d..8f938112a78 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -24,20 +24,6 @@ body: validations: required: true - - type: dropdown - id: version - attributes: - label: Version - description: Affected versions. - multiple: true - options: - - v12 - - v13 - - v14 - - develop - validations: - required: true - - type: dropdown id: module attributes: @@ -86,7 +72,7 @@ body: - manual install - FrappeCloud validations: - required: true + required: false - type: textarea id: logs @@ -95,12 +81,7 @@ body: description: Please copy and paste any relevant log output. This will be automatically formatted. render: shell - - - type: checkboxes - id: terms + - type: markdown attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md) - options: - - label: I agree to follow this project's Code of Conduct - required: true + value: | + By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md) From f6d55346276a81ed5d6d2425cb98015903968ece Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 22 Dec 2021 19:05:18 +0530 Subject: [PATCH 13/35] fix: set resolution by only if not on hold (#28995) --- erpnext/support/doctype/issue/test_issue.py | 1 + .../service_level_agreement/service_level_agreement.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 14cec46ad4f..7a0a5e506fa 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -98,6 +98,7 @@ class TestIssue(TestSetUp): issue.save() self.assertEqual(issue.on_hold_since, frappe.flags.current_time) + self.assertFalse(issue.resolution_by) creation = get_datetime("2020-03-04 5:00") frappe.flags.current_time = get_datetime("2020-03-04 5:00") diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index c94700bdc5f..b3348f1e1e8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -476,7 +476,7 @@ def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): priority = get_response_and_resolution_duration(doc) start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) set_response_by(doc, start_date_time, priority) - if apply_sla_for_resolution: + if apply_sla_for_resolution and not doc.get('on_hold_since'): # resolution_by is reset if on hold set_resolution_by(doc, start_date_time, priority) @@ -624,9 +624,6 @@ def reset_resolution_metrics(doc): if doc.meta.has_field("user_resolution_time"): doc.user_resolution_time = None - if doc.meta.has_field("agreement_status"): - doc.agreement_status = "First Response Due" - # called via hooks on communication update def on_communication_update(doc, status): From 7ad149f9fec06cd730d9734f3515608c1e4c1ed8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 23 Dec 2021 14:39:20 +0530 Subject: [PATCH 14/35] fix: Start date validation for deferred invoices --- erpnext/controllers/accounts_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 2c92820a74d..c8627740605 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -184,6 +184,8 @@ class AccountsController(TransactionBase): frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) elif getdate(self.posting_date) > getdate(d.service_end_date): frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) + elif getdate(self.posting_date) > getdate(d.service_start_date): + frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx)) def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() From ae929d7a63352e05a70c31ac182611f8c9fb89fb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 23 Dec 2021 19:18:05 +0530 Subject: [PATCH 15/35] fix(test): Leave Allocation validation against Leave Application after submit (#29005) * fix(test): Leave Allocation validation against Leave Application after submit * chore: clean-up Leave Allocation tests * fix(test): set holiday list for leave allocation test --- .../leave_allocation/test_leave_allocation.py | 72 +++++++++---------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 46401a2dd8c..6dbe2eca320 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -4,6 +4,7 @@ import frappe from frappe.utils import add_days, add_months, getdate, nowdate import erpnext +from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type @@ -11,18 +12,25 @@ from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type class TestLeaveAllocation(unittest.TestCase): @classmethod def setUpClass(cls): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + frappe.db.sql("delete from `tabLeave Period`") + emp_id = make_employee("test_emp_leave_allocation@salary.com") + cls.employee = frappe.get_doc("Employee", emp_id) + + make_holiday_list() + frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") + + def tearDown(self): + frappe.db.rollback() def test_overlapping_allocation(self): - frappe.db.sql("delete from `tabLeave Allocation`") - - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) leaves = [ { "doctype": "Leave Allocation", "__islocal": 1, - "employee": employee.name, - "employee_name": employee.employee_name, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, "leave_type": "_Test Leave Type", "from_date": getdate("2015-10-01"), "to_date": getdate("2015-10-31"), @@ -32,8 +40,8 @@ class TestLeaveAllocation(unittest.TestCase): { "doctype": "Leave Allocation", "__islocal": 1, - "employee": employee.name, - "employee_name": employee.employee_name, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, "leave_type": "_Test Leave Type", "from_date": getdate("2015-09-01"), "to_date": getdate("2015-11-30"), @@ -45,40 +53,36 @@ class TestLeaveAllocation(unittest.TestCase): self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save) def test_invalid_period(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - doc = frappe.get_doc({ "doctype": "Leave Allocation", "__islocal": 1, - "employee": employee.name, - "employee_name": employee.employee_name, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, "leave_type": "_Test Leave Type", "from_date": getdate("2015-09-30"), "to_date": getdate("2015-09-1"), "new_leaves_allocated": 5 }) - #invalid period + # invalid period self.assertRaises(frappe.ValidationError, doc.save) def test_allocated_leave_days_over_period(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) doc = frappe.get_doc({ "doctype": "Leave Allocation", "__islocal": 1, - "employee": employee.name, - "employee_name": employee.employee_name, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, "leave_type": "_Test Leave Type", "from_date": getdate("2015-09-1"), "to_date": getdate("2015-09-30"), "new_leaves_allocated": 35 }) - #allocated leave more than period + + # allocated leave more than period self.assertRaises(frappe.ValidationError, doc.save) def test_carry_forward_calculation(self): - frappe.db.sql("delete from `tabLeave Allocation`") - frappe.db.sql("delete from `tabLeave Ledger Entry`") leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1) leave_type.maximum_carry_forwarded_leaves = 10 leave_type.max_leaves_allowed = 30 @@ -114,8 +118,6 @@ class TestLeaveAllocation(unittest.TestCase): self.assertEqual(leave_allocation_2.unused_leaves, 5) def test_carry_forward_leaves_expiry(self): - frappe.db.sql("delete from `tabLeave Allocation`") - frappe.db.sql("delete from `tabLeave Ledger Entry`") leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, @@ -151,8 +153,6 @@ class TestLeaveAllocation(unittest.TestCase): self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated) def test_creation_of_leave_ledger_entry_on_submit(self): - frappe.db.sql("delete from `tabLeave Allocation`") - leave_allocation = create_leave_allocation() leave_allocation.submit() @@ -168,9 +168,6 @@ class TestLeaveAllocation(unittest.TestCase): self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) def test_leave_addition_after_submit(self): - frappe.db.sql("delete from `tabLeave Allocation`") - frappe.db.sql("delete from `tabLeave Ledger Entry`") - leave_allocation = create_leave_allocation() leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) @@ -179,8 +176,6 @@ class TestLeaveAllocation(unittest.TestCase): self.assertTrue(leave_allocation.total_leaves_allocated, 40) def test_leave_subtraction_after_submit(self): - frappe.db.sql("delete from `tabLeave Allocation`") - frappe.db.sql("delete from `tabLeave Ledger Entry`") leave_allocation = create_leave_allocation() leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) @@ -188,17 +183,14 @@ class TestLeaveAllocation(unittest.TestCase): leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 10) - def test_against_leave_application_validation_after_submit(self): - frappe.db.sql("delete from `tabLeave Allocation`") - frappe.db.sql("delete from `tabLeave Ledger Entry`") - + def test_validation_against_leave_application_after_submit(self): leave_allocation = create_leave_allocation() leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + leave_application = frappe.get_doc({ "doctype": 'Leave Application', - "employee": employee.name, + "employee": self.employee.name, "leave_type": "_Test Leave Type", "from_date": add_months(nowdate(), 2), "to_date": add_months(add_days(nowdate(), 10), 2), @@ -208,15 +200,20 @@ class TestLeaveAllocation(unittest.TestCase): "leave_approver": 'test@example.com' }) leave_application.submit() - leave_allocation.new_leaves_allocated = 8 - leave_allocation.total_leaves_allocated = 8 + leave_application.reload() + + # allocate less leaves than the ones which are already approved + leave_allocation.new_leaves_allocated = leave_application.total_leave_days - 1 + leave_allocation.total_leaves_allocated = leave_application.total_leave_days - 1 self.assertRaises(frappe.ValidationError, leave_allocation.submit) def create_leave_allocation(**args): args = frappe._dict(args) - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - leave_allocation = frappe.get_doc({ + emp_id = make_employee("test_emp_leave_allocation@salary.com") + employee = frappe.get_doc("Employee", emp_id) + + return frappe.get_doc({ "doctype": "Leave Allocation", "__islocal": 1, "employee": args.employee or employee.name, @@ -227,6 +224,5 @@ def create_leave_allocation(**args): "carry_forward": args.carry_forward or 0, "to_date": args.to_date or add_months(nowdate(), 12) }) - return leave_allocation test_dependencies = ["Employee", "Leave Type"] From 7987a4650947a2ac551b9b76fdca6bb5b4836295 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 23 Dec 2021 21:13:22 +0530 Subject: [PATCH 16/35] chore: add running stock value difference in invariant report (#29012) [skip ci] --- .../stock_ledger_invariant_check.js | 3 ++- .../stock_ledger_invariant_check.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js index c484516a163..31f389f236e 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js @@ -8,7 +8,8 @@ const DIFFERNCE_FIELD_NAMES = [ "fifo_value_diff", "fifo_valuation_diff", "valuation_diff", - "fifo_difference_diff" + "fifo_difference_diff", + "diff_value_diff" ]; frappe.query_reports["Stock Ledger Invariant Check"] = { diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index ca47a1ec5b8..48753b0edd4 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -50,6 +50,7 @@ def get_stock_ledger_entries(filters): def add_invariant_check_fields(sles): balance_qty = 0.0 + balance_stock_value = 0.0 for idx, sle in enumerate(sles): queue = json.loads(sle.stock_queue) @@ -60,6 +61,7 @@ def add_invariant_check_fields(sles): fifo_value += qty * rate balance_qty += sle.actual_qty + balance_stock_value += sle.stock_value_difference if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: balance_qty = sle.qty_after_transaction @@ -70,6 +72,7 @@ def add_invariant_check_fields(sles): sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None ) sle.expected_qty_after_transaction = balance_qty + sle.stock_value_from_diff = balance_stock_value # set difference fields sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction @@ -81,6 +84,7 @@ def add_invariant_check_fields(sles): sle.valuation_diff = ( sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None ) + sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value if idx > 0: sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value @@ -191,12 +195,21 @@ def get_columns(): "fieldtype": "Float", "label": "D - E", }, - { "fieldname": "stock_value_difference", "fieldtype": "Float", "label": "(F) Stock Value Difference", }, + { + "fieldname": "stock_value_from_diff", + "fieldtype": "Float", + "label": "Balance Stock Value using (F)", + }, + { + "fieldname": "diff_value_diff", + "fieldtype": "Float", + "label": "K - D", + }, { "fieldname": "fifo_stock_diff", "fieldtype": "Float", From a6d6de6c4f8a7ba676294bc94cf122d7a82f020c Mon Sep 17 00:00:00 2001 From: Anupam Date: Fri, 24 Dec 2021 15:34:24 +0530 Subject: [PATCH 17/35] fix: grouping project form custom buttons --- erpnext/projects/doctype/project/project.js | 44 +++++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 31460f66ea3..dc7cf81c76a 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -59,22 +59,16 @@ frappe.ui.form.on("Project", { frm.trigger('show_dashboard'); } - frm.events.set_buttons(frm); + frm.trigger("set_custom_buttons"); }, - set_buttons: function(frm) { + set_custom_buttons: function(frm) { if (!frm.is_new()) { frm.add_custom_button(__('Duplicate Project with Tasks'), () => { frm.events.create_duplicate(frm); - }); + }, __("Actions")); - frm.add_custom_button(__('Completed'), () => { - frm.events.set_status(frm, 'Completed'); - }, __('Set Status')); - - frm.add_custom_button(__('Cancelled'), () => { - frm.events.set_status(frm, 'Cancelled'); - }, __('Set Status')); + frm.trigger("set_project_status_button"); if (frappe.model.can_read("Task")) { @@ -83,7 +77,7 @@ frappe.ui.form.on("Project", { "project": frm.doc.name }; frappe.set_route("List", "Task", "Gantt"); - }); + }, __("View")); frm.add_custom_button(__("Kanban Board"), () => { frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', { @@ -91,13 +85,35 @@ frappe.ui.form.on("Project", { }).then(() => { frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name); }); - }); + }, __("View")); } } }, + set_project_status_button: function(frm) { + frm.add_custom_button(__('Set Project Status'), () => { + let d = new frappe.ui.Dialog({ + "title": "Set Project Status", + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "reqd": 1, + "options": "Completed\nCancelled", + }, + ], + primary_action: function() { + frm.events.set_status(frm, d.get_values().status); + d.hide(); + }, + primary_action_label: __("Set Project Status") + }).show(); + }, __("Actions")); + }, + create_duplicate: function(frm) { return new Promise(resolve => { frappe.prompt('Project Name', (data) => { @@ -117,7 +133,9 @@ frappe.ui.form.on("Project", { set_status: function(frm, status) { frappe.confirm(__('Set Project and all Tasks to status {0}?', [status.bold()]), () => { frappe.xcall('erpnext.projects.doctype.project.project.set_project_status', - {project: frm.doc.name, status: status}).then(() => { /* page will auto reload */ }); + {project: frm.doc.name, status: status}).then(() => { + frm.reload_doc() + }); }); }, From 9c0f6d1306ac352bbc7314f1d03b662f7f0b785d Mon Sep 17 00:00:00 2001 From: Anupam Date: Fri, 24 Dec 2021 16:51:50 +0530 Subject: [PATCH 18/35] fix: sider issue --- erpnext/projects/doctype/project/project.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index dc7cf81c76a..6399a50f481 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -133,9 +133,9 @@ frappe.ui.form.on("Project", { set_status: function(frm, status) { frappe.confirm(__('Set Project and all Tasks to status {0}?', [status.bold()]), () => { frappe.xcall('erpnext.projects.doctype.project.project.set_project_status', - {project: frm.doc.name, status: status}).then(() => { - frm.reload_doc() - }); + {project: frm.doc.name, status: status}).then(() => { + frm.reload_doc(); + }); }); }, From 6eeea0a77a0fbb9d014831250c7f1e917b769903 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 24 Dec 2021 17:20:23 +0530 Subject: [PATCH 19/35] fix: ignore links while setting default notification templates in Settings (#29019) --- .../patches/v11_0/add_default_dispatch_notification_template.py | 1 + .../v13_0/add_default_interview_notification_templates.py | 1 + .../add_default_exit_questionnaire_notification_template.py | 1 + 3 files changed, 3 insertions(+) diff --git a/erpnext/patches/v11_0/add_default_dispatch_notification_template.py b/erpnext/patches/v11_0/add_default_dispatch_notification_template.py index 08006ad01b1..c7771a5f191 100644 --- a/erpnext/patches/v11_0/add_default_dispatch_notification_template.py +++ b/erpnext/patches/v11_0/add_default_dispatch_notification_template.py @@ -22,4 +22,5 @@ def execute(): delivery_settings = frappe.get_doc("Delivery Settings") delivery_settings.dispatch_template = _("Dispatch Notification") + delivery_settings.flags.ignore_links = True delivery_settings.save() diff --git a/erpnext/patches/v13_0/add_default_interview_notification_templates.py b/erpnext/patches/v13_0/add_default_interview_notification_templates.py index 0208ca914eb..6b5de52e2b3 100644 --- a/erpnext/patches/v13_0/add_default_interview_notification_templates.py +++ b/erpnext/patches/v13_0/add_default_interview_notification_templates.py @@ -32,4 +32,5 @@ def execute(): hr_settings = frappe.get_doc('HR Settings') hr_settings.interview_reminder_template = _('Interview Reminder') hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder') + hr_settings.flags.ignore_links = True hr_settings.save() diff --git a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py index 8b1752b2c73..120182a80e3 100644 --- a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py +++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py @@ -24,4 +24,5 @@ def execute(): hr_settings = frappe.get_doc("HR Settings") hr_settings.exit_questionnaire_notification_template = template + hr_settings.flags.ignore_links = True hr_settings.save() From 9c4455f77c133b142d40397141240b86e836bd70 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 24 Dec 2021 18:52:35 +0530 Subject: [PATCH 20/35] fix: Removed ERPNext Integration Settings Workspace (#29023) --- .../erpnext_integrations_settings.json | 78 ------------------- erpnext/patches.txt | 3 +- 2 files changed, 2 insertions(+), 79 deletions(-) delete mode 100644 erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json deleted file mode 100644 index 5efafd67fe8..00000000000 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "charts": [], - "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Integrations Settings\", \"col\": 4}}]", - "creation": "2020-07-31 10:38:54.021237", - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "setting", - "idx": 0, - "label": "ERPNext Integrations Settings", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Integrations Settings", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Woocommerce Settings", - "link_count": 0, - "link_to": "Woocommerce Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Amazon MWS Settings", - "link_count": 0, - "link_to": "Amazon MWS Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Plaid Settings", - "link_count": 0, - "link_to": "Plaid Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Exotel Settings", - "link_count": 0, - "link_to": "Exotel Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2021-11-23 04:30:33.106991", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "ERPNext Integrations Settings", - "owner": "Administrator", - "parent_page": "", - "public": 1, - "restrict_to_domain": "", - "roles": [], - "sequence_id": 11, - "shortcuts": [], - "title": "ERPNext Integrations Settings" -} \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d9cedab52ac..c75606afaaf 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -317,4 +317,5 @@ erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.migrate_crm_settings erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 -erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template \ No newline at end of file +erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template +execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') \ No newline at end of file From cfb6b26e4776d164b4c9786bfe3cf517abe1432e Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Fri, 24 Dec 2021 19:13:28 +0530 Subject: [PATCH 21/35] fix: contact duplication on converting lead to customer (#29001) * fix: contact duplication on converting lead to customer * fix: added test case --- erpnext/crm/doctype/lead/test_lead.py | 11 ++++++++++ erpnext/selling/doctype/customer/customer.py | 23 ++++++++++---------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/erpnext/crm/doctype/lead/test_lead.py b/erpnext/crm/doctype/lead/test_lead.py index 56bfc8f1457..3882974022a 100644 --- a/erpnext/crm/doctype/lead/test_lead.py +++ b/erpnext/crm/doctype/lead/test_lead.py @@ -23,6 +23,17 @@ class TestLead(unittest.TestCase): customer.customer_group = "_Test Customer Group" customer.insert() + #check whether lead contact is carried forward to the customer. + contact = frappe.db.get_value('Dynamic Link', { + "parenttype": "Contact", + "link_doctype": "Lead", + "link_name": customer.lead_name, + }, "parent") + + if contact: + contact_doc = frappe.get_doc("Contact", contact) + self.assertEqual(contact_doc.has_link(customer.doctype, customer.name), True) + def test_make_customer_from_organization(self): from erpnext.crm.doctype.lead.lead import make_customer diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 0c8c53aabe3..b7f74df105d 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -196,20 +196,19 @@ class Customer(TransactionBase): if not lead.lead_name: frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name)) - if lead.company_name: - contact_names = frappe.get_all('Dynamic Link', filters={ - "parenttype":"Contact", - "link_doctype":"Lead", - "link_name":self.lead_name - }, fields=["parent as name"]) + contact_names = frappe.get_all('Dynamic Link', filters={ + "parenttype":"Contact", + "link_doctype":"Lead", + "link_name":self.lead_name + }, fields=["parent as name"]) - for contact_name in contact_names: - contact = frappe.get_doc('Contact', contact_name.get('name')) - if not contact.has_link('Customer', self.name): - contact.append('links', dict(link_doctype='Customer', link_name=self.name)) - contact.save(ignore_permissions=self.flags.ignore_permissions) + for contact_name in contact_names: + contact = frappe.get_doc('Contact', contact_name.get('name')) + if not contact.has_link('Customer', self.name): + contact.append('links', dict(link_doctype='Customer', link_name=self.name)) + contact.save(ignore_permissions=self.flags.ignore_permissions) - else: + if not contact_names: lead.lead_name = lead.lead_name.lstrip().split(" ") lead.first_name = lead.lead_name[0] lead.last_name = " ".join(lead.lead_name[1:]) From ab09dc545e77e93a918cea6fd93ab3c730739d9b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 20 Dec 2021 12:16:14 +0530 Subject: [PATCH 22/35] fix(UX): Optimize rate updation of changing price list (cherry picked from commit 6087d5a6038d6e636ce1ba006ebd59e820b3cd8e) --- erpnext/public/js/controllers/transaction.js | 7 +++++-- erpnext/stock/get_item_details.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 773d53c5521..2019bc0ae6f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1586,17 +1586,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var items_rule_dict = {}; for(var i=0, l=children.length; i Date: Mon, 20 Dec 2021 13:26:16 +0530 Subject: [PATCH 23/35] fix: Linting issues (cherry picked from commit 0980c2f9816b3e4a11b5410b5997a72b1cad28fd) --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2019bc0ae6f..92a6113e0f9 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1587,7 +1587,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe for(var i=0, l=children.length; i Date: Wed, 22 Dec 2021 11:26:19 +0530 Subject: [PATCH 24/35] fix: Recalculate taxes irrespective of price list rate changed or not (cherry picked from commit 233f79bf960381b1c2bd753d783afd3020b377e0) # Conflicts: # erpnext/public/js/controllers/transaction.js --- erpnext/public/js/controllers/transaction.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 92a6113e0f9..d04802b2fba 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1582,7 +1582,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe _set_values_for_item_list(children) { var me = this; - var price_list_rate_changed = false; var items_rule_dict = {}; for(var i=0, l=children.length; i>>>>>> 233f79bf96 (fix: Recalculate taxes irrespective of price list rate changed or not) apply_rule_on_other_items(args) { const me = this; From 2d0208ba1fc6808cad18de509ebb2e2cfa5cd152 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 22 Dec 2021 12:01:39 +0530 Subject: [PATCH 25/35] fix: Add round floats for price list rate (cherry picked from commit b60fbf5ba95350e79463f922b0e8dce518780383) --- erpnext/public/js/controllers/transaction.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index d04802b2fba..794e912edac 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -680,7 +680,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]); - // check if child doctype is Sales Order Item/Qutation Item and calculate the rate + // check if child doctype is Sales Order Item/Quotation Item and calculate the rate if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Purchase Receipt Item"]), cdt) this.apply_pricing_rule_on_item(item); else @@ -1601,6 +1601,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + frappe.model.round_floats_in(item_row, ["price_list_rate", "discount_percentage"]); + // if pricing rule set as blank from an existing value, apply price_list if(!me.frm.doc.ignore_pricing_rule && existing_pricing_rule && !d.pricing_rules) { me.apply_price_list(frappe.get_doc(d.doctype, d.name)); From 6eb3d6a814a1a86a6d606946779278488c0a8b68 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 25 Dec 2021 09:04:34 +0530 Subject: [PATCH 26/35] fix: Conflicts --- erpnext/public/js/controllers/transaction.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 794e912edac..37917416635 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1622,13 +1622,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe me.frm.refresh_field('items'); me.apply_rule_on_other_items(items_rule_dict); -<<<<<<< HEAD - if(!price_list_rate_changed) me.calculate_taxes_and_totals(); - } -======= me.calculate_taxes_and_totals(); - }, ->>>>>>> 233f79bf96 (fix: Recalculate taxes irrespective of price list rate changed or not) + } apply_rule_on_other_items(args) { const me = this; From 52397c97714fd5fd5ef75b32908006fc7c9af1e0 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 26 Dec 2021 02:15:57 +0100 Subject: [PATCH 27/35] fix: avoid `"string" in None` condition --- erpnext/patches/v12_0/create_itc_reversal_custom_fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py index d157aad8f2d..d4fbded5a34 100644 --- a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py +++ b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py @@ -97,6 +97,8 @@ def execute(): 'itc_central_tax': 0, 'itc_cess_amount': 0 }) + if not gst_accounts: + continue if d.account_head in gst_accounts.get('igst_account'): amount_map[d.parent]['itc_integrated_tax'] += d.amount From 8e4ea7e99790a2fbe83a4462fe6020eead87c07f Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 26 Dec 2021 10:23:36 +0530 Subject: [PATCH 28/35] fix: Merge Conflicts --- erpnext/patches.txt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 407d68384ba..47be13087a7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -316,14 +316,7 @@ erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.migrate_crm_settings erpnext.patches.v13_0.rename_ksa_qr_field -<<<<<<< HEAD erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template -======= -erpnext.patches.v13_0.disable_ksa_print_format_for_others -<<<<<<< HEAD -erpnext.patches.v13_0.update_tax_category_for_rcm #1 ->>>>>>> b33fd6acc7 (fix: Is Reverse Charge check in Tax Category) -======= erpnext.patches.v13_0.update_tax_category_for_rcm ->>>>>>> 7c1bfe6b46 (chore: Remove patch comment) + From 21b07385baa4855972637b07b1129cdd65196f94 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Dec 2021 16:57:36 +0530 Subject: [PATCH 29/35] fix: flaky HR tests (#29017) * fix(test): use root company in Expense Claim tests * fix(test): set Holiday List for Leave Allocation * fix(test): set Holiday List for company --- .../expense_claim/test_expense_claim.py | 24 +++++----- .../doctype/expense_claim/test_records.json | 1 - .../leave_allocation/test_leave_allocation.py | 45 ++++++++++++++----- 3 files changed, 48 insertions(+), 22 deletions(-) delete mode 100644 erpnext/hr/doctype/expense_claim/test_records.json diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index ec703614c82..2a079201b76 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -10,15 +10,17 @@ from erpnext.accounts.doctype.account.test_account import create_account from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry -test_records = frappe.get_test_records('Expense Claim') test_dependencies = ['Employee'] -company_name = '_Test Company 4' +company_name = '_Test Company 3' class TestExpenseClaim(unittest.TestCase): + def tearDown(self): + frappe.db.rollback() + def test_total_expense_claim_for_project(self): - frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """) - frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """) + frappe.db.sql("""delete from `tabTask`""") + frappe.db.sql("""delete from `tabProject`""") frappe.db.sql("update `tabExpense Claim` set project = '', task = ''") project = frappe.get_doc({ @@ -37,12 +39,12 @@ class TestExpenseClaim(unittest.TestCase): task_name = task.name payable_account = get_payable_account(company_name) - make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name) + make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200) - expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name) + expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700) self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700) @@ -54,7 +56,7 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_status(self): payable_account = get_payable_account(company_name) - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4") + expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3") je_dict = make_bank_entry("Expense Claim", expense_claim.name) je = frappe.get_doc(je_dict) @@ -73,7 +75,7 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_gl_entry(self): payable_account = get_payable_account(company_name) taxes = generate_taxes() - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", + expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", do_not_submit=True, taxes=taxes) expense_claim.submit() @@ -84,9 +86,9 @@ class TestExpenseClaim(unittest.TestCase): self.assertTrue(gl_entries) expected_values = dict((d[0], d) for d in [ - ['Output Tax CGST - _TC4',18.0, 0.0], + ['Output Tax CGST - _TC3',18.0, 0.0], [payable_account, 0.0, 218.0], - ["Travel Expenses - _TC4", 200.0, 0.0] + ["Travel Expenses - _TC3", 200.0, 0.0] ]) for gle in gl_entries: @@ -102,7 +104,7 @@ class TestExpenseClaim(unittest.TestCase): "payable_account": payable_account, "approval_status": "Rejected", "expenses": - [{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }] + [{"expense_type": "Travel", "default_account": "Travel Expenses - _TC3", "amount": 300, "sanctioned_amount": 200}] }) expense_claim.submit() diff --git a/erpnext/hr/doctype/expense_claim/test_records.json b/erpnext/hr/doctype/expense_claim/test_records.json deleted file mode 100644 index fe51488c706..00000000000 --- a/erpnext/hr/doctype/expense_claim/test_records.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 6dbe2eca320..1fe91399a0c 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -12,15 +12,11 @@ from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type class TestLeaveAllocation(unittest.TestCase): @classmethod def setUpClass(cls): - from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list - frappe.db.sql("delete from `tabLeave Period`") + emp_id = make_employee("test_emp_leave_allocation@salary.com") cls.employee = frappe.get_doc("Employee", emp_id) - make_holiday_list() - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") - def tearDown(self): frappe.db.rollback() @@ -90,6 +86,8 @@ class TestLeaveAllocation(unittest.TestCase): # initial leave allocation = 15 leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, leave_type="_Test_CF_leave", from_date=add_months(nowdate(), -12), to_date=add_months(nowdate(), -1), @@ -99,6 +97,8 @@ class TestLeaveAllocation(unittest.TestCase): # carry forwarded leaves considering maximum_carry_forwarded_leaves # new_leaves = 15, carry_forwarded = 10 leave_allocation_1 = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, leave_type="_Test_CF_leave", carry_forward=1) leave_allocation_1.submit() @@ -110,6 +110,8 @@ class TestLeaveAllocation(unittest.TestCase): # carry forwarded leaves considering max_leave_allowed # max_leave_allowed = 30, new_leaves = 25, carry_forwarded = 5 leave_allocation_2 = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, leave_type="_Test_CF_leave", carry_forward=1, new_leaves_allocated=25) @@ -126,6 +128,8 @@ class TestLeaveAllocation(unittest.TestCase): # initial leave allocation leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, leave_type="_Test_CF_leave_expiry", from_date=add_months(nowdate(), -24), to_date=add_months(nowdate(), -12), @@ -133,6 +137,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_allocation.submit() leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, leave_type="_Test_CF_leave_expiry", from_date=add_days(nowdate(), -90), to_date=add_days(nowdate(), 100), @@ -144,6 +150,8 @@ class TestLeaveAllocation(unittest.TestCase): # leave allocation with carry forward of only new leaves allocated leave_allocation_1 = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, leave_type="_Test_CF_leave_expiry", carry_forward=1, from_date=add_months(nowdate(), 6), @@ -153,7 +161,10 @@ class TestLeaveAllocation(unittest.TestCase): self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated) def test_creation_of_leave_ledger_entry_on_submit(self): - leave_allocation = create_leave_allocation() + leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name + ) leave_allocation.submit() leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name)) @@ -168,7 +179,10 @@ class TestLeaveAllocation(unittest.TestCase): self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) def test_leave_addition_after_submit(self): - leave_allocation = create_leave_allocation() + leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name + ) leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) leave_allocation.new_leaves_allocated = 40 @@ -176,7 +190,10 @@ class TestLeaveAllocation(unittest.TestCase): self.assertTrue(leave_allocation.total_leaves_allocated, 40) def test_leave_subtraction_after_submit(self): - leave_allocation = create_leave_allocation() + leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name + ) leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) leave_allocation.new_leaves_allocated = 10 @@ -184,7 +201,15 @@ class TestLeaveAllocation(unittest.TestCase): self.assertTrue(leave_allocation.total_leaves_allocated, 10) def test_validation_against_leave_application_after_submit(self): - leave_allocation = create_leave_allocation() + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + + make_holiday_list() + frappe.db.set_value("Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List") + + leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name + ) leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) @@ -194,7 +219,7 @@ class TestLeaveAllocation(unittest.TestCase): "leave_type": "_Test Leave Type", "from_date": add_months(nowdate(), 2), "to_date": add_months(add_days(nowdate(), 10), 2), - "company": erpnext.get_default_company() or "_Test Company", + "company": self.employee.company, "docstatus": 1, "status": "Approved", "leave_approver": 'test@example.com' From 878fd377c2584f80022dc96e899261e2bcbe493f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 27 Dec 2021 21:09:05 +0530 Subject: [PATCH 30/35] chore: Use frappe.qb for query --- .../stock/report/stock_ageing/stock_ageing.py | 108 ++++++++++-------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 75e235ac05c..8062a1c98ef 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -287,7 +287,7 @@ class FIFOSlots: fifo_queue.append(slot) else: if not serial_nos: - if fifo_queue and fifo_queue[0][0] < 0: + if fifo_queue and flt(fifo_queue[0][0]) < 0: # neutralize negative stock by adding positive stock fifo_queue[0][0] += flt(row.actual_qty) fifo_queue[0][1] = row.posting_date @@ -339,54 +339,68 @@ class FIFOSlots: self.item_details[key]["has_serial_no"] = row.has_serial_no def __get_stock_ledger_entries(self) -> List[Dict]: - return frappe.db.sql(""" - select - item.name, item.item_name, item_group, brand, description, + sle = frappe.qb.DocType("Stock Ledger Entry") + item = self.__get_item_query() # used as derived table in sle query + + sle_query = ( + frappe.qb.from_(sle).from_(item) + .select( + item.name, item.item_name, item.item_group, + item.brand, item.description, item.stock_uom, item.has_serial_no, - actual_qty, posting_date, voucher_type, voucher_no, - serial_no, batch_no, qty_after_transaction, warehouse - from - `tabStock Ledger Entry` sle, - ( - select name, item_name, description, stock_uom, - brand, item_group, has_serial_no - from `tabItem` {item_conditions} - ) item - where - item_code = item.name and - company = %(company)s and - posting_date <= %(to_date)s and - is_cancelled != 1 - {sle_conditions} - order by posting_date, posting_time, sle.creation, actual_qty - """ #nosec - .format( - item_conditions=self.__get_item_conditions(), - sle_conditions=self.__get_sle_conditions() - ), - self.filters, - as_dict=True + sle.actual_qty, sle.posting_date, + sle.voucher_type, sle.voucher_no, + sle.serial_no, sle.batch_no, + sle.qty_after_transaction, sle.warehouse + ).where( + (sle.item_code == item.name) + & (sle.company == self.filters.get("company")) + & (sle.posting_date <= self.filters.get("to_date")) + & (sle.is_cancelled != 1) + ) ) - def __get_item_conditions(self) -> str: - conditions = [] - if self.filters.get("item_code"): - conditions.append("item_code=%(item_code)s") - if self.filters.get("brand"): - conditions.append("brand=%(brand)s") - - return "where {}".format(" and ".join(conditions)) if conditions else "" - - def __get_sle_conditions(self) -> str: - conditions = [] - if self.filters.get("warehouse"): - lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ['lft', 'rgt']) - conditions.append(""" - warehouse in ( - select wh.name from `tabWarehouse` wh - where wh.lft >= {0} and rgt <= {1} - ) - """.format(lft, rgt)) + sle_query = self.__get_warehouse_conditions(sle, sle_query) - return "and {}".format(" and ".join(conditions)) if conditions else "" \ No newline at end of file + sle_query = sle_query.orderby( + sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty + ) + + return sle_query.run(as_dict=True) + + def __get_item_query(self) -> str: + item_table = frappe.qb.DocType("Item") + + item = frappe.qb.from_("Item").select( + "name", "item_name", "description", "stock_uom", + "brand", "item_group", "has_serial_no" + ) + + if self.filters.get("item_code"): + item = item.where(item_table.item_code == self.filters.get("item_code")) + + if self.filters.get("brand"): + item = item.where(item_table.brand == self.filters.get("brand")) + + return item + + def __get_warehouse_conditions(self, sle, sle_query) -> str: + warehouse = frappe.qb.DocType("Warehouse") + lft, rgt = frappe.db.get_value( + "Warehouse", + self.filters.get("warehouse"), + ['lft', 'rgt'] + ) + + warehouse_results = ( + frappe.qb.from_(warehouse) + .select("name") + .where( + (warehouse.lft >= lft) + & (warehouse.rgt <= rgt) + ).run() + ) + warehouse_results = [x[0] for x in warehouse_results] + + return sle_query.where(sle.warehouse.isin(warehouse_results)) From 25f4de80b374ed1526d3490e613bb6cfdf4bba39 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Dec 2021 21:15:40 +0530 Subject: [PATCH 31/35] fix: filter out Claimed employee advances in Expense Claim (#29046) --- erpnext/hr/doctype/expense_claim/expense_claim.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 665556301bb..047945787d7 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", { ['docstatus', '=', 1], ['employee', '=', frm.doc.employee], ['paid_amount', '>', 0], - ['paid_amount', '>', 'claimed_amount'] + ['status', '!=', 'Claimed'] ] }; }); From c007f84ade706e1738e4d964f291bfe4ab3aa948 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Dec 2021 01:27:24 +0530 Subject: [PATCH 32/35] chore: Added .md file to explain stock ageing business logic --- .../stock/report/stock_ageing/stock_ageing.py | 3 +- .../stock_ageing/stock_ageing_fifo_logic.md | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 8062a1c98ef..88e0712fd7f 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -395,8 +395,7 @@ class FIFOSlots: warehouse_results = ( frappe.qb.from_(warehouse) - .select("name") - .where( + .select("name").where( (warehouse.lft >= lft) & (warehouse.rgt <= rgt) ).run() diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md new file mode 100644 index 00000000000..5ffe97fd742 --- /dev/null +++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md @@ -0,0 +1,73 @@ +### Concept of FIFO Slots + +Since we need to know age-wise remaining stock, we maintain all the inward entries as slots. So each time stock comes in, a slot is added for the same. + +Eg. For Item A: +---------------------- +Date | Qty | Queue +---------------------- +1st | +50 | [[50, 1-12-2021]] +2nd | +20 | [[50, 1-12-2021], [20, 2-12-2021]] +---------------------- + +Now the queue can tell us the total stock and also how old the stock is. +Here, the balance qty is 70. +50 qty is (today-the 1st) days old +20 qty is (today-the 2nd) days old + +### Calculation of FIFO Slots + +#### Case 1: Outward from sufficient balance qty +---------------------- +Date | Qty | Queue +---------------------- +1st | +50 | [[50, 1-12-2021]] +2nd | -20 | [[30, 1-12-2021]] +2nd | +20 | [[30, 1-12-2021], [20, 2-12-2021]] + +Here after the first entry, while issuing 20 qty: +- **since 20 is lesser than the balance**, **qty_to_pop (20)** is simply consumed from first slot (FIFO consumption) +- Any inward entry after as usual will get its own slot added to the queue + +#### Case 2: Outward from sufficient cumulative (slots) balance qty +---------------------- +Date | Qty | Queue +---------------------- +1st | +50 | [[50, 1-12-2021]] +2nd | +20 | [[50, 1-12-2021], [20, 2-12-2021]] +2nd | -60 | [[10, 2-12-2021]] + +- Consumption happens slot wise. First slot 1 is consumed +- Since **qty_to_pop (60) is greater than slot 1 qty (50)**, the entire slot is consumed and popped +- Now the queue is [[20, 2-12-2021]], and **qty_to_pop=10** (remaining qty to pop) +- It then goes ahead to the next slot and consumes 10 from it +- Now the queue is [[10, 2-12-2021]] + +#### Case 3: Outward from insufficient balance qty +> This case is possible only if **Allow Negative Stock** was enabled at some point/is enabled. + +---------------------- +Date | Qty | Queue +---------------------- +1st | +50 | [[50, 1-12-2021]] +2nd | -60 | [[-10, 1-12-2021]] + +- Since **qty_to_pop (60)** is more than the balance in slot 1, the entire slot is consumed and popped +- Now the queue is **empty**, and **qty_to_pop=10** (remaining qty to pop) +- Since we still have more to consume, we append the balance since 60 is issued from 50 i.e. -10. +- We register this negative value, since the stock issue has caused the balance to become negative + +Now when stock is inwarded: +- Instead of adding a slot we check if there are any negative balances. +- If yes, we keep adding positive stock to it until we make the balance positive. +- Once the balance is positive, the next inward entry will add a new slot in the queue + +Eg: +---------------------- +Date | Qty | Queue +---------------------- +1st | +50 | [[50, 1-12-2021]] +2nd | -60 | [[-10, 1-12-2021]] +3rd | +5 | [[-5, 3-12-2021]] +4th | +10 | [[5, 4-12-2021]] +4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]] \ No newline at end of file From 098f72e7ec43ee8ccc65236956e496be01ec8d18 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Dec 2021 02:33:54 +0530 Subject: [PATCH 33/35] test: Stock Ageing FIFO Slot generation --- .../stock/report/stock_ageing/stock_ageing.py | 10 +- .../report/stock_ageing/test_stock_ageing.py | 125 ++++++++++++++++++ 2 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 erpnext/stock/report/stock_ageing/test_stock_ageing.py diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 88e0712fd7f..e6dfc97a998 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -292,7 +292,7 @@ class FIFOSlots: fifo_queue[0][0] += flt(row.actual_qty) fifo_queue[0][1] = row.posting_date else: - fifo_queue.append([row.actual_qty, row.posting_date]) + fifo_queue.append([flt(row.actual_qty), row.posting_date]) return for serial_no in serial_nos: @@ -395,10 +395,10 @@ class FIFOSlots: warehouse_results = ( frappe.qb.from_(warehouse) - .select("name").where( - (warehouse.lft >= lft) - & (warehouse.rgt <= rgt) - ).run() + .select("name").where( + (warehouse.lft >= lft) + & (warehouse.rgt <= rgt) + ).run() ) warehouse_results = [x[0] for x in warehouse_results] diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py new file mode 100644 index 00000000000..0e355a5c38e --- /dev/null +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -0,0 +1,125 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots +from erpnext.tests.utils import ERPNextTestCase + +class TestStockAgeing(ERPNextTestCase): + def setUp(self) -> None: + self.filters = frappe._dict( + company="_Test Company", + to_date="2021-12-10" + ) + + def test_normal_inward_outward_queue(self): + "Reference: Case 1 in stock_ageing_fifo_logic.md" + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=30, qty_after_transaction=30, + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=20, qty_after_transaction=50, + posting_date="2021-12-02", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-10), qty_after_transaction=40, + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="003", + has_serial_no=False, serial_no=None + ) + ] + + slots = FIFOSlots(self.filters, sle).generate() + + self.assertTrue(slots["Flask Item"]["fifo_queue"]) + result = slots["Flask Item"] + queue = result["fifo_queue"] + + self.assertEqual(result["qty_after_transaction"], result["total_qty"]) + self.assertEqual(queue[0][0], 20.0) + + def test_insufficient_balance(self): + "Reference: Case 3 in stock_ageing_fifo_logic.md" + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=(-30), qty_after_transaction=(-30), + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=20, qty_after_transaction=(-10), + posting_date="2021-12-02", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=20, qty_after_transaction=10, + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="003", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=10, qty_after_transaction=20, + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="004", + has_serial_no=False, serial_no=None + ) + ] + + slots = FIFOSlots(self.filters, sle).generate() + + result = slots["Flask Item"] + queue = result["fifo_queue"] + + self.assertEqual(result["qty_after_transaction"], result["total_qty"]) + self.assertEqual(queue[0][0], 10.0) + self.assertEqual(queue[1][0], 10.0) + + def test_stock_reconciliation(self): + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=30, qty_after_transaction=30, + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=50, + posting_date="2021-12-02", voucher_type="Stock Reconciliation", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-10), qty_after_transaction=40, + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="003", + has_serial_no=False, serial_no=None + ) + ] + + slots = FIFOSlots(self.filters, sle).generate() + + result = slots["Flask Item"] + queue = result["fifo_queue"] + + self.assertEqual(result["qty_after_transaction"], result["total_qty"]) + self.assertEqual(queue[0][0], 20.0) + self.assertEqual(queue[1][0], 20.0) From 077e2c646721e24eccdf7937715f5250cc6c22ca Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 28 Dec 2021 11:08:13 +0530 Subject: [PATCH 34/35] Update erpnext/projects/doctype/project/project.js --- erpnext/projects/doctype/project/project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 6399a50f481..4f19bbd5163 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -95,7 +95,7 @@ frappe.ui.form.on("Project", { set_project_status_button: function(frm) { frm.add_custom_button(__('Set Project Status'), () => { let d = new frappe.ui.Dialog({ - "title": "Set Project Status", + "title": __("Set Project Status"), "fields": [ { "fieldname": "status", From 6ca978c18b4565a696356280448a4fab2d47f94c Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Dec 2021 11:46:46 +0530 Subject: [PATCH 35/35] fix: Linter (extra line after import) --- erpnext/stock/report/stock_ageing/test_stock_ageing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 0e355a5c38e..949bb7c15a8 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -6,6 +6,7 @@ import frappe from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots from erpnext.tests.utils import ERPNextTestCase + class TestStockAgeing(ERPNextTestCase): def setUp(self) -> None: self.filters = frappe._dict(