From 9c889b37fb3e6572043c7a28706e43d051e2ff46 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 20 Nov 2023 18:33:56 +0530 Subject: [PATCH 01/28] fix(ux): no need to select rows to reserve the stock --- erpnext/selling/doctype/sales_order/sales_order.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 3ad18daf19..01cbbe743d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -214,7 +214,6 @@ frappe.ui.form.on("Sales Order", { label: __("Items to Reserve"), allow_bulk_edit: false, cannot_add_rows: true, - cannot_delete_rows: true, data: [], fields: [ { @@ -260,7 +259,7 @@ frappe.ui.form.on("Sales Order", { ], primary_action_label: __("Reserve Stock"), primary_action: () => { - var data = {items: dialog.fields_dict.items.grid.get_selected_children()}; + var data = {items: dialog.fields_dict.items.grid.data}; if (data.items && data.items.length > 0) { frappe.call({ @@ -278,9 +277,6 @@ frappe.ui.form.on("Sales Order", { } }); } - else { - frappe.msgprint(__("Please select items to reserve.")); - } dialog.hide(); }, @@ -316,7 +312,6 @@ frappe.ui.form.on("Sales Order", { label: __("Reserved Stock"), allow_bulk_edit: false, cannot_add_rows: true, - cannot_delete_rows: true, in_place_edit: true, data: [], fields: [ @@ -360,7 +355,7 @@ frappe.ui.form.on("Sales Order", { ], primary_action_label: __("Unreserve Stock"), primary_action: () => { - var data = {sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children()}; + var data = {sr_entries: dialog.fields_dict.sr_entries.grid.data}; if (data.sr_entries && data.sr_entries.length > 0) { frappe.call({ @@ -377,9 +372,6 @@ frappe.ui.form.on("Sales Order", { } }); } - else { - frappe.msgprint(__("Please select items to unreserve.")); - } dialog.hide(); }, From 73586fd9b2f9392d18f65a063b14ef2de2629615 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 22 Nov 2023 11:37:14 +0530 Subject: [PATCH 02/28] fix: use field `sales_order_item` instead `name` --- erpnext/selling/doctype/sales_order/sales_order.js | 6 +++--- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- .../stock_reservation_entry/stock_reservation_entry.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 01cbbe743d..81b0d016ae 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -217,9 +217,9 @@ frappe.ui.form.on("Sales Order", { data: [], fields: [ { - fieldname: "name", + fieldname: "sales_order_item", fieldtype: "Data", - label: __("Name"), + label: __("Sales Order Item"), reqd: 1, read_only: 1, }, @@ -288,7 +288,7 @@ frappe.ui.form.on("Sales Order", { if (unreserved_qty > 0) { dialog.fields_dict.items.df.data.push({ - 'name': item.name, + 'sales_order_item': item.name, 'item_code': item.item_code, 'warehouse': item.warehouse, 'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor)) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index ed20209577..3350a688e8 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -233,7 +233,7 @@ class PickList(Document): for location in self.locations: if location.warehouse and location.sales_order and location.sales_order_item: item_details = { - "name": location.sales_order_item, + "sales_order_item": location.sales_order_item, "item_code": location.item_code, "warehouse": location.warehouse, "qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)), diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d905fe5ea6..9bc7e58f80 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -775,7 +775,7 @@ class PurchaseReceipt(BuyingController): for item in self.items: if item.sales_order and item.sales_order_item: item_details = { - "name": item.sales_order_item, + "sales_order_item": item.sales_order_item, "item_code": item.item_code, "warehouse": item.warehouse, "qty_to_reserve": item.stock_qty, diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 09542826f3..df3e288c4c 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -869,7 +869,7 @@ def create_stock_reservation_entries_for_so_items( items = [] if items_details: for item in items_details: - so_item = frappe.get_doc("Sales Order Item", item.get("name")) + so_item = frappe.get_doc("Sales Order Item", item.get("sales_order_item")) so_item.warehouse = item.get("warehouse") so_item.qty_to_reserve = ( flt(item.get("qty_to_reserve")) From 2a41da94d443dee51e615b395d18ad161d4e87fc Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 21 Nov 2023 11:34:53 +0530 Subject: [PATCH 03/28] fix(ux): no need to select rows to unreserve the stock --- erpnext/selling/doctype/sales_order/sales_order.js | 10 +++++----- .../stock_reservation_entry/stock_reservation_entry.py | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 81b0d016ae..97b214e33e 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -304,7 +304,7 @@ frappe.ui.form.on("Sales Order", { cancel_stock_reservation_entries(frm) { const dialog = new frappe.ui.Dialog({ title: __("Stock Unreservation"), - size: "large", + size: "extra-large", fields: [ { fieldname: "sr_entries", @@ -316,9 +316,9 @@ frappe.ui.form.on("Sales Order", { data: [], fields: [ { - fieldname: "name", + fieldname: "sre", fieldtype: "Link", - label: __("SRE"), + label: __("Stock Reservation Entry"), options: "Stock Reservation Entry", reqd: 1, read_only: 1, @@ -362,7 +362,7 @@ frappe.ui.form.on("Sales Order", { doc: frm.doc, method: "cancel_stock_reservation_entries", args: { - sre_list: data.sr_entries, + sre_list: data.sr_entries.map(item => item.sre), }, freeze: true, freeze_message: __('Unreserving Stock...'), @@ -388,7 +388,7 @@ frappe.ui.form.on("Sales Order", { r.message.forEach(sre => { if (flt(sre.reserved_qty) > flt(sre.delivered_qty)) { dialog.fields_dict.sr_entries.df.data.push({ - 'name': sre.name, + 'sre': sre.name, 'item_code': sre.item_code, 'warehouse': sre.warehouse, 'qty': (flt(sre.reserved_qty) - flt(sre.delivered_qty)) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index df3e288c4c..cbfa4e0a43 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -1053,12 +1053,14 @@ def cancel_stock_reservation_entries( from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, from_voucher_no: str = None, from_voucher_detail_no: str = None, - sre_list: list[dict] = None, + sre_list: list = None, notify: bool = True, ) -> None: """Cancel Stock Reservation Entries.""" if not sre_list: + sre_list = {} + if voucher_type and voucher_no: sre_list = get_stock_reservation_entries_for_voucher( voucher_type, voucher_no, voucher_detail_no, fields=["name"] @@ -1082,9 +1084,11 @@ def cancel_stock_reservation_entries( sre_list = query.run(as_dict=True) + sre_list = [d.name for d in sre_list] + if sre_list: for sre in sre_list: - frappe.get_doc("Stock Reservation Entry", sre["name"]).cancel() + frappe.get_doc("Stock Reservation Entry", sre).cancel() if notify: msg = _("Stock Reservation Entries Cancelled") From b206b0583b2998382c0ce1a63c867f09a6a0b03a Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 23 Nov 2023 15:10:47 +0530 Subject: [PATCH 04/28] fix: fetch item_tax_template values if fields with fetch_from exisit --- erpnext/controllers/accounts_controller.py | 1 + erpnext/public/js/controllers/transaction.js | 1 + erpnext/stock/get_item_details.py | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d12d50d2e7..e68894fa77 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -625,6 +625,7 @@ class AccountsController(TransactionBase): args["doctype"] = self.doctype args["name"] = self.name + args["child_doctype"] = item.doctype args["child_docname"] = item.name args["ignore_pricing_rule"] = ( self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0 diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2c40f4964b..6dc24faa96 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -512,6 +512,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe cost_center: item.cost_center, tax_category: me.frm.doc.tax_category, item_tax_template: item.item_tax_template, + child_doctype: item.doctype, child_docname: item.name, is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow, } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c766cab0a1..5143067f74 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -8,6 +8,7 @@ import frappe from frappe import _, throw from frappe.model import child_table_fields, default_fields from frappe.model.meta import get_field_precision +from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import IfNull, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, getdate @@ -571,6 +572,9 @@ def get_item_tax_template(args, item, out): item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_group = item_group_doc.parent_item_group + if args.child_doctype and item_tax_template: + out.update(get_fetch_values(args.child_doctype, "item_tax_template", item_tax_template)) + def _get_item_tax_template(args, taxes, out=None, for_validate=False): if out is None: From 477d9fa87e3cd7476f19930d60d23e347e46e658 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Fri, 24 Nov 2023 22:27:01 +0100 Subject: [PATCH 05/28] feat: new Report "Lost Quotations" (#38309) --- .../crm/doctype/competitor/competitor.json | 13 +- .../report/lost_quotations/__init__.py | 0 .../report/lost_quotations/lost_quotations.js | 40 ++++++ .../lost_quotations/lost_quotations.json | 30 +++++ .../report/lost_quotations/lost_quotations.py | 98 ++++++++++++++ .../quotation_lost_reason.json | 123 +++++++----------- 6 files changed, 228 insertions(+), 76 deletions(-) create mode 100644 erpnext/selling/report/lost_quotations/__init__.py create mode 100644 erpnext/selling/report/lost_quotations/lost_quotations.js create mode 100644 erpnext/selling/report/lost_quotations/lost_quotations.json create mode 100644 erpnext/selling/report/lost_quotations/lost_quotations.py diff --git a/erpnext/crm/doctype/competitor/competitor.json b/erpnext/crm/doctype/competitor/competitor.json index 280441f16f..fd6da23921 100644 --- a/erpnext/crm/doctype/competitor/competitor.json +++ b/erpnext/crm/doctype/competitor/competitor.json @@ -29,8 +29,16 @@ } ], "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-10-21 12:43:59.106807", + "links": [ + { + "is_child_table": 1, + "link_doctype": "Competitor Detail", + "link_fieldname": "competitor", + "parent_doctype": "Quotation", + "table_fieldname": "competitors" + } + ], + "modified": "2023-11-23 19:33:54.284279", "modified_by": "Administrator", "module": "CRM", "name": "Competitor", @@ -64,5 +72,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/report/lost_quotations/__init__.py b/erpnext/selling/report/lost_quotations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.js b/erpnext/selling/report/lost_quotations/lost_quotations.js new file mode 100644 index 0000000000..78e76cbf02 --- /dev/null +++ b/erpnext/selling/report/lost_quotations/lost_quotations.js @@ -0,0 +1,40 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Lost Quotations"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + }, + { + label: "Timespan", + fieldtype: "Select", + fieldname: "timespan", + options: [ + "Last Week", + "Last Month", + "Last Quarter", + "Last 6 months", + "Last Year", + "This Week", + "This Month", + "This Quarter", + "This Year", + ], + default: "This Year", + reqd: 1, + }, + { + fieldname: "group_by", + label: __("Group By"), + fieldtype: "Select", + options: ["Lost Reason", "Competitor"], + default: "Lost Reason", + reqd: 1, + }, + ], +}; diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.json b/erpnext/selling/report/lost_quotations/lost_quotations.json new file mode 100644 index 0000000000..8915bab595 --- /dev/null +++ b/erpnext/selling/report/lost_quotations/lost_quotations.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-11-23 18:00:19.141922", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": null, + "letterhead": null, + "modified": "2023-11-23 19:27:28.854108", + "modified_by": "Administrator", + "module": "Selling", + "name": "Lost Quotations", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Quotation", + "report_name": "Lost Quotations", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.py b/erpnext/selling/report/lost_quotations/lost_quotations.py new file mode 100644 index 0000000000..7c0bfbdd52 --- /dev/null +++ b/erpnext/selling/report/lost_quotations/lost_quotations.py @@ -0,0 +1,98 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from typing import Literal + +import frappe +from frappe import _ +from frappe.model.docstatus import DocStatus +from frappe.query_builder.functions import Coalesce, Count, Round, Sum +from frappe.utils.data import get_timespan_date_range + + +def execute(filters=None): + columns = get_columns(filters.get("group_by")) + from_date, to_date = get_timespan_date_range(filters.get("timespan").lower()) + data = get_data(filters.get("company"), from_date, to_date, filters.get("group_by")) + return columns, data + + +def get_columns(group_by: Literal["Lost Reason", "Competitor"]): + return [ + { + "fieldname": "lost_reason" if group_by == "Lost Reason" else "competitor", + "label": _("Lost Reason") if group_by == "Lost Reason" else _("Competitor"), + "fieldtype": "Link", + "options": "Quotation Lost Reason" if group_by == "Lost Reason" else "Competitor", + "width": 200, + }, + { + "filedname": "lost_quotations", + "label": _("Lost Quotations"), + "fieldtype": "Int", + "width": 150, + }, + { + "filedname": "lost_quotations_pct", + "label": _("Lost Quotations %"), + "fieldtype": "Percent", + "width": 200, + }, + { + "fieldname": "lost_value", + "label": _("Lost Value"), + "fieldtype": "Currency", + "width": 150, + }, + { + "filedname": "lost_value_pct", + "label": _("Lost Value %"), + "fieldtype": "Percent", + "width": 200, + }, + ] + + +def get_data( + company: str, from_date: str, to_date: str, group_by: Literal["Lost Reason", "Competitor"] +): + """Return quotation value grouped by lost reason or competitor""" + if group_by == "Lost Reason": + fieldname = "lost_reason" + dimension = frappe.qb.DocType("Quotation Lost Reason Detail") + elif group_by == "Competitor": + fieldname = "competitor" + dimension = frappe.qb.DocType("Competitor Detail") + else: + frappe.throw(_("Invalid Group By")) + + q = frappe.qb.DocType("Quotation") + + lost_quotation_condition = ( + (q.status == "Lost") + & (q.docstatus == DocStatus.submitted()) + & (q.transaction_date >= from_date) + & (q.transaction_date <= to_date) + & (q.company == company) + ) + + from_lost_quotations = frappe.qb.from_(q).where(lost_quotation_condition) + total_quotations = from_lost_quotations.select(Count(q.name)) + total_value = from_lost_quotations.select(Sum(q.base_net_total)) + + query = ( + frappe.qb.from_(q) + .select( + Coalesce(dimension[fieldname], _("Not Specified")), + Count(q.name).distinct(), + Round((Count(q.name).distinct() / total_quotations * 100), 2), + Sum(q.base_net_total), + Round((Sum(q.base_net_total) / total_value * 100), 2), + ) + .left_join(dimension) + .on(dimension.parent == q.name) + .where(lost_quotation_condition) + .groupby(dimension[fieldname]) + ) + + return query.run() diff --git a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json index 5d778eec0b..0eae08e870 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json +++ b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json @@ -1,83 +1,58 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:order_lost_reason", - "beta": 0, - "creation": "2013-01-10 16:34:24", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "autoname": "field:order_lost_reason", + "creation": "2013-01-10 16:34:24", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "order_lost_reason" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "order_lost_reason", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Quotation Lost Reason", - "length": 0, - "no_copy": 0, - "oldfieldname": "order_lost_reason", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "order_lost_reason", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Quotation Lost Reason", + "oldfieldname": "order_lost_reason", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-07-25 05:24:25.533953", - "modified_by": "Administrator", - "module": "Setup", - "name": "Quotation Lost Reason", - "owner": "Administrator", + ], + "icon": "fa fa-flag", + "idx": 1, + "links": [ + { + "is_child_table": 1, + "link_doctype": "Quotation Lost Reason Detail", + "link_fieldname": "lost_reason", + "parent_doctype": "Quotation", + "table_fieldname": "lost_reasons" + } + ], + "modified": "2023-11-23 19:31:02.743353", + "modified_by": "Administrator", + "module": "Setup", + "name": "Quotation Lost Reason", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file From d8245cef723575eaf52c2e39d25a6f9c83ce9ba0 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 26 Nov 2023 16:13:39 +0530 Subject: [PATCH 06/28] fix: job card overlap validation (#38345) --- .../doctype/job_card/job_card.py | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index db6bc80838..f303531aee 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -185,7 +185,8 @@ class JobCard(Document): # override capacity for employee production_capacity = 1 - if time_logs and production_capacity > len(time_logs): + overlap_count = self.get_overlap_count(time_logs) + if time_logs and production_capacity > overlap_count: return {} if self.workstation_type and time_logs: @@ -195,6 +196,37 @@ class JobCard(Document): return time_logs[-1] + @staticmethod + def get_overlap_count(time_logs): + count = 1 + + # Check overlap exists or not between the overlapping time logs with the current Job Card + for idx, row in enumerate(time_logs): + next_idx = idx + if idx + 1 < len(time_logs): + next_idx = idx + 1 + next_row = time_logs[next_idx] + if row.name == next_row.name: + continue + + if ( + ( + get_datetime(next_row.from_time) >= get_datetime(row.from_time) + and get_datetime(next_row.from_time) <= get_datetime(row.to_time) + ) + or ( + get_datetime(next_row.to_time) >= get_datetime(row.from_time) + and get_datetime(next_row.to_time) <= get_datetime(row.to_time) + ) + or ( + get_datetime(next_row.from_time) <= get_datetime(row.from_time) + and get_datetime(next_row.to_time) >= get_datetime(row.to_time) + ) + ): + count += 1 + + return count + def get_time_logs(self, args, doctype, check_next_available_slot=False): jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType(doctype) @@ -211,7 +243,14 @@ class JobCard(Document): query = ( frappe.qb.from_(jctl) .from_(jc) - .select(jc.name.as_("name"), jctl.from_time, jctl.to_time, jc.workstation, jc.workstation_type) + .select( + jc.name.as_("name"), + jctl.name.as_("row_name"), + jctl.from_time, + jctl.to_time, + jc.workstation, + jc.workstation_type, + ) .where( (jctl.parent == jc.name) & (Criterion.any(time_conditions)) From 284a40aa63d09163e24b089cf0dcc517b8e62a91 Mon Sep 17 00:00:00 2001 From: Monolithon Administrator <5338127+monolithonadmin@users.noreply.github.com> Date: Sun, 26 Nov 2023 13:47:10 +0100 Subject: [PATCH 07/28] fix: Add name to Hungary - Chart of Accounts for Microenterprises json (#38278) Co-authored-by: Deepesh Garg --- ..._of_accounts_for_microenterprises_with_account_number.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json index 2cd6c0fc61..4013bb0926 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json @@ -1,4 +1,6 @@ { + "country_code": "hu", + "name": "Hungary - Chart of Accounts for Microenterprises", "tree": { "SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": { "account_number": 1, @@ -1651,4 +1653,4 @@ } } } -} \ No newline at end of file +} From 5426b93387c8a4599b11d5e064d4d8b37078aca1 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 26 Nov 2023 14:07:55 +0100 Subject: [PATCH 08/28] refactor: bank transaction (#38182) --- .../bank_reconciliation_tool.py | 4 +- .../bank_transaction/bank_transaction.json | 17 ++- .../bank_transaction/bank_transaction.py | 139 ++++++++---------- 3 files changed, 77 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 7e2f763137..c2ddb39964 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -424,7 +424,9 @@ def reconcile_vouchers(bank_transaction_name, vouchers): vouchers = json.loads(vouchers) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction.add_payment_entries(vouchers) - return frappe.get_doc("Bank Transaction", bank_transaction_name) + transaction.save() + + return transaction @frappe.whitelist() diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index b32022e6fd..0328d51b89 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -13,6 +13,7 @@ "status", "bank_account", "company", + "amended_from", "section_break_4", "deposit", "withdrawal", @@ -25,10 +26,10 @@ "transaction_id", "transaction_type", "section_break_14", + "column_break_oufv", "payment_entries", "section_break_18", "allocated_amount", - "amended_from", "column_break_17", "unallocated_amount", "party_section", @@ -138,10 +139,12 @@ "fieldtype": "Section Break" }, { + "allow_on_submit": 1, "fieldname": "allocated_amount", "fieldtype": "Currency", "label": "Allocated Amount", - "options": "currency" + "options": "currency", + "read_only": 1 }, { "fieldname": "amended_from", @@ -157,10 +160,12 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "unallocated_amount", "fieldtype": "Currency", "label": "Unallocated Amount", - "options": "currency" + "options": "currency", + "read_only": 1 }, { "fieldname": "party_section", @@ -225,11 +230,15 @@ "fieldname": "bank_party_account_number", "fieldtype": "Data", "label": "Party Account No. (Bank Statement)" + }, + { + "fieldname": "column_break_oufv", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2023-06-06 13:58:12.821411", + "modified": "2023-11-18 18:32:47.203694", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 4649d23162..51c823a459 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -2,78 +2,73 @@ # For license information, please see license.txt import frappe +from frappe import _ from frappe.utils import flt from erpnext.controllers.status_updater import StatusUpdater class BankTransaction(StatusUpdater): - def after_insert(self): - self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) + def before_validate(self): + self.update_allocated_amount() - def on_submit(self): - self.clear_linked_payment_entries() + def validate(self): + self.validate_duplicate_references() + + def validate_duplicate_references(self): + """Make sure the same voucher is not allocated twice within the same Bank Transaction""" + if not self.payment_entries: + return + + pe = [] + for row in self.payment_entries: + reference = (row.payment_document, row.payment_entry) + if reference in pe: + frappe.throw( + _("{0} {1} is allocated twice in this Bank Transaction").format( + row.payment_document, row.payment_entry + ) + ) + pe.append(reference) + + def update_allocated_amount(self): + self.allocated_amount = ( + sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0 + ) + self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount + + def before_submit(self): + self.allocate_payment_entries() self.set_status() if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"): self.auto_set_party() - _saving_flag = False - - # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting - def on_update_after_submit(self): - "Run on save(). Avoid recursion caused by multiple saves" - if not self._saving_flag: - self._saving_flag = True - self.clear_linked_payment_entries() - self.update_allocations() - self._saving_flag = False + def before_update_after_submit(self): + self.validate_duplicate_references() + self.allocate_payment_entries() + self.update_allocated_amount() def on_cancel(self): - self.clear_linked_payment_entries(for_cancel=True) - self.set_status(update=True) + for payment_entry in self.payment_entries: + self.clear_linked_payment_entry(payment_entry, for_cancel=True) - def update_allocations(self): - "The doctype does not allow modifications after submission, so write to the db direct" - if self.payment_entries: - allocated_amount = sum(p.allocated_amount for p in self.payment_entries) - else: - allocated_amount = 0.0 - - amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.db_set("allocated_amount", flt(allocated_amount)) - self.db_set("unallocated_amount", amount - flt(allocated_amount)) - self.reload() self.set_status(update=True) def add_payment_entries(self, vouchers): "Add the vouchers with zero allocation. Save() will perform the allocations and clearance" if 0.0 >= self.unallocated_amount: - frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name)) + frappe.throw(_("Bank Transaction {0} is already fully reconciled").format(self.name)) - added = False for voucher in vouchers: - # Can't add same voucher twice - found = False - for pe in self.payment_entries: - if ( - pe.payment_document == voucher["payment_doctype"] - and pe.payment_entry == voucher["payment_name"] - ): - found = True - - if not found: - pe = { + self.append( + "payment_entries", + { "payment_document": voucher["payment_doctype"], "payment_entry": voucher["payment_name"], "allocated_amount": 0.0, # Temporary - } - child = self.append("payment_entries", pe) - added = True - - # runs on_update_after_submit - if added: - self.save() + }, + ) def allocate_payment_entries(self): """Refactored from bank reconciliation tool. @@ -90,6 +85,7 @@ class BankTransaction(StatusUpdater): - clear means: set the latest transaction date as clearance date """ remaining_amount = self.unallocated_amount + to_remove = [] for payment_entry in self.payment_entries: if payment_entry.allocated_amount == 0.0: unallocated_amount, should_clear, latest_transaction = get_clearance_details( @@ -99,49 +95,39 @@ class BankTransaction(StatusUpdater): if 0.0 == unallocated_amount: if should_clear: latest_transaction.clear_linked_payment_entry(payment_entry) - self.db_delete_payment_entry(payment_entry) + to_remove.append(payment_entry) elif remaining_amount <= 0.0: - self.db_delete_payment_entry(payment_entry) + to_remove.append(payment_entry) - elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount: - payment_entry.db_set("allocated_amount", unallocated_amount) + elif 0.0 < unallocated_amount <= remaining_amount: + payment_entry.allocated_amount = unallocated_amount remaining_amount -= unallocated_amount if should_clear: latest_transaction.clear_linked_payment_entry(payment_entry) - elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount: - payment_entry.db_set("allocated_amount", remaining_amount) + elif 0.0 < unallocated_amount: + payment_entry.allocated_amount = remaining_amount remaining_amount = 0.0 elif 0.0 > unallocated_amount: - self.db_delete_payment_entry(payment_entry) - frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount)) + frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount)) - self.reload() - - def db_delete_payment_entry(self, payment_entry): - frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name}) + for payment_entry in to_remove: + self.remove(to_remove) @frappe.whitelist() def remove_payment_entries(self): for payment_entry in self.payment_entries: self.remove_payment_entry(payment_entry) - # runs on_update_after_submit - self.save() + + self.save() # runs before_update_after_submit def remove_payment_entry(self, payment_entry): "Clear payment entry and clearance" self.clear_linked_payment_entry(payment_entry, for_cancel=True) self.remove(payment_entry) - def clear_linked_payment_entries(self, for_cancel=False): - if for_cancel: - for payment_entry in self.payment_entries: - self.clear_linked_payment_entry(payment_entry, for_cancel) - else: - self.allocate_payment_entries() - def clear_linked_payment_entry(self, payment_entry, for_cancel=False): clearance_date = None if for_cancel else self.date set_voucher_clearance( @@ -162,11 +148,10 @@ class BankTransaction(StatusUpdater): deposit=self.deposit, ).match() - if result: - party_type, party = result - frappe.db.set_value( - "Bank Transaction", self.name, field={"party_type": party_type, "party": party} - ) + if not result: + return + + self.party_type, self.party = result @frappe.whitelist() @@ -198,9 +183,7 @@ def get_clearance_details(transaction, payment_entry): if gle["gl_account"] == gl_bank_account: if gle["amount"] <= 0.0: frappe.throw( - frappe._("Voucher {0} value is broken: {1}").format( - payment_entry.payment_entry, gle["amount"] - ) + _("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"]) ) unmatched_gles -= 1 @@ -221,7 +204,7 @@ def get_clearance_details(transaction, payment_entry): def get_related_bank_gl_entries(doctype, docname): # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql - result = frappe.db.sql( + return frappe.db.sql( """ SELECT ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount, @@ -239,7 +222,6 @@ def get_related_bank_gl_entries(doctype, docname): dict(doctype=doctype, docname=docname), as_dict=True, ) - return result def get_total_allocated_amount(doctype, docname): @@ -365,6 +347,7 @@ def set_voucher_clearance(doctype, docname, clearance_date, self): if clearance_date: vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}] bt.add_payment_entries(vouchers) + bt.save() else: for pe in bt.payment_entries: if pe.payment_document == self.doctype and pe.payment_entry == self.name: From d9b3b9585472656c820fa241f32ed707f7df5c54 Mon Sep 17 00:00:00 2001 From: NandhiniDevi <95607404+Nandhinidevi123@users.noreply.github.com> Date: Sun, 26 Nov 2023 18:40:31 +0530 Subject: [PATCH 09/28] fix : Payment Reco Issue and chart of account importer (#38321) fix : Payment Reco Issue and chart of account importer --- .../chart_of_accounts_importer/chart_of_accounts_importer.py | 2 +- erpnext/accounts/doctype/journal_entry/journal_entry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 5a1c139bde..1e64eeeae6 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -113,7 +113,7 @@ def generate_data_from_csv(file_doc, as_dict=False): if as_dict: data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) else: - if not row[1]: + if not row[1] and len(row) > 1: row[1] = row[0] row[3] = row[2] data.append(row) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 1cff4c7f2d..0ad20c31c1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -508,7 +508,7 @@ class JournalEntry(AccountsController): ).format(d.reference_name, d.account) ) else: - dr_or_cr = "debit" if d.credit > 0 else "credit" + dr_or_cr = "debit" if flt(d.credit) > 0 else "credit" valid = False for jvd in against_entries: if flt(jvd[dr_or_cr]) > 0: From 592ce45da7659dcf4ca148f5924dbe0d783956bf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 27 Nov 2023 08:51:22 +0530 Subject: [PATCH 10/28] refactor: handle rounding loss on AR/AP reports --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 0e62ad61cc..7948e5f465 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -285,8 +285,8 @@ class ReceivablePayableReport(object): must_consider = False if self.filters.get("for_revaluation_journals"): - if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) or ( - (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) + if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or ( + (abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision) ): must_consider = True else: From b9f5a1c85dc29acc22e704d866178a98f3035c1d Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 27 Nov 2023 09:05:22 +0530 Subject: [PATCH 11/28] fix: Negative Qty and Rates in SO/PO (#38252) * fix: Don't allow negative qty in SO/PO * fix: Type casting for safe comparisons * fix: Grammar in error message * fix: Negative rates should be allowed via Update Items in SO/PO * fix: Use `non_negative` property in docfield & emove code validation --- .../purchase_order/test_purchase_order.py | 17 ++++++++++- .../purchase_order_item.json | 3 +- erpnext/controllers/accounts_controller.py | 29 ++++++++++++++----- .../doctype/sales_order/test_sales_order.py | 29 +++++++++++++++++++ .../sales_order_item/sales_order_item.json | 3 +- 5 files changed, 70 insertions(+), 11 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 55c01e8513..0f8574c84d 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -16,7 +16,7 @@ from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt -from erpnext.controllers.accounts_controller import update_child_qty_rate +from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.material_request.material_request import make_purchase_order @@ -27,6 +27,21 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( class TestPurchaseOrder(FrappeTestCase): + def test_purchase_order_qty(self): + po = create_purchase_order(qty=1, do_not_save=True) + po.append( + "items", + { + "item_code": "_Test Item", + "qty": -1, + "rate": 10, + }, + ) + self.assertRaises(frappe.NonNegativeError, po.save) + + po.items[1].qty = 0 + self.assertRaises(InvalidQtyError, po.save) + def test_make_purchase_receipt(self): po = create_purchase_order(do_not_submit=True) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 2d706f41e5..98c1b388c1 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -214,6 +214,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Quantity", + "non_negative": 1, "oldfieldname": "qty", "oldfieldtype": "Currency", "print_width": "60px", @@ -917,7 +918,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:34:27.267382", + "modified": "2023-11-24 13:24:41.298416", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e68894fa77..f551133a28 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -71,6 +71,10 @@ class AccountMissingError(frappe.ValidationError): pass +class InvalidQtyError(frappe.ValidationError): + pass + + force_item_fields = ( "item_group", "brand", @@ -911,10 +915,16 @@ class AccountsController(TransactionBase): return flt(args.get(field, 0) / self.get("conversion_rate", 1)) def validate_qty_is_not_zero(self): - if self.doctype != "Purchase Receipt": - for item in self.items: - if not item.qty: - frappe.throw(_("Item quantity can not be zero")) + if self.doctype == "Purchase Receipt": + return + + for item in self.items: + if not flt(item.qty): + frappe.throw( + msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx), + title=_("Invalid Quantity"), + exc=InvalidQtyError, + ) def validate_account_currency(self, account, account_currency=None): valid_currency = [self.company_currency] @@ -3142,16 +3152,19 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil conv_fac_precision = child_item.precision("conversion_factor") or 2 qty_precision = child_item.precision("qty") or 2 - if flt(child_item.billed_amt, rate_precision) > flt( - flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision - ): + # Amount cannot be lesser than billed amount, except for negative amounts + row_rate = flt(d.get("rate"), rate_precision) + amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt( + row_rate * flt(d.get("qty"), qty_precision), rate_precision + ) + if amount_below_billed_amt and row_rate > 0.0: frappe.throw( _("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format( child_item.idx, child_item.item_code ) ) else: - child_item.rate = flt(d.get("rate"), rate_precision) + child_item.rate = row_rate if d.get("conversion_factor"): if child_item.stock_uom == child_item.uom: diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index d8b5878aa3..a518597aa6 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -51,6 +51,35 @@ class TestSalesOrder(FrappeTestCase): def tearDown(self): frappe.set_user("Administrator") + def test_sales_order_with_negative_rate(self): + """ + Test if negative rate is allowed in Sales Order via doc submission and update items + """ + so = make_sales_order(qty=1, rate=100, do_not_save=True) + so.append("items", {"item_code": "_Test Item", "qty": 1, "rate": -10}) + so.save() + so.submit() + + first_item = so.get("items")[0] + second_item = so.get("items")[1] + trans_item = json.dumps( + [ + { + "item_code": first_item.item_code, + "rate": first_item.rate, + "qty": first_item.qty, + "docname": first_item.name, + }, + { + "item_code": second_item.item_code, + "rate": -20, + "qty": second_item.qty, + "docname": second_item.name, + }, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) + def test_make_material_request(self): so = make_sales_order(do_not_submit=True) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index b4f73003ae..d4ccfc4753 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -200,6 +200,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Quantity", + "non_negative": 1, "oldfieldname": "qty", "oldfieldtype": "Currency", "print_width": "100px", @@ -895,7 +896,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:12.787893", + "modified": "2023-11-24 13:24:55.756320", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From 5a53a4b044be7e889080329056c7911970d92da8 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 27 Nov 2023 20:21:19 +0530 Subject: [PATCH 12/28] fix: make parameters of `create_subscription_process` optional (and other minor fixes) (#38360) --- .../doctype/process_subscription/process_subscription.py | 3 +-- erpnext/accounts/doctype/subscription/subscription.py | 2 +- erpnext/hooks.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.py b/erpnext/accounts/doctype/process_subscription/process_subscription.py index 99269d6a7d..0aa9970cb8 100644 --- a/erpnext/accounts/doctype/process_subscription/process_subscription.py +++ b/erpnext/accounts/doctype/process_subscription/process_subscription.py @@ -17,11 +17,10 @@ class ProcessSubscription(Document): def create_subscription_process( - subscription: str | None, posting_date: Union[str, datetime.date] | None + subscription: str | None = None, posting_date: Union[str, datetime.date] | None = None ): """Create a new Process Subscription document""" doc = frappe.new_doc("Process Subscription") doc.subscription = subscription doc.posting_date = getdate(posting_date) - doc.insert(ignore_permissions=True) doc.submit() diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 3cf7d284bb..a3d8c23418 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -676,7 +676,7 @@ def get_prorata_factor( def process_all( - subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None + subscription: str | None = None, posting_date: Optional["DateTimeLikeObject"] = None ) -> None: """ Task to updates the status of all `Subscription` apart from those that are cancelled diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c6ab6f12f6..857471f1fd 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -419,7 +419,6 @@ scheduler_events = { "erpnext.projects.doctype.project.project.collect_project_status", ], "hourly_long": [ - "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.utilities.bulk_transaction.retry", ], @@ -450,6 +449,7 @@ scheduler_events = { "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly", ], "daily_long": [ + "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process", "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", From ad3634be7c2607ab5c9c831ae01e5ddff7c9ba8b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 28 Nov 2023 06:24:21 +0530 Subject: [PATCH 13/28] chore: fix flaky test case (#38369) --- .../doctype/work_order/test_work_order.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0ae7657c42..e2c8f07980 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -921,6 +921,20 @@ class TestWorkOrder(FrappeTestCase): "Test RM Item 2 for Scrap Item Test", ] + from_time = add_days(now(), -1) + job_cards = frappe.get_all( + "Job Card Time Log", + fields=["distinct parent as name", "docstatus"], + filters={"from_time": (">", from_time)}, + order_by="creation asc", + ) + + for job_card in job_cards: + if job_card.docstatus == 1: + frappe.get_doc("Job Card", job_card.name).cancel() + + frappe.delete_doc("Job Card Time Log", job_card.name) + company = "_Test Company with perpetual inventory" for item_code in items: create_item( From add238c892364ce25f3e7483cb5fd295f9666a56 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Nov 2023 08:21:45 +0530 Subject: [PATCH 14/28] fix: debit credit mismatch in multi-currecy asset purchase receipt (#38342) * fix: Debit credit mimatch in multicurrecy asset purchase receipt * test: multi currency purchase receipt * chore: update init files * test: roolback --- erpnext/assets/doctype/asset/test_asset.py | 103 ++++++------------ .../purchase_receipt/purchase_receipt.py | 2 +- erpnext/www/all-products/__init__.py | 0 erpnext/www/shop-by-category/__init__.py | 0 4 files changed, 37 insertions(+), 68 deletions(-) create mode 100644 erpnext/www/all-products/__init__.py create mode 100644 erpnext/www/shop-by-category/__init__.py diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 9e3ec6faa8..ca2b980579 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -149,12 +149,7 @@ class TestAsset(AssetSetup): ("Creditors - _TC", 0.0, 100000.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", - pi.name, - ) + gle = get_gl_entries("Purchase Invoice", pi.name) self.assertSequenceEqual(gle, expected_gle) pi.cancel() @@ -264,12 +259,7 @@ class TestAsset(AssetSetup): ), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Journal Entry' and voucher_no = %s - order by account""", - asset.journal_entry_for_scrap, - ) + gle = get_gl_entries("Journal Entry", asset.journal_entry_for_scrap) self.assertSequenceEqual(gle, expected_gle) restore_asset(asset.name) @@ -345,13 +335,7 @@ class TestAsset(AssetSetup): ("Debtors - _TC", 25000.0, 0.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - + gle = get_gl_entries("Sales Invoice", si.name) self.assertSequenceEqual(gle, expected_gle) si.cancel() @@ -425,13 +409,7 @@ class TestAsset(AssetSetup): ("Debtors - _TC", 40000.0, 0.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - + gle = get_gl_entries("Sales Invoice", si.name) self.assertSequenceEqual(gle, expected_gle) def test_asset_with_maintenance_required_status_after_sale(self): @@ -572,13 +550,7 @@ class TestAsset(AssetSetup): ("CWIP Account - _TC", 5250.0, 0.0), ) - pr_gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Receipt' and voucher_no = %s - order by account""", - pr.name, - ) - + pr_gle = get_gl_entries("Purchase Receipt", pr.name) self.assertSequenceEqual(pr_gle, expected_gle) pi = make_invoice(pr.name) @@ -591,13 +563,7 @@ class TestAsset(AssetSetup): ("Creditors - _TC", 0.0, 5500.0), ) - pi_gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", - pi.name, - ) - + pi_gle = get_gl_entries("Purchase Invoice", pi.name) self.assertSequenceEqual(pi_gle, expected_gle) asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name") @@ -624,13 +590,7 @@ class TestAsset(AssetSetup): expected_gle = (("_Test Fixed Asset - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 0.0, 5250.0)) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Asset' and voucher_no = %s - order by account""", - asset_doc.name, - ) - + gle = get_gl_entries("Asset", asset_doc.name) self.assertSequenceEqual(gle, expected_gle) def test_asset_cwip_toggling_cases(self): @@ -653,10 +613,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 1 -- PR with cwip disabled, Asset with cwip enabled @@ -670,10 +627,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 2 -- PR with cwip enabled, Asset with cwip disabled @@ -686,10 +640,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertTrue(gle) # case 3 -- PI with cwip disabled, Asset with cwip enabled @@ -702,10 +653,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 4 -- PI with cwip enabled, Asset with cwip disabled @@ -718,10 +666,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertTrue(gle) frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip) @@ -1701,6 +1646,30 @@ class TestDepreciationBasics(AssetSetup): self.assertRaises(frappe.ValidationError, jv.insert) + def test_multi_currency_asset_pr_creation(self): + pr = make_purchase_receipt( + item_code="Macbook Pro", + qty=1, + rate=100.0, + location="Test Location", + supplier="_Test Supplier USD", + currency="USD", + ) + + pr.submit() + self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) + + +def get_gl_entries(doctype, docname): + gl_entry = frappe.qb.DocType("GL Entry") + return ( + frappe.qb.from_(gl_entry) + .select(gl_entry.account, gl_entry.debit, gl_entry.credit) + .where((gl_entry.voucher_type == doctype) & (gl_entry.voucher_no == docname)) + .orderby(gl_entry.account) + .run() + ) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index a7aa7e2ab4..8647528ea5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -571,7 +571,7 @@ class PurchaseReceipt(BuyingController): ) stock_value_diff = ( - flt(d.net_amount) + flt(d.base_net_amount) + flt(d.item_tax_amount / self.conversion_rate) + flt(d.landed_cost_voucher_amount) ) diff --git a/erpnext/www/all-products/__init__.py b/erpnext/www/all-products/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 729fc738af7d78a6f75cb0f794f4c4451d74cd05 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Nov 2023 11:08:52 +0530 Subject: [PATCH 15/28] fix: product bundle search input --- .../doctype/product_bundle/product_bundle.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 2fd9cc1301..c934f0dffa 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -76,16 +76,19 @@ class ProductBundle(Document): @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name") + item = frappe.qb.DocType("Item") - return ( + query = ( frappe.qb.from_(item) .select("*") .where( - (item.is_stock_item == 0) - & (item.is_fixed_asset == 0) - & (item.name.notin(product_bundles)) - & (item[searchfield].like(f"%{txt}%")) + (item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%")) ) .limit(page_len) .offset(start) - ).run() + ) + + if product_bundles: + query = query.where(item.name.notin(product_bundles)) + + return query.run() From 8c3713b649c7777e35be74b7afe437cec682359e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Nov 2023 11:12:55 +0530 Subject: [PATCH 16/28] fix: don't select all fields --- erpnext/selling/doctype/product_bundle/product_bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index c934f0dffa..3d4ffebbfb 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -80,7 +80,7 @@ def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): item = frappe.qb.DocType("Item") query = ( frappe.qb.from_(item) - .select("*") + .select(item.item_code, item.item_name) .where( (item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%")) ) From 8f00481c5f7742b120a232622fae7b3f7e3d2e86 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:35:28 +0100 Subject: [PATCH 17/28] fix: no fstring in translation (#38381) --- .../stock_and_account_value_comparison.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index b1da3ec1bd..416cf48871 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -166,4 +166,4 @@ def create_reposting_entries(rows, company): if entries: entries = ", ".join(entries) - frappe.msgprint(_(f"Reposting entries created: {entries}")) + frappe.msgprint(_("Reposting entries created: {0}").format(entries)) From aee2e12f3944ae4db2dd77a31d0c544c1bacb65a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 28 Nov 2023 17:11:35 +0530 Subject: [PATCH 18/28] chore: fix imports for renamed report --- .../report/tds_computation_summary/tds_computation_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 82f97f1894..2b5566fb2f 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -1,7 +1,7 @@ import frappe from frappe import _ -from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import ( +from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import ( get_result, get_tds_docs, ) From 1763824e5f7ccaefb83aea84db47c92f9e4c9417 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Nov 2023 17:19:21 +0530 Subject: [PATCH 19/28] refactor: use arrow function --- .../doctype/warranty_claim/warranty_claim.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js index 358768eb46..6888f61ae8 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js @@ -4,7 +4,7 @@ frappe.provide("erpnext.support"); frappe.ui.form.on("Warranty Claim", { - setup: function(frm) { + setup: (frm) => { frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer', erpnext.queries.customer); @@ -20,18 +20,22 @@ frappe.ui.form.on("Warranty Claim", { frm.add_fetch('item_code', 'item_name', 'item_name'); frm.add_fetch('item_code', 'description', 'description'); }, - onload: function(frm) { + + onload: (frm) => { if(!frm.doc.status) { frm.set_value('status', 'Open'); } }, - customer: function(frm) { + + customer: (frm) => { erpnext.utils.get_party_details(frm); }, - customer_address: function(frm) { + + customer_address: (frm) => { erpnext.utils.get_address_display(frm); }, - contact_person: function(frm) { + + contact_person: (frm) => { erpnext.utils.get_contact_details(frm); } }); @@ -57,7 +61,7 @@ erpnext.support.WarrantyClaim = class WarrantyClaim extends frappe.ui.form.Contr extend_cscript(cur_frm.cscript, new erpnext.support.WarrantyClaim({frm: cur_frm})); -cur_frm.fields_dict['serial_no'].get_query = function(doc, cdt, cdn) { +cur_frm.fields_dict['serial_no'].get_query = (doc, cdt, cdn) => { var cond = []; var filter = [ ['Serial No', 'docstatus', '!=', 2] @@ -75,7 +79,7 @@ cur_frm.fields_dict['serial_no'].get_query = function(doc, cdt, cdn) { } } -cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) { +cur_frm.fields_dict['item_code'].get_query = (doc, cdt, cdn) => { if(doc.serial_no) { return{ doctype: "Serial No", From 01044ca8e95ec4e84e63fcfe4928ca111cf3a75b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Nov 2023 17:33:27 +0530 Subject: [PATCH 20/28] refactor: use DocType `Fetch From` instead of `frm.add_fetch` --- .../doctype/warranty_claim/warranty_claim.js | 11 ----------- .../warranty_claim/warranty_claim.json | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js index 6888f61ae8..a653f840d8 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js @@ -8,17 +8,6 @@ frappe.ui.form.on("Warranty Claim", { frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer', erpnext.queries.customer); - - frm.add_fetch('serial_no', 'item_code', 'item_code'); - frm.add_fetch('serial_no', 'item_name', 'item_name'); - frm.add_fetch('serial_no', 'description', 'description'); - frm.add_fetch('serial_no', 'maintenance_status', 'warranty_amc_status'); - frm.add_fetch('serial_no', 'warranty_expiry_date', 'warranty_expiry_date'); - frm.add_fetch('serial_no', 'amc_expiry_date', 'amc_expiry_date'); - frm.add_fetch('serial_no', 'customer', 'customer'); - frm.add_fetch('serial_no', 'customer_name', 'customer_name'); - frm.add_fetch('item_code', 'item_name', 'item_name'); - frm.add_fetch('item_code', 'description', 'description'); }, onload: (frm) => { diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.json b/erpnext/support/doctype/warranty_claim/warranty_claim.json index 01d9b01390..9af2b4606c 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.json +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.json @@ -92,7 +92,8 @@ "fieldname": "serial_no", "fieldtype": "Link", "label": "Serial No", - "options": "Serial No" + "options": "Serial No", + "search_index": 1 }, { "fieldname": "customer", @@ -128,6 +129,8 @@ "options": "fa fa-ticket" }, { + "fetch_from": "serial_no.item_code", + "fetch_if_empty": 1, "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, @@ -140,6 +143,7 @@ }, { "depends_on": "eval:doc.item_code", + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "label": "Item Name", @@ -149,6 +153,7 @@ }, { "depends_on": "eval:doc.item_code", + "fetch_from": "item_code.description", "fieldname": "description", "fieldtype": "Small Text", "label": "Description", @@ -164,17 +169,24 @@ "width": "50%" }, { + "fetch_from": "serial_no.maintenance_status", + "fetch_if_empty": 1, "fieldname": "warranty_amc_status", "fieldtype": "Select", "label": "Warranty / AMC Status", - "options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC" + "options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC", + "search_index": 1 }, { + "fetch_from": "serial_no.warranty_expiry_date", + "fetch_if_empty": 1, "fieldname": "warranty_expiry_date", "fieldtype": "Date", "label": "Warranty Expiry Date" }, { + "fetch_from": "serial_no.amc_expiry_date", + "fetch_if_empty": 1, "fieldname": "amc_expiry_date", "fieldtype": "Date", "label": "AMC Expiry Date" @@ -225,6 +237,7 @@ { "bold": 1, "depends_on": "customer", + "fetch_from": "customer.customer_name", "fieldname": "customer_name", "fieldtype": "Data", "in_global_search": 1, @@ -366,7 +379,7 @@ "icon": "fa fa-bug", "idx": 1, "links": [], - "modified": "2023-06-03 16:17:07.694449", + "modified": "2023-11-28 17:30:35.676410", "modified_by": "Administrator", "module": "Support", "name": "Warranty Claim", From 640dfab827f2e83b9c3ae7ff839c6f94b63b71b2 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Nov 2023 17:45:17 +0530 Subject: [PATCH 21/28] refactor: use `frm.set_query` to add filters --- .../doctype/warranty_claim/warranty_claim.js | 102 +++++++++--------- 1 file changed, 49 insertions(+), 53 deletions(-) diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js index a653f840d8..32494efe53 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js @@ -5,14 +5,34 @@ frappe.provide("erpnext.support"); frappe.ui.form.on("Warranty Claim", { setup: (frm) => { - frm.set_query('contact_person', erpnext.queries.contact_query); - frm.set_query('customer_address', erpnext.queries.address_query); - frm.set_query('customer', erpnext.queries.customer); + frm.set_query("contact_person", erpnext.queries.contact_query); + frm.set_query("customer_address", erpnext.queries.address_query); + frm.set_query("customer", erpnext.queries.customer); + + frm.set_query("serial_no", () => { + let filters = { + company: frm.doc.company, + }; + + if (frm.doc.item_code) { + filters["item_code"] = frm.doc.item_code; + } + + return { filters: filters }; + }); + + frm.set_query("item_code", () => { + return { + filters: { + disabled: 0, + }, + }; + }); }, onload: (frm) => { - if(!frm.doc.status) { - frm.set_value('status', 'Open'); + if (!frm.doc.status) { + frm.set_value("status", "Open"); } }, @@ -26,64 +46,40 @@ frappe.ui.form.on("Warranty Claim", { contact_person: (frm) => { erpnext.utils.get_contact_details(frm); - } + }, }); -erpnext.support.WarrantyClaim = class WarrantyClaim extends frappe.ui.form.Controller { +erpnext.support.WarrantyClaim = class WarrantyClaim extends ( + frappe.ui.form.Controller +) { refresh() { - frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} + frappe.dynamic_link = { + doc: this.frm.doc, + fieldname: "customer", + doctype: "Customer", + }; - if(!cur_frm.doc.__islocal && - (cur_frm.doc.status=='Open' || cur_frm.doc.status == 'Work In Progress')) { - cur_frm.add_custom_button(__('Maintenance Visit'), - this.make_maintenance_visit); + if ( + !cur_frm.doc.__islocal && + (cur_frm.doc.status == "Open" || + cur_frm.doc.status == "Work In Progress") + ) { + cur_frm.add_custom_button( + __("Maintenance Visit"), + this.make_maintenance_visit + ); } } make_maintenance_visit() { frappe.model.open_mapped_doc({ method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", - frm: cur_frm - }) + frm: cur_frm, + }); } }; -extend_cscript(cur_frm.cscript, new erpnext.support.WarrantyClaim({frm: cur_frm})); - -cur_frm.fields_dict['serial_no'].get_query = (doc, cdt, cdn) => { - var cond = []; - var filter = [ - ['Serial No', 'docstatus', '!=', 2] - ]; - if(doc.item_code) { - cond = ['Serial No', 'item_code', '=', doc.item_code]; - filter.push(cond); - } - if(doc.customer) { - cond = ['Serial No', 'customer', '=', doc.customer]; - filter.push(cond); - } - return{ - filters:filter - } -} - -cur_frm.fields_dict['item_code'].get_query = (doc, cdt, cdn) => { - if(doc.serial_no) { - return{ - doctype: "Serial No", - fields: "item_code", - filters:{ - name: doc.serial_no - } - } - } - else{ - return{ - filters:[ - ['Item', 'docstatus', '!=', 2], - ['Item', 'disabled', '=', 0] - ] - } - } -}; +extend_cscript( + cur_frm.cscript, + new erpnext.support.WarrantyClaim({ frm: cur_frm }) +); From 592fc81260779ec42b35c88c6b8de19a598911d0 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 28 Nov 2023 18:28:48 +0530 Subject: [PATCH 22/28] fix: serial no status (#38391) --- .../delivery_note/test_delivery_note.py | 19 +++++++++++++++ erpnext/stock/doctype/serial_no/serial_no.js | 19 +++++++++++++++ .../stock/doctype/serial_no/serial_no.json | 4 ++-- .../serial_no_ledger/serial_no_ledger.py | 24 +++++++++++++++---- erpnext/stock/serial_batch_bundle.py | 6 ++++- 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 137c352e99..94655747e4 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1247,6 +1247,25 @@ class TestDeliveryNote(FrappeTestCase): dn.reload() self.assertFalse(dn.items[0].target_warehouse) + def test_serial_no_status(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item = make_item( + "Test Serial Item For Status", + {"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TESTSERIAL.#####"}, + ) + + item_code = item.name + pi = make_purchase_receipt(qty=1, item_code=item.name) + pi.reload() + serial_no = get_serial_nos_from_bundle(pi.items[0].serial_and_batch_bundle) + + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + + dn = create_delivery_note(qty=1, item_code=item_code, serial_no=serial_no) + dn.reload() + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered") + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/serial_no/serial_no.js b/erpnext/stock/doctype/serial_no/serial_no.js index 9d5555ed63..1cb9fd1800 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.js +++ b/erpnext/stock/doctype/serial_no/serial_no.js @@ -18,3 +18,22 @@ cur_frm.cscript.onload = function() { frappe.ui.form.on("Serial No", "refresh", function(frm) { frm.toggle_enable("item_code", frm.doc.__islocal); }); + + +frappe.ui.form.on("Serial No", { + refresh(frm) { + frm.trigger("view_ledgers") + }, + + view_ledgers(frm) { + frm.add_custom_button(__("View Ledgers"), () => { + frappe.route_options = { + "item_code": frm.doc.item_code, + "serial_no": frm.doc.name, + "posting_date": frappe.datetime.now_date(), + "posting_time": frappe.datetime.now_time() + }; + frappe.set_route("query-report", "Serial No Ledger"); + }).addClass('btn-primary'); + } +}) \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index ed1b0af30a..b4ece00fe6 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -269,7 +269,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "\nActive\nInactive\nExpired", + "options": "\nActive\nInactive\nDelivered\nExpired", "read_only": 1 }, { @@ -280,7 +280,7 @@ "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2023-04-16 15:58:46.139887", + "modified": "2023-11-28 15:37:59.489945", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py index 7212b92bb3..ae12fbb3e4 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -36,21 +36,27 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "company", "options": "Company", - "width": 150, + "width": 120, }, { "label": _("Warehouse"), "fieldtype": "Link", "fieldname": "warehouse", "options": "Warehouse", - "width": 150, + "width": 120, + }, + { + "label": _("Status"), + "fieldtype": "Data", + "fieldname": "status", + "width": 120, }, { "label": _("Serial No"), "fieldtype": "Link", "fieldname": "serial_no", "options": "Serial No", - "width": 150, + "width": 130, }, { "label": _("Valuation Rate"), @@ -58,6 +64,12 @@ def get_columns(filters): "fieldname": "valuation_rate", "width": 150, }, + { + "label": _("Qty"), + "fieldtype": "Float", + "fieldname": "qty", + "width": 150, + }, ] return columns @@ -83,12 +95,16 @@ def get_data(filters): "posting_time": row.posting_time, "voucher_type": row.voucher_type, "voucher_no": row.voucher_no, + "status": "Active" if row.actual_qty > 0 else "Delivered", "company": row.company, "warehouse": row.warehouse, + "qty": 1 if row.actual_qty > 0 else -1, } ) - serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, []) + serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}] + if row.serial_and_batch_bundle: + serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, []) for index, bundle_data in enumerate(serial_nos): if index == 0: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index da98455b5c..de28be1c35 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -255,11 +255,15 @@ class SerialBatchBundle: if not serial_nos: return + status = "Inactive" + if self.sle.actual_qty < 0: + status = "Delivered" + sn_table = frappe.qb.DocType("Serial No") ( frappe.qb.update(sn_table) .set(sn_table.warehouse, warehouse) - .set(sn_table.status, "Active" if warehouse else "Inactive") + .set(sn_table.status, "Active" if warehouse else status) .where(sn_table.name.isin(serial_nos)) ).run() From 23b0b8ba36595f8d1a62e44f51967a4d9a56641f Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 28 Nov 2023 19:24:15 +0530 Subject: [PATCH 23/28] fix: create contact if existing customer doesn't have contact --- erpnext/crm/doctype/lead/lead.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index fdec88d70d..d22cc5548a 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -36,6 +36,15 @@ class Lead(SellingController, CRMNote): def before_insert(self): self.contact_doc = None if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"): + if self.source == "Existing Customer" and self.customer: + contact = frappe.db.get_value( + "Dynamic Link", + {"link_doctype": "Customer", "link_name": self.customer}, + "parent", + ) + if contact: + self.contact_doc = frappe.get_doc("Contact", contact) + return self.contact_doc = self.create_contact() def after_insert(self): From 9fadf5f42678736567160bb2b06619383146d4ca Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Nov 2023 21:34:07 +0530 Subject: [PATCH 24/28] refactor: don't use `cur_frm` --- .../doctype/warranty_claim/warranty_claim.js | 55 +++++++------------ 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js index 32494efe53..10cb37f512 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js @@ -36,6 +36,26 @@ frappe.ui.form.on("Warranty Claim", { } }, + refresh: (frm) => { + frappe.dynamic_link = { + doc: frm.doc, + fieldname: "customer", + doctype: "Customer", + }; + + if ( + !frm.doc.__islocal && + ["Open", "Work In Progress"].includes(frm.doc.status) + ) { + frm.add_custom_button(__("Maintenance Visit"), () => { + frappe.model.open_mapped_doc({ + method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", + frm: frm, + }); + }); + } + }, + customer: (frm) => { erpnext.utils.get_party_details(frm); }, @@ -48,38 +68,3 @@ frappe.ui.form.on("Warranty Claim", { erpnext.utils.get_contact_details(frm); }, }); - -erpnext.support.WarrantyClaim = class WarrantyClaim extends ( - frappe.ui.form.Controller -) { - refresh() { - frappe.dynamic_link = { - doc: this.frm.doc, - fieldname: "customer", - doctype: "Customer", - }; - - if ( - !cur_frm.doc.__islocal && - (cur_frm.doc.status == "Open" || - cur_frm.doc.status == "Work In Progress") - ) { - cur_frm.add_custom_button( - __("Maintenance Visit"), - this.make_maintenance_visit - ); - } - } - - make_maintenance_visit() { - frappe.model.open_mapped_doc({ - method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", - frm: cur_frm, - }); - } -}; - -extend_cscript( - cur_frm.cscript, - new erpnext.support.WarrantyClaim({ frm: cur_frm }) -); From 663bb8726c25005ab83a6ca4c82814b82a98a1e2 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 29 Nov 2023 09:03:47 +0530 Subject: [PATCH 25/28] fix(regional): use net figures for sales calc (#38260) --- erpnext/regional/report/uae_vat_201/uae_vat_201.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 59ef58bfde..6ef21e52ca 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -141,7 +141,7 @@ def get_total_emiratewise(filters): return frappe.db.sql( """ select - s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount) + s.vat_emirate as emirate, sum(i.base_net_amount) as total, sum(i.tax_amount) from `tabSales Invoice Item` i inner join `tabSales Invoice` s on @@ -356,7 +356,7 @@ def get_zero_rated_total(filters): frappe.db.sql( """ select - sum(i.base_amount) as total + sum(i.base_net_amount) as total from `tabSales Invoice Item` i inner join `tabSales Invoice` s on @@ -383,7 +383,7 @@ def get_exempt_total(filters): frappe.db.sql( """ select - sum(i.base_amount) as total + sum(i.base_net_amount) as total from `tabSales Invoice Item` i inner join `tabSales Invoice` s on From 8e4b591ea2ad420a7c861a06414d3bee6e6ee042 Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Wed, 29 Nov 2023 05:52:47 +0100 Subject: [PATCH 26/28] fix: make create button translatable (#38165) * fix: make all create buttons translatable * style: use double quotes --------- Co-authored-by: PatrickDenis-stack <77415730+PatrickDenis-stack@users.noreply.github.com> Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/public/js/communication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index 7ce8b0913c..f205d88965 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", { frappe.confirm(__(confirm_msg, [__("Issue")]), () => { frm.trigger('make_issue_from_communication'); }) - }, "Create"); + }, __("Create")); } if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) { From fe5fc5bd3abfff57f85f62eac18f3ffc242baa4b Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 29 Nov 2023 12:14:12 +0530 Subject: [PATCH 27/28] feat: shift depreciation for assets (#38327) * feat: shift depreciation for assets * chore: create new asset depr schedule on shift change * refactor: move create_depr_schedule to after_insert * fix: args in get_depreciation_amount test * refactor: rename shift adjustment to shift allocation * chore: asset shift factor doctype and auto allocate shift diff * chore: use check instead of depr type, and add tests * chore: make linter happy * chore: give permissions to accounts users --- erpnext/assets/doctype/asset/asset.js | 41 ++- erpnext/assets/doctype/asset/asset.py | 1 + erpnext/assets/doctype/asset/test_asset.py | 7 +- .../asset_depreciation_schedule.js | 10 +- .../asset_depreciation_schedule.json | 11 +- .../asset_depreciation_schedule.py | 110 ++++++-- .../asset_finance_book.json | 10 +- .../asset_shift_allocation/__init__.py | 0 .../asset_shift_allocation.js | 14 + .../asset_shift_allocation.json | 111 ++++++++ .../asset_shift_allocation.py | 262 ++++++++++++++++++ .../test_asset_shift_allocation.py | 113 ++++++++ .../doctype/asset_shift_factor/__init__.py | 0 .../asset_shift_factor/asset_shift_factor.js | 8 + .../asset_shift_factor.json | 74 +++++ .../asset_shift_factor/asset_shift_factor.py | 24 ++ .../test_asset_shift_factor.py | 9 + .../depreciation_schedule.json | 9 +- ...sset_depreciation_schedules_from_assets.py | 1 + 19 files changed, 776 insertions(+), 39 deletions(-) create mode 100644 erpnext/assets/doctype/asset_shift_allocation/__init__.py create mode 100644 erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js create mode 100644 erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json create mode 100644 erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py create mode 100644 erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py create mode 100644 erpnext/assets/doctype/asset_shift_factor/__init__.py create mode 100644 erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js create mode 100644 erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json create mode 100644 erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py create mode 100644 erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index d378fbd26a..58fd6d4ef8 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -214,30 +214,43 @@ frappe.ui.form.on('Asset', { }) }, - render_depreciation_schedule_view: function(frm, depr_schedule) { + render_depreciation_schedule_view: function(frm, asset_depr_schedule_doc) { let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty(); let data = []; - depr_schedule.forEach((sch) => { + asset_depr_schedule_doc.depreciation_schedule.forEach((sch) => { const row = [ sch['idx'], frappe.format(sch['schedule_date'], { fieldtype: 'Date' }), frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' }), frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' }), - sch['journal_entry'] || '' + sch['journal_entry'] || '', ]; + + if (asset_depr_schedule_doc.shift_based) { + row.push(sch['shift']); + } + data.push(row); }); + let columns = [ + {name: __("No."), editable: false, resizable: false, format: value => value, width: 60}, + {name: __("Schedule Date"), editable: false, resizable: false, width: 270}, + {name: __("Depreciation Amount"), editable: false, resizable: false, width: 164}, + {name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164}, + ]; + + if (asset_depr_schedule_doc.shift_based) { + columns.push({name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 245}); + columns.push({name: __("Shift"), editable: false, resizable: false, width: 59}); + } else { + columns.push({name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 304}); + } + let datatable = new frappe.DataTable(wrapper.get(0), { - columns: [ - {name: __("No."), editable: false, resizable: false, format: value => value, width: 60}, - {name: __("Schedule Date"), editable: false, resizable: false, width: 270}, - {name: __("Depreciation Amount"), editable: false, resizable: false, width: 164}, - {name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164}, - {name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 304} - ], + columns: columns, data: data, layout: "fluid", serialNoColumn: false, @@ -272,8 +285,8 @@ frappe.ui.form.on('Asset', { asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); } - let depr_schedule = (await frappe.call( - "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", + let asset_depr_schedule_doc = (await frappe.call( + "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_asset_depr_schedule_doc", { asset_name: frm.doc.name, status: "Active", @@ -281,7 +294,7 @@ frappe.ui.form.on('Asset', { } )).message; - $.each(depr_schedule || [], function(i, v) { + $.each(asset_depr_schedule_doc.depreciation_schedule || [], function(i, v) { x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' })); var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount')); if(v.journal_entry) { @@ -296,7 +309,7 @@ frappe.ui.form.on('Asset', { }); frm.toggle_display(["depreciation_schedule_view"], 1); - frm.events.render_depreciation_schedule_view(frm, depr_schedule); + frm.events.render_depreciation_schedule_view(frm, asset_depr_schedule_doc); } else { if(frm.doc.opening_accumulated_depreciation) { x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' })); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7b7953b6ce..d1f03f2046 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -825,6 +825,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount): "total_number_of_depreciations": d.total_number_of_depreciations, "frequency_of_depreciation": d.frequency_of_depreciation, "daily_prorata_based": d.daily_prorata_based, + "shift_based": d.shift_based, "salvage_value_percentage": d.salvage_value_percentage, "expected_value_after_useful_life": flt(gross_purchase_amount) * flt(d.salvage_value_percentage / 100), diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index ca2b980579..dc80aa5c5c 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1000,7 +1000,11 @@ class TestDepreciationBasics(AssetSetup): }, ) - depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active") + + depreciation_amount = get_depreciation_amount( + asset_depr_schedule_doc, asset, 100000, asset.finance_books[0] + ) self.assertEqual(depreciation_amount, 30000) def test_make_depr_schedule(self): @@ -1732,6 +1736,7 @@ def create_asset(**args): "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, "depreciation_start_date": args.depreciation_start_date, "daily_prorata_based": args.daily_prorata_based or 0, + "shift_based": args.shift_based or 0, }, ) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js index 3d2dff179a..c99297d626 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js @@ -8,11 +8,13 @@ frappe.ui.form.on('Asset Depreciation Schedule', { }, make_schedules_editable: function(frm) { - var is_editable = frm.doc.depreciation_method == "Manual" ? true : false; + var is_manual_hence_editable = frm.doc.depreciation_method === "Manual" ? true : false; + var is_shift_hence_editable = frm.doc.shift_based ? true : false; - frm.toggle_enable("depreciation_schedule", is_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_editable); + frm.toggle_enable("depreciation_schedule", is_manual_hence_editable || is_shift_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_manual_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", is_shift_hence_editable); } }); diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json index 8d8b46321f..be35914251 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json @@ -20,6 +20,7 @@ "total_number_of_depreciations", "rate_of_depreciation", "daily_prorata_based", + "shift_based", "column_break_8", "frequency_of_depreciation", "expected_value_after_useful_life", @@ -184,12 +185,20 @@ "label": "Depreciate based on daily pro-rata", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.depreciation_method == \"Straight Line\"", + "fieldname": "shift_based", + "fieldtype": "Check", + "label": "Depreciate based on shifts", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-03 21:32:15.021796", + "modified": "2023-11-29 00:57:00.461998", "modified_by": "Administrator", "module": "Assets", "name": "Asset Depreciation Schedule", diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 7305691f97..6e390ce6f8 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -26,6 +26,7 @@ class AssetDepreciationSchedule(Document): self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name( self.asset, self.finance_book ) + self.update_shift_depr_schedule() def validate(self): self.validate_another_asset_depr_schedule_does_not_exist() @@ -73,6 +74,16 @@ class AssetDepreciationSchedule(Document): def on_cancel(self): self.db_set("status", "Cancelled") + def update_shift_depr_schedule(self): + if not self.shift_based or self.docstatus != 0: + return + + asset_doc = frappe.get_doc("Asset", self.asset) + fb_row = asset_doc.finance_books[self.finance_book_id - 1] + + self.make_depr_schedule(asset_doc, fb_row) + self.set_accumulated_depreciation(asset_doc, fb_row) + def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name): asset_doc = frappe.get_doc("Asset", asset_name) @@ -154,13 +165,14 @@ class AssetDepreciationSchedule(Document): self.rate_of_depreciation = row.rate_of_depreciation self.expected_value_after_useful_life = row.expected_value_after_useful_life self.daily_prorata_based = row.daily_prorata_based + self.shift_based = row.shift_based self.status = "Draft" def make_depr_schedule( self, asset_doc, row, - date_of_disposal, + date_of_disposal=None, update_asset_finance_book_row=True, value_after_depreciation=None, ): @@ -181,6 +193,8 @@ class AssetDepreciationSchedule(Document): num_of_depreciations_completed = 0 depr_schedule = [] + self.schedules_before_clearing = self.get("depreciation_schedule") + for schedule in self.get("depreciation_schedule"): if schedule.journal_entry: num_of_depreciations_completed += 1 @@ -246,6 +260,7 @@ class AssetDepreciationSchedule(Document): prev_depreciation_amount = 0 depreciation_amount = get_depreciation_amount( + self, asset_doc, value_after_depreciation, row, @@ -282,10 +297,7 @@ class AssetDepreciationSchedule(Document): ) if depreciation_amount > 0: - self.add_depr_schedule_row( - date_of_disposal, - depreciation_amount, - ) + self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n) break @@ -369,10 +381,7 @@ class AssetDepreciationSchedule(Document): skip_row = True if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0: - self.add_depr_schedule_row( - schedule_date, - depreciation_amount, - ) + self.add_depr_schedule_row(schedule_date, depreciation_amount, n) # to ensure that final accumulated depreciation amount is accurate def get_adjusted_depreciation_amount( @@ -394,16 +403,22 @@ class AssetDepreciationSchedule(Document): def get_depreciation_amount_for_first_row(self): return self.get("depreciation_schedule")[0].depreciation_amount - def add_depr_schedule_row( - self, - schedule_date, - depreciation_amount, - ): + def add_depr_schedule_row(self, schedule_date, depreciation_amount, schedule_idx): + if self.shift_based: + shift = ( + self.schedules_before_clearing[schedule_idx].shift + if self.schedules_before_clearing and len(self.schedules_before_clearing) > schedule_idx + else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name") + ) + else: + shift = None + self.append( "depreciation_schedule", { "schedule_date": schedule_date, "depreciation_amount": depreciation_amount, + "shift": shift, }, ) @@ -445,6 +460,7 @@ class AssetDepreciationSchedule(Document): and i == max(straight_line_idx) - 1 and not date_of_disposal and not date_of_return + and not row.shift_based ): depreciation_amount += flt( value_after_depreciation - flt(row.expected_value_after_useful_life), @@ -527,6 +543,7 @@ def get_total_days(date, frequency): def get_depreciation_amount( + asset_depr_schedule, asset, depreciable_value, fb_row, @@ -537,7 +554,7 @@ def get_depreciation_amount( ): if fb_row.depreciation_method in ("Straight Line", "Manual"): return get_straight_line_or_manual_depr_amount( - asset, fb_row, schedule_idx, number_of_pending_depreciations + asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations ) else: rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( @@ -559,8 +576,11 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb def get_straight_line_or_manual_depr_amount( - asset, row, schedule_idx, number_of_pending_depreciations + asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations ): + if row.shift_based: + return get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx) + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value if asset.flags.increase_in_asset_life: return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( @@ -655,6 +675,41 @@ def get_straight_line_or_manual_depr_amount( ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) +def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx): + if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation: + return ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + + asset_shift_factors_map = get_asset_shift_factors_map() + shift = ( + asset_depr_schedule.schedules_before_clearing[schedule_idx].shift + if len(asset_depr_schedule.schedules_before_clearing) > schedule_idx + else None + ) + shift_factor = asset_shift_factors_map.get(shift) if shift else 0 + + shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) + for schedule in asset_depr_schedule.schedules_before_clearing + ) + + return ( + ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) + / flt(shift_factors_sum) + ) * shift_factor + + +def get_asset_shift_factors_map(): + return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) + + def get_wdv_or_dd_depr_amount( depreciable_value, rate_of_depreciation, @@ -803,7 +858,12 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones( def get_temp_asset_depr_schedule_doc( - asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False + asset_doc, + row, + date_of_disposal=None, + date_of_return=None, + update_asset_finance_book_row=False, + new_depr_schedule=None, ): current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( asset_doc.name, "Active", row.finance_book @@ -818,6 +878,21 @@ def get_temp_asset_depr_schedule_doc( temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + if new_depr_schedule: + temp_asset_depr_schedule_doc.depreciation_schedule = [] + + for schedule in new_depr_schedule: + temp_asset_depr_schedule_doc.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( asset_doc, row, @@ -839,6 +914,7 @@ def get_depr_schedule(asset_name, status, finance_book=None): return asset_depr_schedule_doc.get("depreciation_schedule") +@frappe.whitelist() def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index e597d5fe31..25ae7a492c 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -9,6 +9,7 @@ "depreciation_method", "total_number_of_depreciations", "daily_prorata_based", + "shift_based", "column_break_5", "frequency_of_depreciation", "depreciation_start_date", @@ -97,12 +98,19 @@ "fieldname": "daily_prorata_based", "fieldtype": "Check", "label": "Depreciate based on daily pro-rata" + }, + { + "default": "0", + "depends_on": "eval:doc.depreciation_method == \"Straight Line\"", + "fieldname": "shift_based", + "fieldtype": "Check", + "label": "Depreciate based on shifts" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-03 21:30:24.266601", + "modified": "2023-11-29 00:57:07.579777", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_shift_allocation/__init__.py b/erpnext/assets/doctype/asset_shift_allocation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js new file mode 100644 index 0000000000..54df69339b --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js @@ -0,0 +1,14 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.ui.form.on('Asset Shift Allocation', { + onload: function(frm) { + frm.events.make_schedules_editable(frm); + }, + + make_schedules_editable: function(frm) { + frm.toggle_enable("depreciation_schedule", true); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", false); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", false); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", true); + } +}); diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json new file mode 100644 index 0000000000..89fa298a74 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2023-11-24 15:07:44.652133", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_esaa", + "asset", + "naming_series", + "column_break_tdae", + "finance_book", + "amended_from", + "depreciation_schedule_section", + "depreciation_schedule" + ], + "fields": [ + { + "fieldname": "section_break_esaa", + "fieldtype": "Section Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Shift Allocation", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fieldname": "column_break_tdae", + "fieldtype": "Column Break" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "depreciation_schedule_section", + "fieldtype": "Section Break", + "label": "Depreciation Schedule" + }, + { + "fieldname": "depreciation_schedule", + "fieldtype": "Table", + "label": "Depreciation Schedule", + "options": "Depreciation Schedule" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "ACC-ASA-.YYYY.-", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-11-29 04:05:04.683518", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Shift Allocation", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py new file mode 100644 index 0000000000..d419ef4c84 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py @@ -0,0 +1,262 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import ( + add_months, + cint, + flt, + get_last_day, + get_link_to_form, + is_last_day_of_the_month, +) + +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, + get_asset_shift_factors_map, + get_temp_asset_depr_schedule_doc, +) + + +class AssetShiftAllocation(Document): + def after_insert(self): + self.fetch_and_set_depr_schedule() + + def validate(self): + self.asset_depr_schedule_doc = get_asset_depr_schedule_doc( + self.asset, "Active", self.finance_book + ) + + self.validate_invalid_shift_change() + self.update_depr_schedule() + + def on_submit(self): + self.create_new_asset_depr_schedule() + + def fetch_and_set_depr_schedule(self): + if self.asset_depr_schedule_doc: + if self.asset_depr_schedule_doc.shift_based: + for schedule in self.asset_depr_schedule_doc.get("depreciation_schedule"): + self.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + + self.flags.ignore_validate = True + self.save() + else: + frappe.throw( + _( + "Asset Depreciation Schedule for Asset {0} and Finance Book {1} is not using shift based depreciation" + ).format(self.asset, self.finance_book) + ) + else: + frappe.throw( + _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( + self.asset, self.finance_book + ) + ) + + def validate_invalid_shift_change(self): + if not self.get("depreciation_schedule") or self.docstatus == 1: + return + + for i, sch in enumerate(self.depreciation_schedule): + if ( + sch.journal_entry and self.asset_depr_schedule_doc.depreciation_schedule[i].shift != sch.shift + ): + frappe.throw( + _( + "Row {0}: Shift cannot be changed since the depreciation has already been processed" + ).format(i) + ) + + def update_depr_schedule(self): + if not self.get("depreciation_schedule") or self.docstatus == 1: + return + + self.allocate_shift_diff_in_depr_schedule() + + asset_doc = frappe.get_doc("Asset", self.asset) + fb_row = asset_doc.finance_books[self.asset_depr_schedule_doc.finance_book_id - 1] + + asset_doc.flags.shift_allocation = True + + temp_depr_schedule = get_temp_asset_depr_schedule_doc( + asset_doc, fb_row, new_depr_schedule=self.depreciation_schedule + ).get("depreciation_schedule") + + self.depreciation_schedule = [] + + for schedule in temp_depr_schedule: + self.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + + def allocate_shift_diff_in_depr_schedule(self): + asset_shift_factors_map = get_asset_shift_factors_map() + reverse_asset_shift_factors_map = { + asset_shift_factors_map[k]: k for k in asset_shift_factors_map + } + + original_shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) + for schedule in self.asset_depr_schedule_doc.depreciation_schedule + ) + + new_shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.depreciation_schedule + ) + + diff = new_shift_factors_sum - original_shift_factors_sum + + if diff > 0: + for i, schedule in reversed(list(enumerate(self.depreciation_schedule))): + if diff <= 0: + break + + shift_factor = flt(asset_shift_factors_map.get(schedule.shift)) + + if shift_factor <= diff: + self.depreciation_schedule.pop() + diff -= shift_factor + else: + try: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get( + shift_factor - diff + ) + diff = 0 + except Exception: + frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format( + shift_factor - diff + ) + elif diff < 0: + shift_factors = list(asset_shift_factors_map.values()) + desc_shift_factors = sorted(shift_factors, reverse=True) + depr_schedule_len_diff = self.asset_depr_schedule_doc.total_number_of_depreciations - len( + self.depreciation_schedule + ) + subsets_result = [] + + if depr_schedule_len_diff > 0: + num_rows_to_add = depr_schedule_len_diff + + while not subsets_result and num_rows_to_add > 0: + find_subsets_with_sum(shift_factors, num_rows_to_add, abs(diff), [], subsets_result) + if subsets_result: + break + num_rows_to_add -= 1 + + if subsets_result: + for i in range(num_rows_to_add): + schedule_date = add_months( + self.depreciation_schedule[-1].schedule_date, + cint(self.asset_depr_schedule_doc.frequency_of_depreciation), + ) + + if is_last_day_of_the_month(self.depreciation_schedule[-1].schedule_date): + schedule_date = get_last_day(schedule_date) + + self.append( + "depreciation_schedule", + { + "schedule_date": schedule_date, + "shift": reverse_asset_shift_factors_map.get(subsets_result[0][i]), + }, + ) + + if depr_schedule_len_diff <= 0 or not subsets_result: + for i, schedule in reversed(list(enumerate(self.depreciation_schedule))): + diff = abs(diff) + + if diff <= 0: + break + + shift_factor = flt(asset_shift_factors_map.get(schedule.shift)) + + if shift_factor <= diff: + for sf in desc_shift_factors: + if sf - shift_factor <= diff: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(sf) + diff -= sf - shift_factor + break + else: + try: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get( + shift_factor + diff + ) + diff = 0 + except Exception: + frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format( + shift_factor + diff + ) + + def create_new_asset_depr_schedule(self): + new_asset_depr_schedule_doc = frappe.copy_doc(self.asset_depr_schedule_doc) + + new_asset_depr_schedule_doc.depreciation_schedule = [] + + for schedule in self.depreciation_schedule: + new_asset_depr_schedule_doc.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + + notes = _( + "This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}." + ).format( + get_link_to_form("Asset", self.asset), + get_link_to_form(self.doctype, self.name), + ) + + new_asset_depr_schedule_doc.notes = notes + + self.asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True + self.asset_depr_schedule_doc.cancel() + + new_asset_depr_schedule_doc.submit() + + add_asset_activity( + self.asset, + _("Asset's depreciation schedule updated after Asset Shift Allocation {0}").format( + get_link_to_form(self.doctype, self.name) + ), + ) + + +def find_subsets_with_sum(numbers, k, target_sum, current_subset, result): + if k == 0 and target_sum == 0: + result.append(current_subset.copy()) + return + if k <= 0 or target_sum <= 0 or not numbers: + return + + # Include the current number in the subset + find_subsets_with_sum( + numbers, k - 1, target_sum - numbers[0], current_subset + [numbers[0]], result + ) + + # Exclude the current number from the subset + find_subsets_with_sum(numbers[1:], k, target_sum, current_subset, result) diff --git a/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py new file mode 100644 index 0000000000..8d00a24f6b --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py @@ -0,0 +1,113 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import cstr + +from erpnext.assets.doctype.asset.test_asset import create_asset +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depr_schedule, +) + + +class TestAssetShiftAllocation(FrappeTestCase): + @classmethod + def setUpClass(cls): + create_asset_shift_factors() + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + def test_asset_shift_allocation(self): + asset = create_asset( + calculate_depreciation=1, + available_for_use_date="2023-01-01", + purchase_date="2023-01-01", + gross_purchase_amount=120000, + depreciation_start_date="2023-01-31", + total_number_of_depreciations=12, + frequency_of_depreciation=1, + shift_based=1, + submit=1, + ) + + expected_schedules = [ + ["2023-01-31", 10000.0, 10000.0, "Single"], + ["2023-02-28", 10000.0, 20000.0, "Single"], + ["2023-03-31", 10000.0, 30000.0, "Single"], + ["2023-04-30", 10000.0, 40000.0, "Single"], + ["2023-05-31", 10000.0, 50000.0, "Single"], + ["2023-06-30", 10000.0, 60000.0, "Single"], + ["2023-07-31", 10000.0, 70000.0, "Single"], + ["2023-08-31", 10000.0, 80000.0, "Single"], + ["2023-09-30", 10000.0, 90000.0, "Single"], + ["2023-10-31", 10000.0, 100000.0, "Single"], + ["2023-11-30", 10000.0, 110000.0, "Single"], + ["2023-12-31", 10000.0, 120000.0, "Single"], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in get_depr_schedule(asset.name, "Active") + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation = frappe.get_doc( + {"doctype": "Asset Shift Allocation", "asset": asset.name} + ).insert() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in asset_shift_allocation.get("depreciation_schedule") + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation = frappe.get_doc("Asset Shift Allocation", asset_shift_allocation.name) + asset_shift_allocation.depreciation_schedule[4].shift = "Triple" + asset_shift_allocation.save() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in asset_shift_allocation.get("depreciation_schedule") + ] + + expected_schedules = [ + ["2023-01-31", 10000.0, 10000.0, "Single"], + ["2023-02-28", 10000.0, 20000.0, "Single"], + ["2023-03-31", 10000.0, 30000.0, "Single"], + ["2023-04-30", 10000.0, 40000.0, "Single"], + ["2023-05-31", 20000.0, 60000.0, "Triple"], + ["2023-06-30", 10000.0, 70000.0, "Single"], + ["2023-07-31", 10000.0, 80000.0, "Single"], + ["2023-08-31", 10000.0, 90000.0, "Single"], + ["2023-09-30", 10000.0, 100000.0, "Single"], + ["2023-10-31", 10000.0, 110000.0, "Single"], + ["2023-11-30", 10000.0, 120000.0, "Single"], + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation.submit() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in get_depr_schedule(asset.name, "Active") + ] + + self.assertEqual(schedules, expected_schedules) + + +def create_asset_shift_factors(): + shifts = [ + {"doctype": "Asset Shift Factor", "shift_name": "Half", "shift_factor": 0.5, "default": 0}, + {"doctype": "Asset Shift Factor", "shift_name": "Single", "shift_factor": 1, "default": 1}, + {"doctype": "Asset Shift Factor", "shift_name": "Double", "shift_factor": 1.5, "default": 0}, + {"doctype": "Asset Shift Factor", "shift_name": "Triple", "shift_factor": 2, "default": 0}, + ] + + for s in shifts: + frappe.get_doc(s).insert() diff --git a/erpnext/assets/doctype/asset_shift_factor/__init__.py b/erpnext/assets/doctype/asset_shift_factor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js new file mode 100644 index 0000000000..88887fea87 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Asset Shift Factor", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json new file mode 100644 index 0000000000..fd04ffc5d4 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json @@ -0,0 +1,74 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:shift_name", + "creation": "2023-11-27 18:16:03.980086", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "shift_name", + "shift_factor", + "default" + ], + "fields": [ + { + "fieldname": "shift_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Shift Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "shift_factor", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Shift Factor", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-11-29 04:04:24.272872", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Shift Factor", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py new file mode 100644 index 0000000000..4c275ce092 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py @@ -0,0 +1,24 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class AssetShiftFactor(Document): + def validate(self): + self.validate_default() + + def validate_default(self): + if self.default: + existing_default_shift_factor = frappe.db.get_value( + "Asset Shift Factor", {"default": 1}, "name" + ) + + if existing_default_shift_factor: + frappe.throw( + _("Asset Shift Factor {0} is set as default currently. Please change it first.").format( + frappe.bold(existing_default_shift_factor) + ) + ) diff --git a/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py new file mode 100644 index 0000000000..75073673c0 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestAssetShiftFactor(FrappeTestCase): + pass diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json index 884e0c6cb2..ef706e8f25 100644 --- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json +++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json @@ -12,6 +12,7 @@ "column_break_3", "accumulated_depreciation_amount", "journal_entry", + "shift", "make_depreciation_entry" ], "fields": [ @@ -57,11 +58,17 @@ "fieldname": "make_depreciation_entry", "fieldtype": "Button", "label": "Make Depreciation Entry" + }, + { + "fieldname": "shift", + "fieldtype": "Link", + "label": "Shift", + "options": "Asset Shift Factor" } ], "istable": 1, "links": [], - "modified": "2023-07-26 12:56:48.718736", + "modified": "2023-11-27 18:28:35.325376", "modified_by": "Administrator", "module": "Assets", "name": "Depreciation Schedule", diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 9a2a39fb78..793497b766 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -86,6 +86,7 @@ def get_asset_finance_books_map(): afb.frequency_of_depreciation, afb.rate_of_depreciation, afb.expected_value_after_useful_life, + afb.shift_based, ) .where(asset.docstatus < 2) .orderby(afb.idx) From fddf341f444efa89f80ec0fcb4e35b9b840061f5 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 29 Nov 2023 12:21:11 +0530 Subject: [PATCH 28/28] fix: incorrect ordered qty for Subcontracting Order (#38415) --- .../tests/test_subcontracting_controller.py | 4 ++ .../subcontracting_order.py | 29 ++++++++- .../test_subcontracting_order.py | 62 +++++++++++++++++++ .../subcontracting_receipt.py | 6 +- 4 files changed, 96 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 6b61ae949d..47762ac4cf 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -1001,6 +1001,7 @@ def make_subcontracted_items(): "Subcontracted Item SA5": {}, "Subcontracted Item SA6": {}, "Subcontracted Item SA7": {}, + "Subcontracted Item SA8": {}, } for item, properties in sub_contracted_items.items(): @@ -1020,6 +1021,7 @@ def make_raw_materials(): }, "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"}, + "Subcontracted SRM Item 8": {}, } for item, properties in raw_materials.items(): @@ -1043,6 +1045,7 @@ def make_service_items(): "Subcontracted Service Item 5": {}, "Subcontracted Service Item 6": {}, "Subcontracted Service Item 7": {}, + "Subcontracted Service Item 8": {}, } for item, properties in service_items.items(): @@ -1066,6 +1069,7 @@ def make_bom_for_subcontracted_items(): "Subcontracted Item SA5": ["Subcontracted SRM Item 5"], "Subcontracted Item SA6": ["Subcontracted SRM Item 3"], "Subcontracted Item SA7": ["Subcontracted SRM Item 1"], + "Subcontracted Item SA8": ["Subcontracted SRM Item 8"], } for item_code, raw_materials in boms.items(): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index faf0cadb75..70ca1c31f5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -8,7 +8,7 @@ from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created from erpnext.controllers.subcontracting_controller import SubcontractingController -from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty +from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.utils import get_bin @@ -114,7 +114,32 @@ class SubcontractingOrder(SubcontractingController): ): item_wh_list.append([item.item_code, item.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)}) + update_bin_qty( + item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)} + ) + + @staticmethod + def get_ordered_qty(item_code, warehouse): + table = frappe.qb.DocType("Subcontracting Order") + child = frappe.qb.DocType("Subcontracting Order Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select((child.qty - child.received_qty) * child.conversion_factor) + .where( + (table.docstatus == 1) + & (child.item_code == item_code) + & (child.warehouse == warehouse) + & (child.qty > child.received_qty) + & (table.status != "Completed") + ) + ) + + query = query.run() + + return flt(query[0][0]) if query else 0 def update_reserved_qty_for_subcontracting(self): for item in self.supplied_items: diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 22fdc13cc1..3557858935 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -6,6 +6,7 @@ from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order from erpnext.controllers.subcontracting_controller import ( @@ -566,6 +567,67 @@ class TestSubcontractingOrder(FrappeTestCase): self.assertEqual(sco.status, "Closed") self.assertEqual(sco.supplied_items[0].returned_qty, 5) + def test_ordered_qty_for_subcontracting_order(self): + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 8", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA8", + "fg_item_qty": 10, + }, + ] + + ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + ordered_qty = flt(ordered_qty) + + sco = get_subcontracting_order(service_items=service_items) + sco.reload() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + new_ordered_qty = flt(new_ordered_qty) + + self.assertEqual(ordered_qty + 10, new_ordered_qty) + + for row in sco.supplied_items: + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code=row.rm_item_code, + qty=row.required_qty, + basic_rate=100, + ) + + scr = make_subcontracting_receipt(sco.name) + scr.submit() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + + self.assertEqual(ordered_qty, new_ordered_qty) + + scr.reload() + scr.cancel() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + + self.assertEqual(ordered_qty + 10, new_ordered_qty) + def create_subcontracting_order(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 8d705aa97d..ae64cc632a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -95,12 +95,12 @@ class SubcontractingReceipt(SubcontractingController): ) self.update_status_updater_args() self.update_prevdoc_status() - self.update_stock_ledger() - self.make_gl_entries_on_cancel() - self.repost_future_sle_and_gle() self.delete_auto_created_batches() self.set_consumed_qty_in_subcontract_order() self.set_subcontracting_order_status() + self.update_stock_ledger() + self.make_gl_entries_on_cancel() + self.repost_future_sle_and_gle() self.update_status() def validate_items_qty(self):