From 6c748966e732387383399389176afeb2fcc1ab7d Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Mon, 6 Sep 2021 17:27:47 +0500 Subject: [PATCH 01/54] feat: Asset Capitalization Form --- .../doctype/asset_capitalization/__init__.py | 0 .../asset_capitalization.js | 389 +++++++++++++++++ .../asset_capitalization.json | 340 +++++++++++++++ .../asset_capitalization.py | 402 ++++++++++++++++++ .../test_asset_capitalization.py | 8 + .../__init__.py | 0 .../asset_capitalization_asset_item.json | 85 ++++ .../asset_capitalization_asset_item.py | 8 + .../__init__.py | 0 .../asset_capitalization_service_item.json | 129 ++++++ .../asset_capitalization_service_item.py | 8 + .../__init__.py | 0 .../asset_capitalization_stock_item.json | 137 ++++++ .../asset_capitalization_stock_item.py | 8 + erpnext/assets/workspace/assets/assets.json | 13 +- 15 files changed, 1526 insertions(+), 1 deletion(-) create mode 100644 erpnext/assets/doctype/asset_capitalization/__init__.py create mode 100644 erpnext/assets/doctype/asset_capitalization/asset_capitalization.js create mode 100644 erpnext/assets/doctype/asset_capitalization/asset_capitalization.json create mode 100644 erpnext/assets/doctype/asset_capitalization/asset_capitalization.py create mode 100644 erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py create mode 100644 erpnext/assets/doctype/asset_capitalization_asset_item/__init__.py create mode 100644 erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json create mode 100644 erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py create mode 100644 erpnext/assets/doctype/asset_capitalization_service_item/__init__.py create mode 100644 erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json create mode 100644 erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py create mode 100644 erpnext/assets/doctype/asset_capitalization_stock_item/__init__.py create mode 100644 erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json create mode 100644 erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py diff --git a/erpnext/assets/doctype/asset_capitalization/__init__.py b/erpnext/assets/doctype/asset_capitalization/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js new file mode 100644 index 00000000000..9276d00c053 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -0,0 +1,389 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.provide("erpnext.assets"); + + +erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController { + setup() { + this.setup_posting_date_time_check(); + } + + onload() { + this.setup_queries(); + } + + refresh() { + erpnext.hide_company(); + } + + setup_queries() { + var me = this; + + me.setup_warehouse_query(); + + me.frm.set_query("target_item_code", function() { + return erpnext.queries.item(); + }); + + me.frm.set_query("target_asset", function() { + var filters = {}; + + if (me.frm.doc.target_item_code) { + filters['item_code'] = me.frm.doc.target_item_code; + } + + filters['status'] = ["not in", ["Draft", "Scrapped", "Sold"]] + filters['docstatus'] = 1; + + return { + filters: filters + } + }); + + me.frm.set_query("asset", "asset_items", function() { + var filters = { + 'status': ["not in", ["Draft", "Scrapped", "Sold"]], + 'docstatus': 1 + } + + if (me.frm.doc.target_asset) { + filters['name'] = ['!=', me.frm.doc.target_asset] + } + + return { + filters: filters + } + }); + + me.frm.set_query("item_code", "stock_items", function() { + return erpnext.queries.item({"is_stock_item": 1}); + }); + + me.frm.set_query("item_code", "service_items", function() { + return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 0}); + }); + + me.frm.set_query('batch_no', 'stock_items', function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + if(!item.item_code) { + frappe.throw(__("Please enter Item Code to get Batch Number")); + } else { + var filters = { + 'item_code': item.item_code, + 'posting_date': me.frm.doc.posting_date || frappe.datetime.nowdate(), + 'warehouse': item.warehouse + } + + return { + query : "erpnext.controllers.queries.get_batch_no", + filters: filters + } + } + }); + + me.frm.set_query('expense_account', 'service_items', function() { + return { + filters: { + "account_type": ['in', ["Tax", "Expense Account", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], + "is_group": 0, + "company": me.frm.doc.company + } + }; + }); + } + + target_item_code() { + return this.get_target_item_details(); + } + + target_asset() { + return this.get_target_asset_details(); + } + + item_code(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Stock Item") { + this.get_consumed_stock_item_details(row); + } else if (cdt == "Asset Capitalization Service Item") { + this.get_service_item_details(row); + } + } + + warehouse(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Stock Item") { + this.get_warehouse_details(row); + } + } + + asset(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Asset Item") { + this.get_consumed_asset_details(row); + } + } + + posting_date() { + if (this.frm.doc.posting_date) { + this.get_all_item_warehouse_details(); + } + } + + posting_time() { + if (this.frm.doc.posting_time) { + this.get_all_item_warehouse_details(); + } + } + + finance_book() { + this.get_all_asset_values(); + } + + stock_qty() { + this.calculate_totals(); + } + + qty() { + this.calculate_totals(); + } + + rate() { + this.calculate_totals(); + } + + company() { + var me = this; + + if (me.frm.doc.company) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Company", + filters: {"name": me.frm.doc.company}, + fieldname: "cost_center" + }, + callback: function (r) { + if (r.message) { + $.each(me.frm.doc.service_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", r.message.cost_center); + }); + } + } + }); + } + + erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype); + } + + serivce_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'service_items'); + } + + get_target_item_details() { + var me = this; + + if (me.frm.doc.target_item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_item_details", + child: me.frm.doc, + args: { + item_code: me.frm.doc.target_item_code, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + + get_target_asset_details() { + var me = this; + + if (me.frm.doc.target_asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", + child: me.frm.doc, + args: { + asset: me.frm.doc.target_asset + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + + get_consumed_stock_item_details(row) { + var me = this; + + if (row && row.item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_stock_item_details", + child: row, + args: { + args: { + item_code: row.item_code, + warehouse: row.warehouse, + stock_qty: flt(row.stock_qty), + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + company: me.frm.doc.company, + posting_date: me.frm.doc.posting_date, + posting_time: me.frm.doc.posting_time, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_consumed_asset_details(row) { + var me = this; + + if (row && row.asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_asset_details", + child: row, + args: { + args: { + asset: row.asset, + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + company: me.frm.doc.company, + finance_book: me.frm.doc.finance_book, + posting_date: me.frm.doc.posting_date, + posting_time: me.frm.doc.posting_time, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_service_item_details(row) { + var me = this; + + if (row && row.item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_service_item_details", + child: row, + args: { + args: { + item_code: row.item_code, + qty: flt(row.qty), + expense_account: row.expense_account, + company: me.frm.doc.company, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_warehouse_details(item) { + var me = this; + if(item.item_code && item.warehouse) { + me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details", + child: item, + args: { + args: { + 'item_code': item.item_code, + 'warehouse': cstr(item.warehouse), + 'qty': flt(item.stock_qty), + 'serial_no': item.serial_no, + 'posting_date': me.frm.doc.posting_date, + 'posting_time': me.frm.doc.posting_time, + 'company': me.frm.doc.company, + 'voucher_type': me.frm.doc.doctype, + 'voucher_no': me.frm.doc.name, + 'allow_zero_valuation': 1 + } + }, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_all_item_warehouse_details() { + var me = this; + me.frm.call({ + method: "set_warehouse_details", + doc: me.frm.doc, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + + get_all_asset_values() { + var me = this; + me.frm.call({ + method: "set_asset_values", + doc: me.frm.doc, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + + calculate_totals() { + var me = this; + + me.frm.doc.stock_items_total = 0; + me.frm.doc.asset_items_total = 0; + me.frm.doc.service_items_total = 0; + + $.each(me.frm.doc.stock_items || [], function (i, d) { + d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), precision('amount', d)); + me.frm.doc.stock_items_total += d.amount; + }); + + $.each(me.frm.doc.asset_items || [], function (i, d) { + d.asset_value = flt(flt(d.asset_value), precision('asset_value', d)); + me.frm.doc.asset_items_total += d.asset_value; + }); + + $.each(me.frm.doc.service_items || [], function (i, d) { + d.amount = flt(flt(d.qty) * flt(d.rate), precision('amount', d)); + me.frm.doc.service_items_total += d.amount; + }); + + me.frm.doc.stock_items_total = flt(me.frm.doc.stock_items_total, precision('stock_items_total')); + me.frm.doc.asset_items_total = flt(me.frm.doc.asset_items_total, precision('asset_items_total')); + me.frm.doc.service_items_total = flt(me.frm.doc.service_items_total, precision('service_items_total')); + + me.frm.doc.total_value = me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total; + me.frm.doc.total_value = flt(me.frm.doc.total_value, precision('total_value')); + + me.frm.refresh_fields(); + } +}; + +//$.extend(cur_frm.cscript, new erpnext.assets.AssetCapitalization({frm: cur_frm})); +cur_frm.cscript = new erpnext.assets.AssetCapitalization({frm: cur_frm}); diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json new file mode 100644 index 00000000000..b697c206bf2 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -0,0 +1,340 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2021-09-04 13:38:04.217187", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "target_item_code", + "target_item_name", + "target_is_fixed_asset", + "target_has_batch_no", + "target_has_serial_no", + "entry_type", + "finance_book", + "naming_series", + "column_break_9", + "company", + "posting_date", + "posting_time", + "set_posting_time", + "amended_from", + "target_item_details_section", + "target_asset", + "target_asset_name", + "target_warehouse", + "target_batch_no", + "target_serial_no", + "column_break_5", + "target_qty", + "target_stock_uom", + "section_break_16", + "stock_items", + "stock_items_total", + "section_break_26", + "asset_items", + "asset_items_total", + "service_expenses_section", + "service_items", + "service_items_total", + "totals_section", + "total_value" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title" + }, + { + "fieldname": "target_item_code", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Target Item Code", + "options": "Item", + "reqd": 1 + }, + { + "depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code", + "fetch_from": "target_item_code.item_name", + "fieldname": "target_item_name", + "fieldtype": "Data", + "label": "Target Item Name", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.is_fixed_asset", + "fieldname": "target_is_fixed_asset", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Is Fixed Asset", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:!doc.target_item_code || doc.target_is_fixed_asset", + "fieldname": "target_asset", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Target Asset", + "no_copy": 1, + "options": "Asset" + }, + { + "depends_on": "target_asset", + "fetch_from": "target_asset.asset_name", + "fieldname": "target_asset_name", + "fieldtype": "Data", + "label": "Asset Name", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fetch_from": "asset.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Posting Date", + "no_copy": 1, + "reqd": 1, + "search_index": 1 + }, + { + "default": "Now", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.docstatus==0", + "fieldname": "set_posting_time", + "fieldtype": "Check", + "label": "Edit Posting Date and Time" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "ACC-ASC-.YYYY.-", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Capitalization", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break", + "label": "Consumed Stock Items" + }, + { + "fieldname": "stock_items", + "fieldtype": "Table", + "label": "Stock Items", + "options": "Asset Capitalization Stock Item" + }, + { + "fieldname": "target_item_details_section", + "fieldtype": "Section Break", + "label": "Target Item Details" + }, + { + "depends_on": "eval:!doc.target_is_fixed_asset", + "fieldname": "target_warehouse", + "fieldtype": "Link", + "label": "Target Warehouse", + "options": "Warehouse" + }, + { + "depends_on": "target_has_batch_no", + "fieldname": "target_batch_no", + "fieldtype": "Link", + "label": "Target Batch No", + "options": "Batch" + }, + { + "default": "1", + "fieldname": "target_qty", + "fieldtype": "Float", + "label": "Target Qty", + "read_only_depends_on": "target_is_fixed_asset" + }, + { + "fetch_from": "target_item_code.stock_uom", + "fieldname": "target_stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.has_batch_no", + "fieldname": "target_has_batch_no", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Has Batch No", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.has_serial_no", + "fieldname": "target_has_serial_no", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Has Serial No", + "read_only": 1 + }, + { + "depends_on": "target_has_serial_no", + "fieldname": "target_serial_no", + "fieldtype": "Small Text", + "label": "Target Serial No" + }, + { + "fieldname": "section_break_26", + "fieldtype": "Section Break", + "label": "Consumed Asset Items" + }, + { + "fieldname": "asset_items", + "fieldtype": "Table", + "label": "Assets", + "options": "Asset Capitalization Asset Item" + }, + { + "fieldname": "entry_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Entry Type", + "options": "\nCapitalization\nDecapitalization", + "read_only": 1 + }, + { + "fieldname": "stock_items_total", + "fieldtype": "Currency", + "label": "Consumed Stock Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "asset_items_total", + "fieldtype": "Currency", + "label": "Consumed Asset Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "fieldname": "service_expenses_section", + "fieldtype": "Section Break", + "label": "Service Expenses" + }, + { + "fieldname": "service_items", + "fieldtype": "Table", + "label": "Services", + "options": "Asset Capitalization Service Item" + }, + { + "fieldname": "service_items_total", + "fieldtype": "Currency", + "label": "Service Expense Total Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "totals_section", + "fieldtype": "Section Break", + "label": "Totals" + }, + { + "fieldname": "total_value", + "fieldtype": "Currency", + "label": "Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-09-06 17:18:31.881006", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title", + "track_changes": 1, + "track_seen": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py new file mode 100644 index 00000000000..586710a635c --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -0,0 +1,402 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from erpnext.controllers.accounts_controller import AccountsController +from frappe.utils import cint, flt +from erpnext.stock.get_item_details import get_item_warehouse, get_default_expense_account, get_default_cost_center +from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.setup.doctype.brand.brand import get_brand_defaults +from erpnext.stock.utils import get_incoming_rate +from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import get_current_asset_value +from six import string_types +import json + +force_fields = ['target_item_name', 'target_asset_name', 'item_name', 'asset_name', + 'target_is_fixed_asset', 'target_has_serial_no', 'target_has_batch_no', + 'target_stock_uom', 'stock_uom'] + + +class AssetCapitalization(AccountsController): + def validate(self): + self.validate_posting_time() + self.set_missing_values(for_validate=True) + self.set_entry_type() + self.validate_target_item() + self.validate_target_asset() + self.validate_consumed_stock_item() + self.validate_consumed_asset_item() + self.validate_service_item() + self.set_warehouse_details() + self.set_asset_values() + self.calculate_totals() + self.set_title() + + def set_entry_type(self): + self.entry_type = "Capitalization" if self.target_is_fixed_asset else "Decapitalization" + + def set_title(self): + self.title = self.target_asset_name or self.target_item_name or self.target_item_code + + def set_missing_values(self, for_validate=False): + target_item_details = get_target_item_details(self.target_item_code) + for k, v in target_item_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + + # Remove asset if item not a fixed asset + if not self.target_is_fixed_asset: + self.target_asset = None + + target_asset_details = get_target_asset_details(self.target_asset) + for k, v in target_asset_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + + for d in self.stock_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + consumed_stock_item_details = get_consumed_stock_item_details(args, get_valuation_rate=False) + for k, v in consumed_stock_item_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + for d in self.asset_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + consumed_asset_details = get_consumed_asset_details(args, get_asset_value=False) + for k, v in consumed_asset_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + for d in self.service_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + service_item_details = get_service_item_details(args) + for k, v in service_item_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + def validate_target_item(self): + target_item = frappe.get_cached_doc("Item", self.target_item_code) + + if not target_item.is_fixed_asset and not target_item.is_stock_item: + frappe.throw(_("Target Item {0} is neither a Fixed Asset nor a Stock Item") + .format(target_item.name)) + + if target_item.is_fixed_asset: + self.target_qty = 1 + + if not target_item.is_stock_item: + self.target_warehouse = None + if not target_item.is_fixed_asset: + self.target_asset = None + if not target_item.has_batch_no: + self.target_batch_no = None + if not target_item.has_serial_no: + self.target_serial_no = "" + + self.validate_item(target_item) + + def validate_target_asset(self): + if self.target_is_fixed_asset and not self.target_asset: + frappe.throw(_("Target Asset is mandatory for Capitalization")) + + if self.target_asset: + target_asset = self.get_asset_for_validation(self.target_asset) + + if target_asset.item_code != self.target_item_code: + frappe.throw(_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)) + + self.validate_asset(target_asset) + + def validate_consumed_stock_item(self): + for d in self.stock_items: + if d.item_code: + item = frappe.get_cached_doc("Item", d.item_code) + + if not item.is_stock_item: + frappe.throw(_("Row #{0}: Item {1} is not a stock item").format(d.idx, d.item_code)) + + if flt(d.stock_qty) <= 0: + frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx)) + + self.validate_item(item) + + def validate_consumed_asset_item(self): + for d in self.asset_items: + if d.asset: + if d.asset == self.target_asset: + frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset") + .format(d.idx, d.asset)) + + asset = self.get_asset_for_validation(d.asset) + self.validate_asset(asset) + + def validate_service_item(self): + for d in self.service_items: + if d.item_code: + item = frappe.get_cached_doc("Item", d.item_code) + + if item.is_stock_item or item.is_fixed_asset: + frappe.throw(_("Row #{0}: Item {1} is not a service item").format(d.idx, d.item_code)) + + if flt(d.qty) <= 0: + frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx)) + + if flt(d.amount) <= 0: + frappe.throw(_("Row #{0}: Amount must be a positive number").format(d.idx)) + + self.validate_item(item) + + if not d.cost_center: + d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") + + def validate_item(self, item): + from erpnext.stock.doctype.item.item import validate_end_of_life + validate_end_of_life(item.name, item.end_of_life, item.disabled) + + def get_asset_for_validation(self, asset): + return frappe.db.get_value("Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1) + + def validate_asset(self, asset): + if asset.status in ("Draft", "Scrapped", "Sold"): + frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status)) + + if asset.docstatus == 0: + frappe.throw(_("Asset {0} is Draft").format(asset.name)) + if asset.docstatus == 2: + frappe.throw(_("Asset {0} is cancelled").format(asset.name)) + + if asset.company != self.company: + frappe.throw(_("Asset {0} does not belong to company {1}").format(self.target_asset, self.company)) + + @frappe.whitelist() + def set_warehouse_details(self): + for d in self.stock_items: + if d.item_code and d.warehouse: + args = self.get_args_for_incoming_rate(d) + warehouse_details = get_warehouse_details(args) + d.update(warehouse_details) + + @frappe.whitelist() + def set_asset_values(self): + for d in self.asset_items: + if d.asset: + d.asset_value = flt(get_current_asset_value(d.asset, self.finance_book)) + + def get_args_for_incoming_rate(self, item): + return frappe._dict({ + "item_code": item.item_code, + "warehouse": item.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * flt(item.stock_qty), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": cint(item.get('allow_zero_valuation_rate')), + }) + + def calculate_totals(self): + self.stock_items_total = 0 + self.asset_items_total = 0 + self.service_items_total = 0 + + for d in self.stock_items: + d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), d.precision('amount')) + self.stock_items_total += d.amount + + for d in self.asset_items: + d.asset_value = flt(flt(d.asset_value), d.precision('asset_value')) + self.asset_items_total += d.asset_value + + for d in self.service_items: + d.amount = flt(flt(d.qty) * flt(d.rate), d.precision('amount')) + self.service_items_total += d.amount + + self.stock_items_total = flt(self.stock_items_total, self.precision('stock_items_total')) + self.asset_items_total = flt(self.asset_items_total, self.precision('asset_items_total')) + self.service_items_total = flt(self.service_items_total, self.precision('service_items_total')) + + self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total + self.total_value = flt(self.total_value, self.precision('total_value')) + + +@frappe.whitelist() +def get_target_item_details(item_code=None): + out = frappe._dict() + + # Get Item Details + item = frappe._dict() + if item_code: + item = frappe.get_cached_doc("Item", item_code) + + # Set Item Details + out.target_item_name = item.item_name + out.target_stock_uom = item.stock_uom + out.target_is_fixed_asset = cint(item.is_fixed_asset) + out.target_has_batch_no = cint(item.has_batch_no) + out.target_has_serial_no = cint(item.has_serial_no) + + if out.target_is_fixed_asset: + out.target_qty = 1 + out.target_warehouse = None + else: + out.target_asset = None + + if not out.target_has_batch_no: + out.target_batch_no = None + if not out.target_has_serial_no: + out.target_serial_no = "" + + # Set Entry Type + if not item_code: + out.entry_type = "" + elif out.target_is_fixed_asset: + out.entry_type = "Capitalization" + else: + out.entry_type = "Decapitalization" + + return out + + +@frappe.whitelist() +def get_target_asset_details(asset=None): + out = frappe._dict() + + # Get Asset Details + asset_details = frappe._dict() + if asset: + asset_details = frappe.db.get_value("Asset", asset, ['asset_name', 'item_code'], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(asset)) + + # Re-set item code from Asset + out.target_item_code = asset_details.item_code + + # Set Asset Details + out.asset_name = asset_details.asset_name + + return out + + +@frappe.whitelist() +def get_consumed_stock_item_details(args, get_valuation_rate=True): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + item = frappe._dict() + if args.item_code: + item = frappe.get_cached_doc("Item", args.item_code) + + out.item_name = item.item_name + out.batch_no = None + out.serial_no = "" + + out.stock_qty = flt(args.stock_qty) or 1 + out.stock_uom = item.stock_uom + + out.warehouse = get_item_warehouse(item, args, overwrite_warehouse=True) if item else None + + if get_valuation_rate: + if args.item_code and out.warehouse: + incoming_rate_args = frappe._dict({ + 'item_code': args.item_code, + 'warehouse': out.warehouse, + 'posting_date': args.posting_date, + 'posting_time': args.posting_time, + 'qty': -1 * flt(out.stock_qty), + "voucher_type": args.doctype, + "voucher_no": args.name, + "company": args.company, + }) + out.update(get_warehouse_details(incoming_rate_args)) + else: + out.valuation_rate = 0 + out.actual_qty = 0 + + return out + + +@frappe.whitelist() +def get_warehouse_details(args): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + + out = {} + if args.warehouse and args.item_code: + out = { + "actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0, + "valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False) + } + return out + + +@frappe.whitelist() +def get_consumed_asset_details(args, get_asset_value=True): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + asset_details = frappe._dict() + if args.asset: + asset_details = frappe.db.get_value("Asset", args.asset, ['asset_name', 'item_code', 'item_name'], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(args.asset)) + + out.item_code = asset_details.item_code + out.asset_name = asset_details.asset_name + out.item_name = asset_details.item_name + + if get_asset_value: + if args.asset: + out.asset_value = flt(get_current_asset_value(args.asset, finance_book=args.finance_book)) + else: + out.asset_value = 0 + + return out + + +@frappe.whitelist() +def get_service_item_details(args): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + item = frappe._dict() + if args.item_code: + item = frappe.get_cached_doc("Item", args.item_code) + + out.item_name = item.item_name + out.qty = flt(args.qty) or 1 + out.uom = item.purchase_uom or item.stock_uom + + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + + out.expense_account = get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + + return out diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py new file mode 100644 index 00000000000..d8e22c51011 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestAssetCapitalization(unittest.TestCase): + pass diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/__init__.py b/erpnext/assets/doctype/asset_capitalization_asset_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json new file mode 100644 index 00000000000..a0040338c0b --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json @@ -0,0 +1,85 @@ +{ + "actions": [], + "creation": "2021-09-05 15:52:10.124538", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "asset", + "asset_name", + "column_break_3", + "item_code", + "item_name", + "section_break_6", + "asset_value", + "column_break_9" + ], + "fields": [ + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fetch_from": "asset.asset_name", + "fieldname": "asset_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Asset Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "asset.item_code", + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Value" + }, + { + "default": "0", + "fieldname": "asset_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Asset Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-06 13:46:04.892863", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Asset Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py new file mode 100644 index 00000000000..8817317e70c --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class AssetCapitalizationAssetItem(Document): + pass diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/__init__.py b/erpnext/assets/doctype/asset_capitalization_service_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json new file mode 100644 index 00000000000..2d3584dce47 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json @@ -0,0 +1,129 @@ +{ + "actions": [], + "creation": "2021-09-06 13:32:08.642060", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "expense_account", + "section_break_6", + "qty", + "uom", + "column_break_9", + "rate", + "amount", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project" + ], + "fields": [ + { + "bold": 1, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item" + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Expense Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Qty and Rate" + }, + { + "columns": 1, + "default": "1", + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1 + }, + { + "columns": 1, + "fetch_from": "stock_item_code.stock_uom", + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "Company:company:default_currency" + }, + { + "default": "0", + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-06 14:06:34.768152", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Service Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py new file mode 100644 index 00000000000..fa158295ae7 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class AssetCapitalizationServiceItem(Document): + pass diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/__init__.py b/erpnext/assets/doctype/asset_capitalization_stock_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json new file mode 100644 index 00000000000..19c455894a5 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -0,0 +1,137 @@ +{ + "actions": [], + "creation": "2021-09-05 15:23:23.492310", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "warehouse", + "section_break_6", + "stock_qty", + "stock_uom", + "actual_qty", + "column_break_9", + "valuation_rate", + "amount", + "batch_and_serial_no_section", + "batch_no", + "column_break_13", + "serial_no" + ], + "fields": [ + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Qty and Rate" + }, + { + "columns": 1, + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1 + }, + { + "columns": 1, + "fetch_from": "stock_item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Stock UOM", + "options": "UOM", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Valuation Rate", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "batch_and_serial_no_section", + "fieldtype": "Section Break", + "label": "Batch and Serial No" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "actual_qty", + "fieldtype": "Float", + "label": "Actual Qty in Warehouse", + "no_copy": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-06 13:46:13.579140", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Stock Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py new file mode 100644 index 00000000000..4449538d8e2 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class AssetCapitalizationStockItem(Document): + pass diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index dfbf1a378e5..cf437a83755 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -137,6 +137,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Asset", + "hidden": 0, + "is_query_report": 0, + "label": "Asset Capitalization", + "link_count": 0, + "link_to": "Asset Capitalization", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -179,7 +190,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:15:54.839452", + "modified": "2021-09-06 16:59:02.668813", "modified_by": "Administrator", "module": "Assets", "name": "Assets", From 702b5c32c1e4c1e30bf9e84ac738df30a3b4a435 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Wed, 8 Sep 2021 16:36:07 +0500 Subject: [PATCH 02/54] feat(Asset Capitalization): Accounting Fields --- .../asset_capitalization.js | 43 ++++++++++------ .../asset_capitalization.json | 43 +++++++++++++++- .../asset_capitalization.py | 50 +++++++++++++++++-- .../asset_capitalization_asset_item.json | 31 +++++++++++- .../asset_capitalization_service_item.json | 11 +--- .../asset_capitalization_stock_item.json | 23 ++++++++- 6 files changed, 166 insertions(+), 35 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 9276d00c053..b42634a5091 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -148,6 +148,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s this.calculate_totals(); } + target_qty() { + this.calculate_totals(); + } + rate() { this.calculate_totals(); } @@ -156,26 +160,29 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s var me = this; if (me.frm.doc.company) { - frappe.call({ - method: "frappe.client.get_value", - args: { - doctype: "Company", - filters: {"name": me.frm.doc.company}, - fieldname: "cost_center" - }, - callback: function (r) { - if (r.message) { - $.each(me.frm.doc.service_items || [], function (i, d) { - frappe.model.set_value(d.doctype, d.name, "cost_center", r.message.cost_center); - }); - } - } + frappe.model.set_value(me.frm.doc.doctype, me.frm.doc.name, "cost_center", null); + $.each(me.frm.doc.stock_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); + }); + $.each(me.frm.doc.asset_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); + }); + $.each(me.frm.doc.service_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); }); } erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype); } + stock_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'stock_items'); + } + + asset_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'asset_items'); + } + serivce_items_add(doc, cdt, cdn) { erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'service_items'); } @@ -189,6 +196,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s child: me.frm.doc, args: { item_code: me.frm.doc.target_item_code, + company: me.frm.doc.company, }, callback: function (r) { if (!r.exc) { @@ -207,7 +215,8 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", child: me.frm.doc, args: { - asset: me.frm.doc.target_asset + asset: me.frm.doc.target_asset, + company: me.frm.doc.company, }, callback: function (r) { if (!r.exc) { @@ -381,6 +390,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s me.frm.doc.total_value = me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total; me.frm.doc.total_value = flt(me.frm.doc.total_value, precision('total_value')); + me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision('target_qty')); + me.frm.doc.target_incoming_rate = me.frm.doc.target_qty ? me.frm.doc.total_value / flt(me.frm.doc.target_qty) + : me.frm.doc.total_value; + me.frm.refresh_fields(); } }; diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index b697c206bf2..0582b1ebc1e 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -41,7 +41,13 @@ "service_items", "service_items_total", "totals_section", - "total_value" + "total_value", + "column_break_36", + "target_incoming_rate", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "target_fixed_asset_account" ], "fields": [ { @@ -289,12 +295,45 @@ "label": "Total Value", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fieldname": "target_incoming_rate", + "fieldtype": "Currency", + "label": "Target Incoming Rate", + "options": "Company:company:default_currency" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "target_fixed_asset_account", + "fieldtype": "Link", + "label": "Target Fixed Asset Account", + "options": "Account", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-09-06 17:18:31.881006", + "modified": "2021-09-08 15:58:40.417579", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 586710a635c..64f13887c2b 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -11,13 +11,14 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.stock.utils import get_incoming_rate from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import get_current_asset_value from six import string_types import json force_fields = ['target_item_name', 'target_asset_name', 'item_name', 'asset_name', 'target_is_fixed_asset', 'target_has_serial_no', 'target_has_batch_no', - 'target_stock_uom', 'stock_uom'] + 'target_stock_uom', 'stock_uom', 'target_fixed_asset_account', 'fixed_asset_account'] class AssetCapitalization(AccountsController): @@ -42,7 +43,7 @@ class AssetCapitalization(AccountsController): self.title = self.target_asset_name or self.target_item_name or self.target_item_code def set_missing_values(self, for_validate=False): - target_item_details = get_target_item_details(self.target_item_code) + target_item_details = get_target_item_details(self.target_item_code, self.company) for k, v in target_item_details.items(): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) @@ -51,7 +52,7 @@ class AssetCapitalization(AccountsController): if not self.target_is_fixed_asset: self.target_asset = None - target_asset_details = get_target_asset_details(self.target_asset) + target_asset_details = get_target_asset_details(self.target_asset, self.company) for k, v in target_asset_details.items(): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) @@ -95,6 +96,8 @@ class AssetCapitalization(AccountsController): if target_item.is_fixed_asset: self.target_qty = 1 + if flt(self.target_qty) <= 0: + frappe.throw(_("Target Qty must be a positive number")) if not target_item.is_stock_item: self.target_warehouse = None @@ -233,9 +236,12 @@ class AssetCapitalization(AccountsController): self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total self.total_value = flt(self.total_value, self.precision('total_value')) + self.target_qty = flt(self.target_qty, self.precision('target_qty')) + self.target_incoming_rate = self.total_value / self.target_qty + @frappe.whitelist() -def get_target_item_details(item_code=None): +def get_target_item_details(item_code=None, company=None): out = frappe._dict() # Get Item Details @@ -261,6 +267,13 @@ def get_target_item_details(item_code=None): if not out.target_has_serial_no: out.target_serial_no = "" + # Cost Center + item_defaults = get_item_defaults(item.name, company) + item_group_defaults = get_item_group_defaults(item.name, company) + brand_defaults = get_brand_defaults(item.name, company) + out.cost_center = get_default_cost_center(frappe._dict({'item_code': item.name, 'company': company}), + item_defaults, item_group_defaults, brand_defaults) + # Set Entry Type if not item_code: out.entry_type = "" @@ -273,7 +286,7 @@ def get_target_item_details(item_code=None): @frappe.whitelist() -def get_target_asset_details(asset=None): +def get_target_asset_details(asset=None, company=None): out = frappe._dict() # Get Asset Details @@ -289,6 +302,12 @@ def get_target_asset_details(asset=None): # Set Asset Details out.asset_name = asset_details.asset_name + if asset_details.item_code: + out.target_fixed_asset_account = get_asset_category_account('fixed_asset_account', item=asset_details.item_code, + company=company) + else: + out.target_fixed_asset_account = None + return out @@ -313,6 +332,12 @@ def get_consumed_stock_item_details(args, get_valuation_rate=True): out.warehouse = get_item_warehouse(item, args, overwrite_warehouse=True) if item else None + # Cost Center + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + if get_valuation_rate: if args.item_code and out.warehouse: incoming_rate_args = frappe._dict({ @@ -373,6 +398,21 @@ def get_consumed_asset_details(args, get_asset_value=True): else: out.asset_value = 0 + # Account + if asset_details.item_code: + out.fixed_asset_account = get_asset_category_account('fixed_asset_account', item=asset_details.item_code, + company=args.company) + else: + out.fixed_asset_account = None + + # Cost Center + if asset_details.item_code: + item = frappe.get_cached_doc("Item", asset_details.item_code) + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + return out diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json index a0040338c0b..93ec336b159 100644 --- a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json @@ -12,7 +12,11 @@ "item_name", "section_break_6", "asset_value", - "column_break_9" + "column_break_9", + "accounting_dimensions_section", + "fixed_asset_account", + "dimension_col_break", + "cost_center" ], "fields": [ { @@ -68,12 +72,35 @@ { "fieldname": "column_break_9", "fieldtype": "Column Break" + }, + { + "fieldname": "fixed_asset_account", + "fieldtype": "Link", + "label": "Fixed Asset Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-06 13:46:04.892863", + "modified": "2021-09-08 15:54:24.885547", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Asset Item", diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json index 2d3584dce47..0ae1c1428ee 100644 --- a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json @@ -17,8 +17,7 @@ "amount", "accounting_dimensions_section", "cost_center", - "dimension_col_break", - "project" + "dimension_col_break" ], "fields": [ { @@ -106,18 +105,12 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" - }, - { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-06 14:06:34.768152", + "modified": "2021-09-08 15:52:08.598100", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Service Item", diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json index 19c455894a5..14eb0f6ef20 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -19,7 +19,10 @@ "batch_and_serial_no_section", "batch_no", "column_break_13", - "serial_no" + "serial_no", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" ], "fields": [ { @@ -120,12 +123,28 @@ "label": "Actual Qty in Warehouse", "no_copy": 1, "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-06 13:46:13.579140", + "modified": "2021-09-08 15:56:20.230548", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Stock Item", From 3b9bc8e4effcd46d53c30169ea24df2ae849ad5c Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Sun, 12 Sep 2021 14:28:14 +0500 Subject: [PATCH 03/54] feat(Asset Capitalization): Finance Book field in Asset Row --- .../asset_capitalization/asset_capitalization.js | 11 ++++++++--- .../asset_capitalization/asset_capitalization.py | 3 ++- .../asset_capitalization_asset_item.json | 13 ++++++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index b42634a5091..b0f7712d6ed 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -136,8 +136,13 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } } - finance_book() { - this.get_all_asset_values(); + finance_book(doc, cdt, cdn) { + if (cdt === "Asset Capitalization Asset Item") { + var row = frappe.get_doc(cdt, cdn); + this.get_consumed_asset_details(row); + } else { + this.get_all_asset_values(); + } } stock_qty() { @@ -268,7 +273,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s doctype: me.frm.doc.doctype, name: me.frm.doc.name, company: me.frm.doc.company, - finance_book: me.frm.doc.finance_book, + finance_book: row.finance_book || me.frm.doc.finance_book, posting_date: me.frm.doc.posting_date, posting_time: me.frm.doc.posting_time, } diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 64f13887c2b..b29decb2d96 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -72,6 +72,7 @@ class AssetCapitalization(AccountsController): args.update(d.as_dict()) args.doctype = self.doctype args.name = self.name + args.finance_book = d.get('finance_book') or self.get('finance_book') consumed_asset_details = get_consumed_asset_details(args, get_asset_value=False) for k, v in consumed_asset_details.items(): if d.meta.has_field(k) and (not d.get(k) or k in force_fields): @@ -195,7 +196,7 @@ class AssetCapitalization(AccountsController): def set_asset_values(self): for d in self.asset_items: if d.asset: - d.asset_value = flt(get_current_asset_value(d.asset, self.finance_book)) + d.asset_value = flt(get_current_asset_value(d.asset, d.get('finance_book') or self.finance_book)) def get_args_for_incoming_rate(self, item): return frappe._dict({ diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json index 93ec336b159..a5f820299bc 100644 --- a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json @@ -7,6 +7,7 @@ "field_order": [ "asset", "asset_name", + "finance_book", "column_break_3", "item_code", "item_name", @@ -14,9 +15,9 @@ "asset_value", "column_break_9", "accounting_dimensions_section", - "fixed_asset_account", + "cost_center", "dimension_col_break", - "cost_center" + "fixed_asset_account" ], "fields": [ { @@ -95,12 +96,18 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-08 15:54:24.885547", + "modified": "2021-09-08 23:42:25.143272", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Asset Item", From 7a5d75b68d278cf100c9b73ab4ae0475b820eb67 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Mon, 13 Sep 2021 23:01:52 +0500 Subject: [PATCH 04/54] feat(Asset Capitalization): Submission and Cancellation --- .../doctype/sales_invoice/sales_invoice.py | 74 +----- erpnext/assets/doctype/asset/asset_list.js | 3 + erpnext/assets/doctype/asset/depreciation.py | 29 ++- .../asset_capitalization.js | 9 +- .../asset_capitalization.py | 217 +++++++++++++++++- .../asset_capitalization_asset_item.json | 11 +- erpnext/controllers/accounts_controller.py | 81 +++++++ 7 files changed, 340 insertions(+), 84 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ec249c24194..3af7b24b07b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -37,7 +37,6 @@ from erpnext.assets.doctype.asset.depreciation import ( get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, - post_depreciation_entries, ) from erpnext.controllers.selling_controller import SellingController from erpnext.healthcare.utils import manage_invoice_submit_cancel @@ -166,7 +165,7 @@ class SalesInvoice(SellingController): if self.update_stock: frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return): + elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or (asset.status == "Sold" and not self.is_return): frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status)) def validate_item_cost_centers(self): @@ -1007,77 +1006,6 @@ class SalesInvoice(SellingController): self.check_finance_books(item, asset) return asset - def check_finance_books(self, item, asset): - if (len(asset.finance_books) > 1 and not item.finance_book - and asset.finance_books[0].finance_book): - frappe.throw(_("Select finance book for the item {0} at row {1}") - .format(item.item_code, item.idx)) - - def depreciate_asset(self, asset): - asset.flags.ignore_validate_update_after_submit = True - asset.prepare_depreciation_data(self.posting_date) - asset.save() - - post_depreciation_entries(self.posting_date) - - def reset_depreciation_schedule(self, asset): - asset.flags.ignore_validate_update_after_submit = True - - # recreate original depreciation schedule of the asset - asset.prepare_depreciation_data() - - self.modify_depreciation_schedule_for_asset_repairs(asset) - asset.save() - - self.delete_depreciation_entry_made_after_sale(asset) - - def modify_depreciation_schedule_for_asset_repairs(self, asset): - asset_repairs = frappe.get_all( - 'Asset Repair', - filters = {'asset': asset.name}, - fields = ['name', 'increase_in_asset_life'] - ) - - for repair in asset_repairs: - if repair.increase_in_asset_life: - asset_repair = frappe.get_doc('Asset Repair', repair.name) - asset_repair.modify_depreciation_schedule() - asset.prepare_depreciation_data() - - def delete_depreciation_entry_made_after_sale(self, asset): - from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry - - posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() - - row = -1 - finance_book = asset.get('schedules')[0].get('finance_book') - for schedule in asset.get('schedules'): - if schedule.finance_book != finance_book: - row = 0 - finance_book = schedule.finance_book - else: - row += 1 - - if schedule.schedule_date == posting_date_of_original_invoice: - if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice): - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) - reverse_journal_entry.posting_date = nowdate() - reverse_journal_entry.submit() - - def get_posting_date_of_sales_invoice(self): - return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') - - # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice): - for finance_book in asset.get('finance_books'): - if schedule.finance_book == finance_book.finance_book: - orginal_schedule_date = add_months(finance_book.depreciation_start_date, - row * cint(finance_book.frequency_of_depreciation)) - - if orginal_schedule_date == posting_date_of_original_invoice: - return True - return False - @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 4302cb2c518..3d00eb74aa0 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -10,6 +10,9 @@ frappe.listview_settings['Asset'] = { } else if (doc.status === "Sold") { return [__("Sold"), "green", "status,=,Sold"]; + } else if (["Capitalized", "Decapitalized"].includes(doc.status)) { + return [__(doc.status), "grey", "status,=," + doc.status]; + } else if (doc.status === "Scrapped") { return [__("Scrapped"), "grey", "status,=,Scrapped"]; diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 609791012a2..58d4bb5ebbf 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -13,7 +13,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( ) -def post_depreciation_entries(date=None): +def post_depreciation_entries(date=None, commit=True): # Return if automatic booking of asset depreciation is disabled if not cint(frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically")): return @@ -22,7 +22,8 @@ def post_depreciation_entries(date=None): date = today() for asset in get_depreciable_assets(date): make_depreciation_entry(asset, date) - frappe.db.commit() + if commit: + frappe.db.commit() def get_depreciable_assets(date): return frappe.db.sql_list("""select a.name @@ -140,7 +141,7 @@ def scrap_asset(asset_name): if asset.docstatus != 1: frappe.throw(_("Asset {0} must be submitted").format(asset.name)) - elif asset.status in ("Cancelled", "Sold", "Scrapped"): + elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"): frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)) depreciation_series = frappe.get_cached_value('Company', asset.company, "series_for_depreciation_entry") @@ -269,3 +270,25 @@ def get_disposal_account_and_cost_center(company): frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company)) return disposal_account, depreciation_cost_center + + +@frappe.whitelist() +def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None): + asset_doc = frappe.get_doc("Asset", asset) + + if asset_doc.calculate_depreciation: + asset_doc.prepare_depreciation_data(getdate(disposal_date)) + + finance_book_id = 1 + if finance_book: + for fb in asset_doc.finance_books: + if fb.finance_book == finance_book: + finance_book_id = fb.idx + break + + asset_schedules = [sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id] + accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount + + return flt(flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount, asset_doc.precision('gross_purchase_amount')) + else: + return flt(asset_doc.value_after_depreciation) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index b0f7712d6ed..4f8c95e9a06 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -15,6 +15,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s refresh() { erpnext.hide_company(); + this.show_general_ledger(); + if (this.frm.doc.stock_items || !this.frm.doc.target_is_fixed_asset) { + this.show_stock_ledger(); + } } setup_queries() { @@ -33,7 +37,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s filters['item_code'] = me.frm.doc.target_item_code; } - filters['status'] = ["not in", ["Draft", "Scrapped", "Sold"]] + filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]] filters['docstatus'] = 1; return { @@ -43,7 +47,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s me.frm.set_query("asset", "asset_items", function() { var filters = { - 'status': ["not in", ["Draft", "Scrapped", "Sold"]], + 'status': ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]], 'docstatus': 1 } @@ -127,6 +131,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s posting_date() { if (this.frm.doc.posting_date) { this.get_all_item_warehouse_details(); + this.get_all_asset_values(); } } diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index b29decb2d96..e50ddfaba86 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -2,8 +2,9 @@ # For license information, please see license.txt import frappe +# import erpnext from frappe import _ -from erpnext.controllers.accounts_controller import AccountsController +from erpnext.controllers.stock_controller import StockController from frappe.utils import cint, flt from erpnext.stock.get_item_details import get_item_warehouse, get_default_expense_account, get_default_cost_center from erpnext.stock.doctype.item.item import get_item_defaults @@ -13,6 +14,9 @@ from erpnext.stock.utils import get_incoming_rate from erpnext.stock.stock_ledger import get_previous_sle from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import get_current_asset_value +from erpnext.stock import get_warehouse_account_map +from erpnext.assets.doctype.asset.depreciation import get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain,\ + get_value_after_depreciation_on_disposal_date from six import string_types import json @@ -21,7 +25,7 @@ force_fields = ['target_item_name', 'target_asset_name', 'item_name', 'asset_nam 'target_stock_uom', 'stock_uom', 'target_fixed_asset_account', 'fixed_asset_account'] -class AssetCapitalization(AccountsController): +class AssetCapitalization(StockController): def validate(self): self.validate_posting_time() self.set_missing_values(for_validate=True) @@ -36,6 +40,18 @@ class AssetCapitalization(AccountsController): self.calculate_totals() self.set_title() + def before_submit(self): + self.validate_source_mandatory() + + def on_submit(self): + self.update_stock_ledger() + self.make_gl_entries() + + def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.update_stock_ledger() + self.make_gl_entries() + def set_entry_type(self): self.entry_type = "Capitalization" if self.target_is_fixed_asset else "Decapitalization" @@ -104,11 +120,15 @@ class AssetCapitalization(AccountsController): self.target_warehouse = None if not target_item.is_fixed_asset: self.target_asset = None + self.target_fixed_asset_account = None if not target_item.has_batch_no: self.target_batch_no = None if not target_item.has_serial_no: self.target_serial_no = "" + if target_item.is_stock_item and not self.target_warehouse: + frappe.throw(_("Target Warehouse is mandatory for Decapitalization")) + self.validate_item(target_item) def validate_target_asset(self): @@ -165,6 +185,13 @@ class AssetCapitalization(AccountsController): if not d.cost_center: d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") + def validate_source_mandatory(self): + if not self.target_is_fixed_asset and not self.get('asset_items'): + frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization")) + + if not self.get('stock_items') and not self.get('asset_items'): + frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization")) + def validate_item(self, item): from erpnext.stock.doctype.item.item import validate_end_of_life validate_end_of_life(item.name, item.end_of_life, item.disabled) @@ -173,7 +200,7 @@ class AssetCapitalization(AccountsController): return frappe.db.get_value("Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1) def validate_asset(self, asset): - if asset.status in ("Draft", "Scrapped", "Sold"): + if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status)) if asset.docstatus == 0: @@ -196,7 +223,10 @@ class AssetCapitalization(AccountsController): def set_asset_values(self): for d in self.asset_items: if d.asset: - d.asset_value = flt(get_current_asset_value(d.asset, d.get('finance_book') or self.finance_book)) + finance_book = d.get('finance_book') or self.get('finance_book') + d.current_asset_value = flt(get_current_asset_value(d.asset, finance_book=finance_book)) + d.asset_value = get_value_after_depreciation_on_disposal_date(d.asset, self.posting_date, + finance_book=finance_book) def get_args_for_incoming_rate(self, item): return frappe._dict({ @@ -240,6 +270,180 @@ class AssetCapitalization(AccountsController): self.target_qty = flt(self.target_qty, self.precision('target_qty')) self.target_incoming_rate = self.total_value / self.target_qty + def update_stock_ledger(self): + sl_entries = [] + + for d in self.stock_items: + sle = self.get_sl_entries(d, { + "actual_qty": -flt(d.stock_qty), + }) + sl_entries.append(sle) + + if not frappe.db.get_value("Item", self.target_item_code, "is_fixed_asset", cache=1): + sle = self.get_sl_entries(self, { + "item_code": self.target_item_code, + "warehouse": self.target_warehouse, + "batch_no": self.target_batch_no, + "serial_no": self.target_serial_no, + "actual_qty": flt(self.target_qty), + "incoming_rate": flt(self.target_incoming_rate) + }) + sl_entries.append(sle) + + # reverse sl entries if cancel + if self.docstatus == 2: + sl_entries.reverse() + + if sl_entries: + self.make_sl_entries(sl_entries) + + def make_gl_entries(self, gl_entries=None, from_repost=False): + from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries + + if not gl_entries: + gl_entries = self.get_gl_entries() + + if self.docstatus == 1: + if gl_entries: + make_gl_entries(gl_entries, from_repost=from_repost) + elif self.docstatus == 2: + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): + # Stock GL Entries + gl_entries = [] + + if not warehouse_account: + warehouse_account = get_warehouse_account_map(self.company) + + precision = self.get_debit_field_precision() + sle_map = self.get_stock_ledger_details() + + if self.target_is_fixed_asset: + target_account = self.target_fixed_asset_account + else: + target_account = warehouse_account[self.target_warehouse]["account"] + + target_against = set() + + # Consumed Stock Items + total_consumed_stock_value = 0 + for item_row in self.stock_items: + sle_list = sle_map.get(item_row.name) + if sle_list: + for sle in sle_list: + stock_value_difference = flt(sle.stock_value_difference, precision) + total_consumed_stock_value += -1 * sle.stock_value_difference + + account = warehouse_account[sle.warehouse]["account"] + target_against.add(account) + + gl_entries.append(self.get_gl_dict({ + "account": account, + "against": target_account, + "cost_center": item_row.cost_center, + "project": item_row.get('project') or self.get('project'), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "credit": -1 * stock_value_difference, + }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) + + # Consumed Assets + for item in self.asset_items: + asset = self.get_asset(item) + + if self.docstatus == 2: + fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset, + item.asset_value, item.get('finance_book') or self.get('finance_book')) + asset.db_set("disposal_date", None) + + self.set_consumed_asset_status(asset) + + if asset.calculate_depreciation: + self.reset_depreciation_schedule(asset) + else: + if asset.calculate_depreciation: + self.depreciate_asset(asset) + + asset.reload() + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, + item.asset_value, item.get('finance_book') or self.get('finance_book')) + asset.db_set("disposal_date", self.posting_date) + + self.set_consumed_asset_status(asset) + + for gle in fixed_asset_gl_entries: + gle["against"] = target_account + gl_entries.append(self.get_gl_dict(gle, item=item)) + + # Service Expenses + total_service_expenses = 0 + for item_row in self.service_items: + expense_amount = flt(item_row.amount, precision) + total_service_expenses += expense_amount + target_against.add(item_row.expense_account) + + gl_entries.append(self.get_gl_dict({ + "account": item_row.expense_account, + "against": target_account, + "cost_center": item_row.cost_center, + "project": item_row.get('project') or self.get('project'), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "credit": expense_amount, + }, item=item_row)) + + target_against = ", ".join(target_against) + total_target_stock_value = 0 + total_target_asset_value = 0 + + if self.target_is_fixed_asset: + # Target Asset Item + total_target_asset_value = flt(self.total_value, precision) + gl_entries.append(self.get_gl_dict({ + "account": self.target_fixed_asset_account, + "against": target_against, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": total_target_asset_value, + "cost_center": self.get('cost_center') + }, item=self)) + + if self.docstatus == 1: + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.purchase_date = self.posting_date + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.prepare_depreciation_data() + asset_doc.flags.ignore_validate_update_after_submit = True + asset_doc.save() + else: + # Target Stock Item + sle_list = sle_map.get(self.name) + for sle in sle_list: + stock_value_difference = flt(sle.stock_value_difference, precision) + total_target_stock_value += sle.stock_value_difference + account = warehouse_account[sle.warehouse]["account"] + + gl_entries.append(self.get_gl_dict({ + "account": account, + "against": target_against, + "cost_center": self.cost_center, + "project": self.get('project'), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "debit": stock_value_difference, + }, warehouse_account[sle.warehouse]["account_currency"], item=self)) + + return gl_entries + + def get_asset(self, item): + asset = frappe.get_doc("Asset", item.asset) + self.check_finance_books(item, asset) + return asset + + def set_consumed_asset_status(self, asset): + if self.docstatus == 1: + asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized") + else: + asset.set_status() + @frappe.whitelist() def get_target_item_details(item_code=None, company=None): @@ -395,8 +599,11 @@ def get_consumed_asset_details(args, get_asset_value=True): if get_asset_value: if args.asset: - out.asset_value = flt(get_current_asset_value(args.asset, finance_book=args.finance_book)) + out.current_asset_value = flt(get_current_asset_value(args.asset, finance_book=args.finance_book)) + out.asset_value = get_value_after_depreciation_on_disposal_date(args.asset, args.posting_date, + finance_book=args.finance_book) else: + out.current_asset_value = 0 out.asset_value = 0 # Account diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json index a5f820299bc..ebaaffbad15 100644 --- a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json @@ -12,6 +12,7 @@ "item_code", "item_name", "section_break_6", + "current_asset_value", "asset_value", "column_break_9", "accounting_dimensions_section", @@ -102,12 +103,20 @@ "fieldtype": "Link", "label": "Finance Book", "options": "Finance Book" + }, + { + "fieldname": "current_asset_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Current Asset Value", + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-08 23:42:25.143272", + "modified": "2021-09-12 14:30:02.915132", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization Asset Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b90db054b57..930dca82458 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -54,6 +54,7 @@ from erpnext.stock.get_item_details import ( get_item_tax_map, get_item_warehouse, ) +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.utilities.transaction_base import TransactionBase @@ -1457,6 +1458,86 @@ class AccountsController(TransactionBase): jv.save() jv.submit() + def check_finance_books(self, item, asset): + if (len(asset.finance_books) > 1 and not item.get('finance_book') and not self.get('finance_book') + and asset.finance_books[0].finance_book): + frappe.throw(_("Select finance book for the item {0} at row {1}") + .format(item.item_code, item.idx)) + + def depreciate_asset(self, asset): + asset.flags.ignore_validate_update_after_submit = True + asset.prepare_depreciation_data(self.posting_date) + asset.save() + + post_depreciation_entries(self.posting_date, commit=False) + + def reset_depreciation_schedule(self, asset): + asset.flags.ignore_validate_update_after_submit = True + + # recreate original depreciation schedule of the asset + asset.prepare_depreciation_data() + + self.modify_depreciation_schedule_for_asset_repairs(asset) + asset.save() + + self.delete_depreciation_entry_made_after_disposal(asset) + + def modify_depreciation_schedule_for_asset_repairs(self, asset): + asset_repairs = frappe.get_all( + 'Asset Repair', + filters={'asset': asset.name}, + fields=['name', 'increase_in_asset_life'] + ) + + for repair in asset_repairs: + if repair.increase_in_asset_life: + asset_repair = frappe.get_doc('Asset Repair', repair.name) + asset_repair.modify_depreciation_schedule() + asset.prepare_depreciation_data() + + def delete_depreciation_entry_made_after_disposal(self, asset): + from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry + + posting_date_of_original_invoice = self.get_posting_date_of_disposal_entry() + + row = -1 + finance_book = asset.get('schedules')[0].get('finance_book') + for schedule in asset.get('schedules'): + if schedule.finance_book != finance_book: + row = 0 + finance_book = schedule.finance_book + else: + row += 1 + + if schedule.schedule_date == posting_date_of_original_invoice: + if not self.disposal_was_made_on_original_schedule_date(asset, schedule, row, + posting_date_of_original_invoice): + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) + reverse_journal_entry.posting_date = nowdate() + + for d in reverse_journal_entry.accounts: + d.reference_type = "Asset" + d.reference_name = asset.name + + reverse_journal_entry.submit() + + def get_posting_date_of_disposal_entry(self): + if self.doctype == "Sales Invoice" and self.return_against: + return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') + else: + return self.posting_date + + # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone + def disposal_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_disposal): + for finance_book in asset.get('finance_books'): + if schedule.finance_book == finance_book.finance_book: + orginal_schedule_date = add_months(finance_book.depreciation_start_date, + row * cint(finance_book.frequency_of_depreciation)) + + if orginal_schedule_date == posting_date_of_original_disposal: + return True + return False + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) From 8c54be7e999e2c5e7a61ae69a2bae8e624a939b0 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 14 Sep 2021 12:30:40 +0500 Subject: [PATCH 05/54] chore(Asset Capitalization): linting --- .../asset_capitalization.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 4f8c95e9a06..892f8c7d4ac 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -37,27 +37,27 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s filters['item_code'] = me.frm.doc.target_item_code; } - filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]] + filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]]; filters['docstatus'] = 1; return { filters: filters - } + }; }); me.frm.set_query("asset", "asset_items", function() { var filters = { 'status': ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]], 'docstatus': 1 - } + }; if (me.frm.doc.target_asset) { - filters['name'] = ['!=', me.frm.doc.target_asset] + filters['name'] = ['!=', me.frm.doc.target_asset]; } return { filters: filters - } + }; }); me.frm.set_query("item_code", "stock_items", function() { @@ -70,19 +70,19 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s me.frm.set_query('batch_no', 'stock_items', function(doc, cdt, cdn) { var item = locals[cdt][cdn]; - if(!item.item_code) { + if (!item.item_code) { frappe.throw(__("Please enter Item Code to get Batch Number")); } else { var filters = { 'item_code': item.item_code, 'posting_date': me.frm.doc.posting_date || frappe.datetime.nowdate(), 'warehouse': item.warehouse - } + }; return { - query : "erpnext.controllers.queries.get_batch_no", + query: "erpnext.controllers.queries.get_batch_no", filters: filters - } + }; } }); @@ -318,7 +318,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s get_warehouse_details(item) { var me = this; - if(item.item_code && item.warehouse) { + if (item.item_code && item.warehouse) { me.frm.call({ method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details", child: item, From dc24a657fd2c71f03c0f4f6d5a0c1460152725ca Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 14 Sep 2021 12:40:17 +0500 Subject: [PATCH 06/54] chore(Asset Capitalization): linting --- .../doctype/sales_invoice/sales_invoice.py | 1 - .../asset_capitalization/asset_capitalization.py | 16 +++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3af7b24b07b..b2188e1e5ea 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -10,7 +10,6 @@ from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values from frappe.utils import ( add_days, - add_months, cint, cstr, flt, diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index e50ddfaba86..856ace2ee5b 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -5,8 +5,15 @@ import frappe # import erpnext from frappe import _ from erpnext.controllers.stock_controller import StockController -from frappe.utils import cint, flt -from erpnext.stock.get_item_details import get_item_warehouse, get_default_expense_account, get_default_cost_center +from frappe.utils import ( + cint, + flt +) +from erpnext.stock.get_item_details import ( + get_item_warehouse, + get_default_expense_account, + get_default_cost_center +) from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.brand.brand import get_brand_defaults @@ -15,8 +22,11 @@ from erpnext.stock.stock_ledger import get_previous_sle from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import get_current_asset_value from erpnext.stock import get_warehouse_account_map -from erpnext.assets.doctype.asset.depreciation import get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain,\ +from erpnext.assets.doctype.asset.depreciation import ( + get_gl_entries_on_asset_disposal, + get_gl_entries_on_asset_regain, get_value_after_depreciation_on_disposal_date +) from six import string_types import json From 8873ef7b675965f920d0534caa981091910d9d4b Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 14 Sep 2021 15:05:39 +0500 Subject: [PATCH 07/54] chore(Asset Capitalization): isort linting --- .../asset_capitalization.py | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 856ace2ee5b..129b1aa4e1a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -1,34 +1,36 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import json + import frappe + # import erpnext from frappe import _ -from erpnext.controllers.stock_controller import StockController -from frappe.utils import ( - cint, - flt -) -from erpnext.stock.get_item_details import ( - get_item_warehouse, - get_default_expense_account, - get_default_cost_center -) -from erpnext.stock.doctype.item.item import get_item_defaults -from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults -from erpnext.setup.doctype.brand.brand import get_brand_defaults -from erpnext.stock.utils import get_incoming_rate -from erpnext.stock.stock_ledger import get_previous_sle -from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account -from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import get_current_asset_value -from erpnext.stock import get_warehouse_account_map +from frappe.utils import cint, flt +from six import string_types + from erpnext.assets.doctype.asset.depreciation import ( get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, - get_value_after_depreciation_on_disposal_date + get_value_after_depreciation_on_disposal_date, ) -from six import string_types -import json +from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account +from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( + get_current_asset_value, +) +from erpnext.controllers.stock_controller import StockController +from erpnext.setup.doctype.brand.brand import get_brand_defaults +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.stock import get_warehouse_account_map +from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.get_item_details import ( + get_default_cost_center, + get_default_expense_account, + get_item_warehouse, +) +from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.utils import get_incoming_rate force_fields = ['target_item_name', 'target_asset_name', 'item_name', 'asset_name', 'target_is_fixed_asset', 'target_has_serial_no', 'target_has_batch_no', From 9ae0380a96ebe7521e28262c5d9e10914d45de18 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 14 Sep 2021 15:09:58 +0500 Subject: [PATCH 08/54] chore(Asset Capitalization): isort linting --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 +---------- erpnext/controllers/accounts_controller.py | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b2188e1e5ea..14f4787cda0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -8,16 +8,7 @@ from frappe import _, msgprint, throw from frappe.contacts.doctype.address.address import get_address_display from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values -from frappe.utils import ( - add_days, - cint, - cstr, - flt, - formatdate, - get_link_to_form, - getdate, - nowdate, -) +from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate from six import iteritems import erpnext diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 930dca82458..31af7f37449 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -38,6 +38,7 @@ from erpnext.accounts.party import ( validate_party_frozen_disabled, ) from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.print_settings import ( set_print_templates_for_item_table, @@ -54,7 +55,6 @@ from erpnext.stock.get_item_details import ( get_item_tax_map, get_item_warehouse, ) -from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.utilities.transaction_base import TransactionBase From d173e06e69feb60f59e7c43b7daa6752a3236db8 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 14 Sep 2021 15:13:35 +0500 Subject: [PATCH 09/54] chore(Asset Capitalization): isort linting --- .../doctype/asset_capitalization/test_asset_capitalization.py | 1 + .../asset_capitalization_asset_item.py | 1 + .../asset_capitalization_service_item.py | 1 + .../asset_capitalization_stock_item.py | 1 + 4 files changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index d8e22c51011..da128467fe7 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestAssetCapitalization(unittest.TestCase): pass diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py index 8817317e70c..ba356d6b9f0 100644 --- a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class AssetCapitalizationAssetItem(Document): pass diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py index fa158295ae7..28d018ee39a 100644 --- a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class AssetCapitalizationServiceItem(Document): pass diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py index 4449538d8e2..5d6f98d5cf0 100644 --- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class AssetCapitalizationStockItem(Document): pass From 132b517584352a8a6c895fd3256bdc08b03c1df5 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Thu, 16 Sep 2021 23:20:36 +0500 Subject: [PATCH 10/54] fix(Asset Captalization): run_serially on posting_date changed --- .../asset_capitalization/asset_capitalization.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 892f8c7d4ac..d135e60ae7f 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -16,7 +16,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s refresh() { erpnext.hide_company(); this.show_general_ledger(); - if (this.frm.doc.stock_items || !this.frm.doc.target_is_fixed_asset) { + if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { this.show_stock_ledger(); } } @@ -130,8 +130,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s posting_date() { if (this.frm.doc.posting_date) { - this.get_all_item_warehouse_details(); - this.get_all_asset_values(); + frappe.run_serially([ + () => this.get_all_item_warehouse_details(), + () => this.get_all_asset_values() + ]); } } @@ -347,7 +349,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s get_all_item_warehouse_details() { var me = this; - me.frm.call({ + return me.frm.call({ method: "set_warehouse_details", doc: me.frm.doc, callback: function(r) { @@ -360,7 +362,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s get_all_asset_values() { var me = this; - me.frm.call({ + return me.frm.call({ method: "set_asset_values", doc: me.frm.doc, callback: function(r) { From 003cfe27172f1f1eecf1e663b7c0559c1a009be6 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Thu, 16 Sep 2021 23:21:09 +0500 Subject: [PATCH 11/54] fix(Asset Capitalization): Hide source items section if table is empty --- .../asset_capitalization/asset_capitalization.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index 0582b1ebc1e..d7e6b547162 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -158,6 +158,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)", "fieldname": "section_break_16", "fieldtype": "Section Break", "label": "Consumed Stock Items" @@ -227,6 +228,7 @@ "label": "Target Serial No" }, { + "depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)", "fieldname": "section_break_26", "fieldtype": "Section Break", "label": "Consumed Asset Items" @@ -267,6 +269,7 @@ "options": "Finance Book" }, { + "depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)", "fieldname": "service_expenses_section", "fieldtype": "Section Break", "label": "Service Expenses" @@ -304,7 +307,8 @@ "fieldname": "target_incoming_rate", "fieldtype": "Currency", "label": "Target Incoming Rate", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 }, { "collapsible": 1, @@ -333,7 +337,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-09-08 15:58:40.417579", + "modified": "2021-09-15 15:41:27.917458", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", From e832944dfede4e979691ec7a4dd22c2fd89de4f7 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Thu, 16 Sep 2021 23:22:31 +0500 Subject: [PATCH 12/54] fix(Asset): On Depreciation reversal, remove Journal Entry reference --- erpnext/controllers/accounts_controller.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 31af7f37449..ba0c7a8a8be 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1475,13 +1475,12 @@ class AccountsController(TransactionBase): asset.flags.ignore_validate_update_after_submit = True # recreate original depreciation schedule of the asset + self.delete_depreciation_entry_made_after_disposal(asset) asset.prepare_depreciation_data() self.modify_depreciation_schedule_for_asset_repairs(asset) asset.save() - self.delete_depreciation_entry_made_after_disposal(asset) - def modify_depreciation_schedule_for_asset_repairs(self, asset): asset_repairs = frappe.get_all( 'Asset Repair', @@ -1511,7 +1510,7 @@ class AccountsController(TransactionBase): if schedule.schedule_date == posting_date_of_original_invoice: if not self.disposal_was_made_on_original_schedule_date(asset, schedule, row, - posting_date_of_original_invoice): + posting_date_of_original_invoice) or getdate(schedule.schedule_date) > getdate(today()): reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() @@ -1520,6 +1519,8 @@ class AccountsController(TransactionBase): d.reference_name = asset.name reverse_journal_entry.submit() + schedule.db_set('journal_entry', None) + def get_posting_date_of_disposal_entry(self): if self.doctype == "Sales Invoice" and self.return_against: From c311b8ea4f5c51c18462f671d26ede1c18f7f5b6 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Thu, 16 Sep 2021 23:23:22 +0500 Subject: [PATCH 13/54] fix(Asset Capitalization): validation edge cases --- .../doctype/asset_capitalization/asset_capitalization.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 129b1aa4e1a..5a2398650b0 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -189,7 +189,7 @@ class AssetCapitalization(StockController): if flt(d.qty) <= 0: frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx)) - if flt(d.amount) <= 0: + if flt(d.rate) <= 0: frappe.throw(_("Row #{0}: Amount must be a positive number").format(d.idx)) self.validate_item(item) @@ -221,11 +221,11 @@ class AssetCapitalization(StockController): frappe.throw(_("Asset {0} is cancelled").format(asset.name)) if asset.company != self.company: - frappe.throw(_("Asset {0} does not belong to company {1}").format(self.target_asset, self.company)) + frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company)) @frappe.whitelist() def set_warehouse_details(self): - for d in self.stock_items: + for d in self.get('stock_items'): if d.item_code and d.warehouse: args = self.get_args_for_incoming_rate(d) warehouse_details = get_warehouse_details(args) @@ -233,7 +233,7 @@ class AssetCapitalization(StockController): @frappe.whitelist() def set_asset_values(self): - for d in self.asset_items: + for d in self.get('asset_items'): if d.asset: finance_book = d.get('finance_book') or self.get('finance_book') d.current_asset_value = flt(get_current_asset_value(d.asset, finance_book=finance_book)) From 86a6293e6226fbadbdb28ce681d2b39a77e3acc2 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Thu, 16 Sep 2021 23:24:46 +0500 Subject: [PATCH 14/54] test(Asset Capitalization): unit tests --- erpnext/assets/doctype/asset/test_asset.py | 6 +- .../test_asset_capitalization.py | 329 +++++++++++++++++- 2 files changed, 330 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 4cc9be5b05d..2bb5a099080 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -702,9 +702,9 @@ def create_asset(**args): "company": args.company or"_Test Company", "purchase_date": "2015-01-01", "calculate_depreciation": args.calculate_depreciation or 0, - "gross_purchase_amount": 100000, - "purchase_receipt_amount": 100000, - "expected_value_after_useful_life": 10000, + "gross_purchase_amount": args.asset_value or 100000, + "purchase_receipt_amount": args.asset_value or 100000, + "expected_value_after_useful_life": args.asset_value or 10000, "warehouse": args.warehouse or "_Test Warehouse - _TC", "available_for_use_date": "2020-06-06", "location": "Test Location", diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index da128467fe7..9bfc88b28ca 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -1,9 +1,334 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import cint, flt, getdate, now_datetime + +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries +from erpnext.assets.doctype.asset.test_asset import ( + create_asset, + create_asset_data, + set_depreciation_settings_in_company, +) +from erpnext.stock.doctype.item.test_item import create_item + class TestAssetCapitalization(unittest.TestCase): - pass + def setUp(self): + set_depreciation_settings_in_company() + create_asset_data() + create_asset_capitalization_data() + frappe.db.sql("delete from `tabTax Rule`") + + def test_capitalization(self): + # Variables + consumed_asset_value = 100_000 + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + service_rate = 500 + service_qty = 2 + service_amount = 1000 + + total_amount = 103_000 + + # Create assets + target_asset = create_asset(asset_name='Asset Capitalization Target Asset', submit=1) + consumed_asset = create_asset(asset_name='Asset Capitalization Consumable Asset', asset_value=consumed_asset_value, + submit=1) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization(target_asset=target_asset.name, + stock_qty=stock_qty, stock_rate=stock_rate, + consumed_asset=consumed_asset.name, + service_qty=service_qty, service_rate=service_rate, + service_expense_account='Expenses Included In Asset Valuation - _TC', + submit=1) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, 'Capitalization') + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value) + self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value) + + self.assertEqual(asset_capitalization.service_items[0].amount, service_amount) + self.assertEqual(asset_capitalization.service_items_total, service_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset.reload() + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test Consumed Asset values + self.assertEqual(consumed_asset.db_get('status'), 'Capitalized') + + # Test General Ledger Entries + expected_gle = { + '_Test Fixed Asset - _TC': 3000, + 'Expenses Included In Asset Valuation - _TC': -1000, + 'Stock In Hand - _TC' : -2000 + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ('Capitalization Source Stock Item', '_Test Warehouse - _TC'): { + 'actual_qty': -stock_qty, 'stock_value_difference': -stock_amount + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertEqual(consumed_asset.db_get('status'), 'Submitted') + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + + def test_decapitalization_with_depreciation(self): + # Variables + purchase_date = '2020-01-01' + depreciation_start_date = '2020-12-31' + capitalization_date = '2021-06-30' + + total_number_of_depreciations = 3 + expected_value_after_useful_life = 10_000 + consumed_asset_purchase_value = 100_000 + consumed_asset_current_value = 70_000 + consumed_asset_value_before_disposal = 55_000 + + target_qty = 10 + target_incoming_rate = 5500 + + depreciation_before_disposal_amount = 15_000 + accumulated_depreciation = 45_000 + + # to accomodate for depreciation on disposal calculation bugs TODO remove this when bug is fixed + consumed_asset_value_before_disposal = 60_082.19 + target_incoming_rate = 6008.219 + depreciation_before_disposal_amount = 9917.81 + accumulated_depreciation = 39_917.81 + + # Create assets + consumed_asset = create_depreciation_asset( + asset_name='Asset Capitalization Consumable Asset', + asset_value=consumed_asset_purchase_value, + purchase_date=purchase_date, + depreciation_start_date=depreciation_start_date, + depreciation_method='Straight Line', + total_number_of_depreciations=total_number_of_depreciations, + frequency_of_depreciation=12, + expected_value_after_useful_life=expected_value_after_useful_life, + submit=1) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + posting_date=capitalization_date, # half a year + target_item_code="Capitalization Target Stock Item", + target_qty=target_qty, + consumed_asset=consumed_asset.name, + submit=1) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, 'Decapitalization') + + self.assertEqual(asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value) + self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal) + self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value_before_disposal) + + self.assertEqual(asset_capitalization.total_value, consumed_asset_value_before_disposal) + self.assertEqual(asset_capitalization.target_incoming_rate, target_incoming_rate) + + # Test Consumed Asset values + consumed_asset.reload() + self.assertEqual(consumed_asset.status, 'Decapitalized') + + consumed_depreciation_schedule = [d for d in consumed_asset.schedules + if getdate(d.schedule_date) == getdate(capitalization_date)] + self.assertTrue(consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry) + self.assertEqual(consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount) + + # Test General Ledger Entries + expected_gle = { + 'Stock In Hand - _TC': consumed_asset_value_before_disposal, + '_Test Accumulated Depreciations - _TC': accumulated_depreciation, + '_Test Fixed Asset - _TC': -consumed_asset_purchase_value, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertEqual(consumed_asset.db_get('status'), 'Partially Depreciated') + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + + +def create_asset_capitalization_data(): + create_item("Capitalization Target Stock Item", + is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) + create_item("Capitalization Source Stock Item", + is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) + create_item("Capitalization Source Service Item", + is_stock_item=0, is_fixed_asset=0, is_purchase_item=0) + + +def create_asset_capitalization(**args): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + args = frappe._dict(args) + + now = now_datetime() + target_asset = frappe.get_doc("Asset", args.target_asset) if args.target_asset else frappe._dict() + target_item_code = target_asset.item_code or args.target_item_code + company = target_asset.company or args.company or "_Test Company" + warehouse = args.warehouse or create_warehouse("_Test Warehouse", company=company) + target_warehouse = args.target_warehouse or warehouse + source_warehouse = args.source_warehouse or warehouse + + asset_capitalization = frappe.new_doc("Asset Capitalization") + asset_capitalization.update({ + "company": company, + "posting_date": args.posting_date or now.strftime('%Y-%m-%d'), + "posting_time": args.posting_time or now.strftime('%H:%M:%S.%f'), + "target_item_code": target_item_code, + "target_asset": target_asset.name, + "target_warehouse": target_warehouse, + "target_qty": flt(args.target_qty) or 1, + "target_batch_no": args.target_batch_no, + "target_serial_no": args.target_serial_no, + "finance_book": args.finance_book + }) + + if args.posting_date or args.posting_time: + asset_capitalization.set_posting_time = 1 + + if flt(args.stock_rate): + asset_capitalization.append("stock_items", { + "item_code": args.stock_item or "Capitalization Source Stock Item", + "warehouse": source_warehouse, + "stock_qty": flt(args.stock_qty) or 1, + "batch_no": args.stock_batch_no, + "serial_no": args.stock_serial_no, + }) + + if args.consumed_asset: + asset_capitalization.append("asset_items", { + "asset": args.consumed_asset, + }) + + if flt(args.service_rate): + asset_capitalization.append("service_items", { + "item_code": args.service_item or "Capitalization Source Service Item", + "expense_account": args.service_expense_account, + "qty": flt(args.service_qty) or 1, + "rate": flt(args.service_rate) + }) + + if args.submit: + create_stock_reconciliation(asset_capitalization, stock_rate=args.stock_rate) + + asset_capitalization.insert() + + if args.submit: + asset_capitalization.submit() + + return asset_capitalization + + +def create_stock_reconciliation(asset_capitalization, stock_rate=0): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + if not asset_capitalization.get('stock_items'): + return + + return create_stock_reconciliation( + item_code=asset_capitalization.stock_items[0].item_code, + warehouse=asset_capitalization.stock_items[0].warehouse, + qty=flt(asset_capitalization.stock_items[0].stock_qty), + rate=flt(stock_rate), + company=asset_capitalization.company) + + +def create_depreciation_asset(**args): + args = frappe._dict(args) + + asset = frappe.new_doc("Asset") + asset.is_existing_asset = 1 + asset.calculate_depreciation = 1 + asset.asset_owner = "Company" + + asset.company = args.company or "_Test Company" + asset.item_code = args.item_code or "Macbook Pro" + asset.asset_name = args.asset_name or asset.item_code + asset.location = args.location or "Test Location" + + asset.purchase_date = args.purchase_date or '2020-01-01' + asset.available_for_use_date = args.available_for_use_date or asset.purchase_date + + asset.gross_purchase_amount = args.asset_value or 100000 + asset.purchase_receipt_amount = asset.gross_purchase_amount + + finance_book = asset.append('finance_books') + finance_book.depreciation_start_date = args.depreciation_start_date or '2020-12-31' + finance_book.depreciation_method = args.depreciation_method or 'Straight Line' + finance_book.total_number_of_depreciations = cint(args.total_number_of_depreciations) or 3 + finance_book.frequency_of_depreciation = cint(args.frequency_of_depreciation) or 12 + finance_book.expected_value_after_useful_life = flt(args.expected_value_after_useful_life) + + if args.submit: + asset.submit() + + frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") + post_depreciation_entries(date=finance_book.depreciation_start_date) + asset.load_from_db() + + return asset + + +def get_actual_gle_dict(name): + return dict(frappe.db.sql(""" + select account, sum(debit-credit) as diff + from `tabGL Entry` + where voucher_type = 'Asset Capitalization' and voucher_no = %s + group by account + having diff != 0 + """, name)) + + +def get_actual_sle_dict(name): + sles = frappe.db.sql(""" + select + item_code, warehouse, + sum(actual_qty) as actual_qty, + sum(stock_value_difference) as stock_value_difference + from `tabStock Ledger Entry` + where voucher_type = 'Asset Capitalization' and voucher_no = %s + group by item_code, warehouse + having actual_qty != 0 + """, name, as_dict=1) + + sle_dict = {} + for d in sles: + sle_dict[(d.item_code, d.warehouse)] = { + 'actual_qty': d.actual_qty, 'stock_value_difference': d.stock_value_difference + } + + return sle_dict From dc3c27fd1b6382c1f1e7a6cdbe4af3826859b247 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Thu, 4 Nov 2021 13:47:33 +0500 Subject: [PATCH 15/54] fix(Asset Capitalization): update code for changes in depreciation logic --- .../doctype/sales_invoice/sales_invoice.py | 86 +------------------ erpnext/assets/doctype/asset/asset.py | 14 +-- .../asset_capitalization.py | 2 +- .../test_asset_capitalization.py | 10 +-- erpnext/controllers/accounts_controller.py | 41 +++++---- 5 files changed, 37 insertions(+), 116 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2b3850e513a..7ed45ce26c6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -27,7 +27,6 @@ from erpnext.assets.doctype.asset.depreciation import ( get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, - make_depreciation_entry, ) from erpnext.controllers.selling_controller import SellingController from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data @@ -924,7 +923,7 @@ class SalesInvoice(SellingController): asset.db_set("disposal_date", None) if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_sale(asset) + self.reverse_depreciation_entry_made_after_disposal(asset) self.reset_depreciation_schedule(asset) else: @@ -980,89 +979,6 @@ class SalesInvoice(SellingController): self.check_finance_books(item, asset) return asset - def check_finance_books(self, item, asset): - if (len(asset.finance_books) > 1 and not item.finance_book - and asset.finance_books[0].finance_book): - frappe.throw(_("Select finance book for the item {0} at row {1}") - .format(item.item_code, item.idx)) - - def depreciate_asset(self, asset): - asset.flags.ignore_validate_update_after_submit = True - asset.prepare_depreciation_data(date_of_sale=self.posting_date) - asset.save() - - make_depreciation_entry(asset.name, self.posting_date) - - def reset_depreciation_schedule(self, asset): - asset.flags.ignore_validate_update_after_submit = True - - # recreate original depreciation schedule of the asset - asset.prepare_depreciation_data(date_of_return=self.posting_date) - - self.modify_depreciation_schedule_for_asset_repairs(asset) - asset.save() - - def modify_depreciation_schedule_for_asset_repairs(self, asset): - asset_repairs = frappe.get_all( - 'Asset Repair', - filters = {'asset': asset.name}, - fields = ['name', 'increase_in_asset_life'] - ) - - for repair in asset_repairs: - if repair.increase_in_asset_life: - asset_repair = frappe.get_doc('Asset Repair', repair.name) - asset_repair.modify_depreciation_schedule() - asset.prepare_depreciation_data() - - def reverse_depreciation_entry_made_after_sale(self, asset): - from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry - - posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() - - row = -1 - finance_book = asset.get('schedules')[0].get('finance_book') - for schedule in asset.get('schedules'): - if schedule.finance_book != finance_book: - row = 0 - finance_book = schedule.finance_book - else: - row += 1 - - if schedule.schedule_date == posting_date_of_original_invoice: - if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \ - or self.sale_happens_in_the_future(posting_date_of_original_invoice): - - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) - reverse_journal_entry.posting_date = nowdate() - frappe.flags.is_reverse_depr_entry = True - reverse_journal_entry.submit() - - frappe.flags.is_reverse_depr_entry = False - asset.flags.ignore_validate_update_after_submit = True - schedule.journal_entry = None - asset.save() - - def get_posting_date_of_sales_invoice(self): - return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') - - # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice): - for finance_book in asset.get('finance_books'): - if schedule.finance_book == finance_book.finance_book: - orginal_schedule_date = add_months(finance_book.depreciation_start_date, - row * cint(finance_book.frequency_of_depreciation)) - - if orginal_schedule_date == posting_date_of_original_invoice: - return True - return False - - def sale_happens_in_the_future(self, posting_date_of_original_invoice): - if posting_date_of_original_invoice > getdate(): - return True - - return False - @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index cf62f496ea6..7c05488db5f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -75,12 +75,12 @@ class Asset(AccountsController): if self.is_existing_asset and self.purchase_invoice: frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) - def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None): + def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() - self.make_depreciation_schedule(date_of_sale) - self.set_accumulated_depreciation(date_of_sale, date_of_return) + self.make_depreciation_schedule(date_of_disposal) + self.set_accumulated_depreciation(date_of_disposal, date_of_return) else: self.finance_books = [] self.value_after_depreciation = (flt(self.gross_purchase_amount) - @@ -181,7 +181,7 @@ class Asset(AccountsController): d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")) - def make_depreciation_schedule(self, date_of_sale): + def make_depreciation_schedule(self, date_of_disposal): if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.get('schedules'): self.schedules = [] @@ -227,14 +227,14 @@ class Asset(AccountsController): monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) # if asset is being sold - if date_of_sale: + if date_of_disposal: from_date = self.get_from_date(d.finance_book) depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, - from_date, date_of_sale) + from_date, date_of_disposal) if depreciation_amount > 0: self.append("schedules", { - "schedule_date": date_of_sale, + "schedule_date": date_of_disposal, "depreciation_amount": depreciation_amount, "depreciation_method": d.depreciation_method, "finance_book": d.finance_book, diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 5a2398650b0..7d08581cbef 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -375,8 +375,8 @@ class AssetCapitalization(StockController): else: if asset.calculate_depreciation: self.depreciate_asset(asset) + asset.reload() - asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, item.asset_value, item.get('finance_book') or self.get('finance_book')) asset.db_set("disposal_date", self.posting_date) diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 9bfc88b28ca..5a342f7d2f9 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -118,11 +118,11 @@ class TestAssetCapitalization(unittest.TestCase): depreciation_before_disposal_amount = 15_000 accumulated_depreciation = 45_000 - # to accomodate for depreciation on disposal calculation bugs TODO remove this when bug is fixed - consumed_asset_value_before_disposal = 60_082.19 - target_incoming_rate = 6008.219 - depreciation_before_disposal_amount = 9917.81 - accumulated_depreciation = 39_917.81 + # to accomodate for depreciation on disposal calculation minor difference + consumed_asset_value_before_disposal = 55_123.29 + target_incoming_rate = 5512.329 + depreciation_before_disposal_amount = 14_876.71 + accumulated_depreciation = 44_876.71 # Create assets consumed_asset = create_depreciation_asset( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9d196390201..6b681ffee58 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -38,7 +38,7 @@ from erpnext.accounts.party import ( validate_party_frozen_disabled, ) from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year -from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries +from erpnext.assets.doctype.asset.depreciation import make_depreciation_entry from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.print_settings import ( set_print_templates_for_item_table, @@ -1516,17 +1516,16 @@ class AccountsController(TransactionBase): def depreciate_asset(self, asset): asset.flags.ignore_validate_update_after_submit = True - asset.prepare_depreciation_data(self.posting_date) + asset.prepare_depreciation_data(date_of_disposal=self.posting_date) asset.save() - post_depreciation_entries(self.posting_date, commit=False) + make_depreciation_entry(asset.name, self.posting_date) def reset_depreciation_schedule(self, asset): asset.flags.ignore_validate_update_after_submit = True # recreate original depreciation schedule of the asset - self.delete_depreciation_entry_made_after_disposal(asset) - asset.prepare_depreciation_data() + asset.prepare_depreciation_data(date_of_return=self.posting_date) self.modify_depreciation_schedule_for_asset_repairs(asset) asset.save() @@ -1544,10 +1543,10 @@ class AccountsController(TransactionBase): asset_repair.modify_depreciation_schedule() asset.prepare_depreciation_data() - def delete_depreciation_entry_made_after_disposal(self, asset): + def reverse_depreciation_entry_made_after_disposal(self, asset): from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry - posting_date_of_original_invoice = self.get_posting_date_of_disposal_entry() + posting_date_of_original_disposal = self.get_posting_date_of_disposal_entry() row = -1 finance_book = asset.get('schedules')[0].get('finance_book') @@ -1558,19 +1557,19 @@ class AccountsController(TransactionBase): else: row += 1 - if schedule.schedule_date == posting_date_of_original_invoice: - if not self.disposal_was_made_on_original_schedule_date(asset, schedule, row, - posting_date_of_original_invoice) or getdate(schedule.schedule_date) > getdate(today()): + if schedule.schedule_date == posting_date_of_original_disposal: + if not self.disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_disposal) \ + or self.disposal_happens_in_the_future(posting_date_of_original_disposal): + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() - - for d in reverse_journal_entry.accounts: - d.reference_type = "Asset" - d.reference_name = asset.name - + frappe.flags.is_reverse_depr_entry = True reverse_journal_entry.submit() - schedule.db_set('journal_entry', None) + frappe.flags.is_reverse_depr_entry = False + asset.flags.ignore_validate_update_after_submit = True + schedule.journal_entry = None + asset.save() def get_posting_date_of_disposal_entry(self): if self.doctype == "Sales Invoice" and self.return_against: @@ -1579,16 +1578,22 @@ class AccountsController(TransactionBase): return self.posting_date # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def disposal_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_disposal): + def disposal_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_disposal): for finance_book in asset.get('finance_books'): if schedule.finance_book == finance_book.finance_book: orginal_schedule_date = add_months(finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)) - if orginal_schedule_date == posting_date_of_original_disposal: + if orginal_schedule_date == posting_date_of_disposal: return True return False + def disposal_happens_in_the_future(self, posting_date_of_disposal): + if posting_date_of_disposal > getdate(): + return True + + return False + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) From 85d1a237ce13d857ebbecd6a7e196c06d9aa3735 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Thu, 4 Nov 2021 14:15:47 +0500 Subject: [PATCH 16/54] fix(Asset Capitalization): Reverse depreciation on cancel --- .../assets/doctype/asset_capitalization/asset_capitalization.py | 1 + .../doctype/asset_capitalization/test_asset_capitalization.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 7d08581cbef..a8f2d79c270 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -371,6 +371,7 @@ class AssetCapitalization(StockController): self.set_consumed_asset_status(asset) if asset.calculate_depreciation: + self.reverse_depreciation_entry_made_after_disposal(asset) self.reset_depreciation_schedule(asset) else: if asset.calculate_depreciation: diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 5a342f7d2f9..7046de6f837 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -174,6 +174,7 @@ class TestAssetCapitalization(unittest.TestCase): self.assertEqual(actual_gle, expected_gle) # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.reload() asset_capitalization.cancel() self.assertEqual(consumed_asset.db_get('status'), 'Partially Depreciated') self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) From cdb18000871931def61dda3f1b4a45b4c38d444b Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Tue, 9 Nov 2021 12:35:01 +0500 Subject: [PATCH 17/54] chore: remove unused import --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 86b7aaac879..e83873fe6c3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -9,7 +9,6 @@ from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values from frappe.utils import ( add_days, - add_months, cint, cstr, flt, From 06aead0470d0c1659e7c15db642cb9a10f2999f6 Mon Sep 17 00:00:00 2001 From: Saif Ur Rehman Date: Wed, 10 Nov 2021 13:45:40 +0500 Subject: [PATCH 18/54] chore: isort --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e83873fe6c3..5297cc9502b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -7,16 +7,7 @@ from frappe import _, msgprint, throw from frappe.contacts.doctype.address.address import get_address_display from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values -from frappe.utils import ( - add_days, - cint, - cstr, - flt, - formatdate, - get_link_to_form, - getdate, - nowdate, -) +from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date From 38c31077c9ca63ef4da586680cd5bd326f04144a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 13 Sep 2022 14:56:21 +0530 Subject: [PATCH 19/54] feat: Asset Capitalization - manual selection of entry type - GLE cleanup with smaller functions - GLE considering periodical inventory - test cases --- erpnext/assets/doctype/asset/test_asset.py | 10 + .../asset_capitalization.js | 7 +- .../asset_capitalization.json | 44 +- .../asset_capitalization.py | 498 ++++++++++-------- .../test_asset_capitalization.py | 339 ++++++++---- .../doctype/asset_repair/test_asset_repair.py | 12 - .../test_stock_reconciliation.py | 7 +- 7 files changed, 583 insertions(+), 334 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 132840e38c2..e7af9bd5bc2 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1425,6 +1425,16 @@ def create_asset_category(): "depreciation_expense_account": "_Test Depreciations - _TC", }, ) + asset_category.append( + "accounts", + { + "company_name": "_Test Company with perpetual inventory", + "fixed_asset_account": "_Test Fixed Asset - TCP1", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", + "depreciation_expense_account": "_Test Depreciations - TCP1", + }, + ) + asset_category.insert() diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index d135e60ae7f..9c7f70b0e57 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -27,7 +27,11 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s me.setup_warehouse_query(); me.frm.set_query("target_item_code", function() { - return erpnext.queries.item(); + if (me.frm.doc.entry_type == "Capitalization") { + return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 1}); + } else { + return erpnext.queries.item({"is_stock_item": 1, "is_fixed_asset": 0}); + } }); me.frm.set_query("target_asset", function() { @@ -410,5 +414,4 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } }; -//$.extend(cur_frm.cscript, new erpnext.assets.AssetCapitalization({frm: cur_frm})); cur_frm.cscript = new erpnext.assets.AssetCapitalization({frm: cur_frm}); diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index d7e6b547162..d1be5752d61 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -8,29 +8,28 @@ "engine": "InnoDB", "field_order": [ "title", + "naming_series", + "entry_type", "target_item_code", "target_item_name", "target_is_fixed_asset", "target_has_batch_no", "target_has_serial_no", - "entry_type", - "finance_book", - "naming_series", "column_break_9", + "target_asset", + "target_asset_name", + "target_warehouse", + "target_qty", + "target_stock_uom", + "target_batch_no", + "target_serial_no", + "column_break_5", "company", + "finance_book", "posting_date", "posting_time", "set_posting_time", "amended_from", - "target_item_details_section", - "target_asset", - "target_asset_name", - "target_warehouse", - "target_batch_no", - "target_serial_no", - "column_break_5", - "target_qty", - "target_stock_uom", "section_break_16", "stock_items", "stock_items_total", @@ -86,16 +85,17 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:!doc.target_item_code || doc.target_is_fixed_asset", + "depends_on": "eval:doc.entry_type=='Capitalization'", "fieldname": "target_asset", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Asset", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", "no_copy": 1, "options": "Asset" }, { - "depends_on": "target_asset", + "depends_on": "eval:doc.entry_type=='Capitalization'", "fetch_from": "target_asset.asset_name", "fieldname": "target_asset_name", "fieldtype": "Data", @@ -170,15 +170,11 @@ "options": "Asset Capitalization Stock Item" }, { - "fieldname": "target_item_details_section", - "fieldtype": "Section Break", - "label": "Target Item Details" - }, - { - "depends_on": "eval:!doc.target_is_fixed_asset", + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fieldname": "target_warehouse", "fieldtype": "Link", "label": "Target Warehouse", + "mandatory_depends_on": "eval:doc.entry_type=='Decapitalization'", "options": "Warehouse" }, { @@ -240,13 +236,14 @@ "options": "Asset Capitalization Asset Item" }, { + "default": "Capitalization", "fieldname": "entry_type", "fieldtype": "Select", "in_list_view": 1, "in_standard_filter": 1, "label": "Entry Type", - "options": "\nCapitalization\nDecapitalization", - "read_only": 1 + "options": "Capitalization\nDecapitalization", + "reqd": 1 }, { "fieldname": "stock_items_total", @@ -337,7 +334,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-09-15 15:41:27.917458", + "modified": "2022-09-12 15:09:40.771332", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", @@ -377,6 +374,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1, "track_seen": 1 diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index a8f2d79c270..2e6f0ad7b02 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -10,9 +10,9 @@ from frappe import _ from frappe.utils import cint, flt from six import string_types +import erpnext from erpnext.assets.doctype.asset.depreciation import ( get_gl_entries_on_asset_disposal, - get_gl_entries_on_asset_regain, get_value_after_depreciation_on_disposal_date, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account @@ -32,16 +32,26 @@ from erpnext.stock.get_item_details import ( from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.utils import get_incoming_rate -force_fields = ['target_item_name', 'target_asset_name', 'item_name', 'asset_name', - 'target_is_fixed_asset', 'target_has_serial_no', 'target_has_batch_no', - 'target_stock_uom', 'stock_uom', 'target_fixed_asset_account', 'fixed_asset_account'] +force_fields = [ + "target_item_name", + "target_asset_name", + "item_name", + "asset_name", + "target_is_fixed_asset", + "target_has_serial_no", + "target_has_batch_no", + "target_stock_uom", + "stock_uom", + "target_fixed_asset_account", + "fixed_asset_account", + "valuation_rate", +] class AssetCapitalization(StockController): def validate(self): self.validate_posting_time() self.set_missing_values(for_validate=True) - self.set_entry_type() self.validate_target_item() self.validate_target_asset() self.validate_consumed_stock_item() @@ -58,14 +68,13 @@ class AssetCapitalization(StockController): def on_submit(self): self.update_stock_ledger() self.make_gl_entries() + self.update_target_asset() def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.update_stock_ledger() self.make_gl_entries() - - def set_entry_type(self): - self.entry_type = "Capitalization" if self.target_is_fixed_asset else "Decapitalization" + self.update_target_asset() def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code @@ -90,7 +99,7 @@ class AssetCapitalization(StockController): args.update(d.as_dict()) args.doctype = self.doctype args.name = self.name - consumed_stock_item_details = get_consumed_stock_item_details(args, get_valuation_rate=False) + consumed_stock_item_details = get_consumed_stock_item_details(args) for k, v in consumed_stock_item_details.items(): if d.meta.has_field(k) and (not d.get(k) or k in force_fields): d.set(k, v) @@ -100,8 +109,8 @@ class AssetCapitalization(StockController): args.update(d.as_dict()) args.doctype = self.doctype args.name = self.name - args.finance_book = d.get('finance_book') or self.get('finance_book') - consumed_asset_details = get_consumed_asset_details(args, get_asset_value=False) + args.finance_book = d.get("finance_book") or self.get("finance_book") + consumed_asset_details = get_consumed_asset_details(args) for k, v in consumed_asset_details.items(): if d.meta.has_field(k) and (not d.get(k) or k in force_fields): d.set(k, v) @@ -120,8 +129,14 @@ class AssetCapitalization(StockController): target_item = frappe.get_cached_doc("Item", self.target_item_code) if not target_item.is_fixed_asset and not target_item.is_stock_item: - frappe.throw(_("Target Item {0} is neither a Fixed Asset nor a Stock Item") - .format(target_item.name)) + frappe.throw( + _("Target Item {0} is neither a Fixed Asset nor a Stock Item").format(target_item.name) + ) + + if self.entry_type == "Capitalization" and not target_item.is_fixed_asset: + frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name)) + elif self.entry_type == "Decapitalization" and not target_item.is_stock_item: + frappe.throw(_("Target Item {0} must be a Stock Item").format(target_item.name)) if target_item.is_fixed_asset: self.target_qty = 1 @@ -144,14 +159,13 @@ class AssetCapitalization(StockController): self.validate_item(target_item) def validate_target_asset(self): - if self.target_is_fixed_asset and not self.target_asset: - frappe.throw(_("Target Asset is mandatory for Capitalization")) - if self.target_asset: target_asset = self.get_asset_for_validation(self.target_asset) if target_asset.item_code != self.target_item_code: - frappe.throw(_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)) + frappe.throw( + _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code) + ) self.validate_asset(target_asset) @@ -172,8 +186,11 @@ class AssetCapitalization(StockController): for d in self.asset_items: if d.asset: if d.asset == self.target_asset: - frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset") - .format(d.idx, d.asset)) + frappe.throw( + _("Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset").format( + d.idx, d.asset + ) + ) asset = self.get_asset_for_validation(d.asset) self.validate_asset(asset) @@ -198,18 +215,21 @@ class AssetCapitalization(StockController): d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") def validate_source_mandatory(self): - if not self.target_is_fixed_asset and not self.get('asset_items'): + if not self.target_is_fixed_asset and not self.get("asset_items"): frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization")) - if not self.get('stock_items') and not self.get('asset_items'): + if not self.get("stock_items") and not self.get("asset_items"): frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization")) def validate_item(self, item): from erpnext.stock.doctype.item.item import validate_end_of_life + validate_end_of_life(item.name, item.end_of_life, item.disabled) def get_asset_for_validation(self, asset): - return frappe.db.get_value("Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1) + return frappe.db.get_value( + "Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1 + ) def validate_asset(self, asset): if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): @@ -225,7 +245,7 @@ class AssetCapitalization(StockController): @frappe.whitelist() def set_warehouse_details(self): - for d in self.get('stock_items'): + for d in self.get("stock_items"): if d.item_code and d.warehouse: args = self.get_args_for_incoming_rate(d) warehouse_details = get_warehouse_details(args) @@ -233,27 +253,30 @@ class AssetCapitalization(StockController): @frappe.whitelist() def set_asset_values(self): - for d in self.get('asset_items'): + for d in self.get("asset_items"): if d.asset: - finance_book = d.get('finance_book') or self.get('finance_book') + finance_book = d.get("finance_book") or self.get("finance_book") d.current_asset_value = flt(get_current_asset_value(d.asset, finance_book=finance_book)) - d.asset_value = get_value_after_depreciation_on_disposal_date(d.asset, self.posting_date, - finance_book=finance_book) + d.asset_value = get_value_after_depreciation_on_disposal_date( + d.asset, self.posting_date, finance_book=finance_book + ) def get_args_for_incoming_rate(self, item): - return frappe._dict({ - "item_code": item.item_code, - "warehouse": item.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1 * flt(item.stock_qty), - "serial_no": item.serial_no, - "batch_no": item.batch_no, - "voucher_type": self.doctype, - "voucher_no": self.name, - "company": self.company, - "allow_zero_valuation": cint(item.get('allow_zero_valuation_rate')), - }) + return frappe._dict( + { + "item_code": item.item_code, + "warehouse": item.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * flt(item.stock_qty), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": cint(item.get("allow_zero_valuation_rate")), + } + ) def calculate_totals(self): self.stock_items_total = 0 @@ -261,45 +284,51 @@ class AssetCapitalization(StockController): self.service_items_total = 0 for d in self.stock_items: - d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), d.precision('amount')) + d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), d.precision("amount")) self.stock_items_total += d.amount for d in self.asset_items: - d.asset_value = flt(flt(d.asset_value), d.precision('asset_value')) + d.asset_value = flt(flt(d.asset_value), d.precision("asset_value")) self.asset_items_total += d.asset_value for d in self.service_items: - d.amount = flt(flt(d.qty) * flt(d.rate), d.precision('amount')) + d.amount = flt(flt(d.qty) * flt(d.rate), d.precision("amount")) self.service_items_total += d.amount - self.stock_items_total = flt(self.stock_items_total, self.precision('stock_items_total')) - self.asset_items_total = flt(self.asset_items_total, self.precision('asset_items_total')) - self.service_items_total = flt(self.service_items_total, self.precision('service_items_total')) + self.stock_items_total = flt(self.stock_items_total, self.precision("stock_items_total")) + self.asset_items_total = flt(self.asset_items_total, self.precision("asset_items_total")) + self.service_items_total = flt(self.service_items_total, self.precision("service_items_total")) self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total - self.total_value = flt(self.total_value, self.precision('total_value')) + self.total_value = flt(self.total_value, self.precision("total_value")) - self.target_qty = flt(self.target_qty, self.precision('target_qty')) + self.target_qty = flt(self.target_qty, self.precision("target_qty")) self.target_incoming_rate = self.total_value / self.target_qty def update_stock_ledger(self): sl_entries = [] for d in self.stock_items: - sle = self.get_sl_entries(d, { - "actual_qty": -flt(d.stock_qty), - }) + sle = self.get_sl_entries( + d, + { + "actual_qty": -flt(d.stock_qty), + }, + ) sl_entries.append(sle) - if not frappe.db.get_value("Item", self.target_item_code, "is_fixed_asset", cache=1): - sle = self.get_sl_entries(self, { - "item_code": self.target_item_code, - "warehouse": self.target_warehouse, - "batch_no": self.target_batch_no, - "serial_no": self.target_serial_no, - "actual_qty": flt(self.target_qty), - "incoming_rate": flt(self.target_incoming_rate) - }) + if self.entry_type == "Decapitalization" and not self.target_is_fixed_asset: + sle = self.get_sl_entries( + self, + { + "item_code": self.target_item_code, + "warehouse": self.target_warehouse, + "batch_no": self.target_batch_no, + "serial_no": self.target_serial_no, + "actual_qty": flt(self.target_qty), + "incoming_rate": flt(self.target_incoming_rate), + }, + ) sl_entries.append(sle) # reverse sl entries if cancel @@ -312,139 +341,183 @@ class AssetCapitalization(StockController): def make_gl_entries(self, gl_entries=None, from_repost=False): from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries - if not gl_entries: - gl_entries = self.get_gl_entries() - if self.docstatus == 1: + if not gl_entries: + gl_entries = self.get_gl_entries() + if gl_entries: make_gl_entries(gl_entries, from_repost=from_repost) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): + def get_gl_entries( + self, warehouse_account=None, default_expense_account=None, default_cost_center=None + ): # Stock GL Entries gl_entries = [] - if not warehouse_account: - warehouse_account = get_warehouse_account_map(self.company) + self.warehouse_account = warehouse_account + if not self.warehouse_account: + self.warehouse_account = get_warehouse_account_map(self.company) precision = self.get_debit_field_precision() - sle_map = self.get_stock_ledger_details() - - if self.target_is_fixed_asset: - target_account = self.target_fixed_asset_account - else: - target_account = warehouse_account[self.target_warehouse]["account"] + self.sle_map = self.get_stock_ledger_details() + target_account = self.get_target_account() target_against = set() + self.get_gl_entries_for_consumed_stock_items( + gl_entries, target_account, target_against, precision + ) + self.get_gl_entries_for_consumed_asset_items( + gl_entries, target_account, target_against, precision + ) + self.get_gl_entries_for_consumed_service_items( + gl_entries, target_account, target_against, precision + ) + + self.get_gl_entries_for_target_item(gl_entries, target_against, precision) + return gl_entries + + def get_target_account(self): + if self.target_is_fixed_asset: + return self.target_fixed_asset_account + else: + return self.warehouse_account[self.target_warehouse]["account"] + + def get_gl_entries_for_consumed_stock_items( + self, gl_entries, target_account, target_against, precision + ): # Consumed Stock Items - total_consumed_stock_value = 0 for item_row in self.stock_items: - sle_list = sle_map.get(item_row.name) + sle_list = self.sle_map.get(item_row.name) if sle_list: for sle in sle_list: stock_value_difference = flt(sle.stock_value_difference, precision) - total_consumed_stock_value += -1 * sle.stock_value_difference - account = warehouse_account[sle.warehouse]["account"] + if erpnext.is_perpetual_inventory_enabled(self.company): + account = self.warehouse_account[sle.warehouse]["account"] + else: + account = self.get_company_default("default_expense_account") + target_against.add(account) + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": target_account, + "cost_center": item_row.cost_center, + "project": item_row.get("project") or self.get("project"), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "credit": -1 * stock_value_difference, + }, + self.warehouse_account[sle.warehouse]["account_currency"], + item=item_row, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": account, - "against": target_account, - "cost_center": item_row.cost_center, - "project": item_row.get('project') or self.get('project'), - "remarks": self.get("remarks") or "Accounting Entry for Stock", - "credit": -1 * stock_value_difference, - }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) - + def get_gl_entries_for_consumed_asset_items( + self, gl_entries, target_account, target_against, precision + ): # Consumed Assets for item in self.asset_items: asset = self.get_asset(item) - if self.docstatus == 2: - fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset, - item.asset_value, item.get('finance_book') or self.get('finance_book')) - asset.db_set("disposal_date", None) + if asset.calculate_depreciation: + self.depreciate_asset(asset) + asset.reload() + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( + asset, item.asset_value, item.get("finance_book") or self.get("finance_book") + ) + + asset.db_set("disposal_date", self.posting_date) + + self.set_consumed_asset_status(asset) + + for gle in fixed_asset_gl_entries: + gle["against"] = target_account + gl_entries.append(self.get_gl_dict(gle, item=item)) + target_against.add(gle["account"]) + + def get_gl_entries_for_consumed_service_items( + self, gl_entries, target_account, target_against, precision + ): + # Service Expenses + for item_row in self.service_items: + expense_amount = flt(item_row.amount, precision) + target_against.add(item_row.expense_account) + + gl_entries.append( + self.get_gl_dict( + { + "account": item_row.expense_account, + "against": target_account, + "cost_center": item_row.cost_center, + "project": item_row.get("project") or self.get("project"), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "credit": expense_amount, + }, + item=item_row, + ) + ) + + def get_gl_entries_for_target_item(self, gl_entries, target_against, precision): + if self.target_is_fixed_asset: + # Capitalization + gl_entries.append( + self.get_gl_dict( + { + "account": self.target_fixed_asset_account, + "against": ", ".join(target_against), + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": flt(self.total_value, precision), + "cost_center": self.get("cost_center"), + }, + item=self, + ) + ) + else: + # Target Stock Item + sle_list = self.sle_map.get(self.name) + for sle in sle_list: + stock_value_difference = flt(sle.stock_value_difference, precision) + account = self.warehouse_account[sle.warehouse]["account"] + + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": ", ".join(target_against), + "cost_center": self.cost_center, + "project": self.get("project"), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "debit": stock_value_difference, + }, + self.warehouse_account[sle.warehouse]["account_currency"], + item=self, + ) + ) + + def update_target_asset(self): + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + if self.docstatus == 1 and self.entry_type == "Capitalization": + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.purchase_date = self.posting_date + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.prepare_depreciation_data() + asset_doc.flags.ignore_validate_update_after_submit = True + asset_doc.save() + elif self.docstatus == 2: + for item in self.asset_items: + asset = self.get_asset(item) + asset.db_set("disposal_date", None) self.set_consumed_asset_status(asset) if asset.calculate_depreciation: self.reverse_depreciation_entry_made_after_disposal(asset) self.reset_depreciation_schedule(asset) - else: - if asset.calculate_depreciation: - self.depreciate_asset(asset) - asset.reload() - - fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, - item.asset_value, item.get('finance_book') or self.get('finance_book')) - asset.db_set("disposal_date", self.posting_date) - - self.set_consumed_asset_status(asset) - - for gle in fixed_asset_gl_entries: - gle["against"] = target_account - gl_entries.append(self.get_gl_dict(gle, item=item)) - - # Service Expenses - total_service_expenses = 0 - for item_row in self.service_items: - expense_amount = flt(item_row.amount, precision) - total_service_expenses += expense_amount - target_against.add(item_row.expense_account) - - gl_entries.append(self.get_gl_dict({ - "account": item_row.expense_account, - "against": target_account, - "cost_center": item_row.cost_center, - "project": item_row.get('project') or self.get('project'), - "remarks": self.get("remarks") or "Accounting Entry for Stock", - "credit": expense_amount, - }, item=item_row)) - - target_against = ", ".join(target_against) - total_target_stock_value = 0 - total_target_asset_value = 0 - - if self.target_is_fixed_asset: - # Target Asset Item - total_target_asset_value = flt(self.total_value, precision) - gl_entries.append(self.get_gl_dict({ - "account": self.target_fixed_asset_account, - "against": target_against, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": total_target_asset_value, - "cost_center": self.get('cost_center') - }, item=self)) - - if self.docstatus == 1: - asset_doc = frappe.get_doc("Asset", self.target_asset) - asset_doc.purchase_date = self.posting_date - asset_doc.gross_purchase_amount = total_target_asset_value - asset_doc.purchase_receipt_amount = total_target_asset_value - asset_doc.prepare_depreciation_data() - asset_doc.flags.ignore_validate_update_after_submit = True - asset_doc.save() - else: - # Target Stock Item - sle_list = sle_map.get(self.name) - for sle in sle_list: - stock_value_difference = flt(sle.stock_value_difference, precision) - total_target_stock_value += sle.stock_value_difference - account = warehouse_account[sle.warehouse]["account"] - - gl_entries.append(self.get_gl_dict({ - "account": account, - "against": target_against, - "cost_center": self.cost_center, - "project": self.get('project'), - "remarks": self.get("remarks") or "Accounting Entry for Stock", - "debit": stock_value_difference, - }, warehouse_account[sle.warehouse]["account_currency"], item=self)) - - return gl_entries def get_asset(self, item): asset = frappe.get_doc("Asset", item.asset) @@ -489,16 +562,12 @@ def get_target_item_details(item_code=None, company=None): item_defaults = get_item_defaults(item.name, company) item_group_defaults = get_item_group_defaults(item.name, company) brand_defaults = get_brand_defaults(item.name, company) - out.cost_center = get_default_cost_center(frappe._dict({'item_code': item.name, 'company': company}), - item_defaults, item_group_defaults, brand_defaults) - - # Set Entry Type - if not item_code: - out.entry_type = "" - elif out.target_is_fixed_asset: - out.entry_type = "Capitalization" - else: - out.entry_type = "Decapitalization" + out.cost_center = get_default_cost_center( + frappe._dict({"item_code": item.name, "company": company}), + item_defaults, + item_group_defaults, + brand_defaults, + ) return out @@ -510,7 +579,7 @@ def get_target_asset_details(asset=None, company=None): # Get Asset Details asset_details = frappe._dict() if asset: - asset_details = frappe.db.get_value("Asset", asset, ['asset_name', 'item_code'], as_dict=1) + asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1) if not asset_details: frappe.throw(_("Asset {0} does not exist").format(asset)) @@ -521,8 +590,9 @@ def get_target_asset_details(asset=None, company=None): out.asset_name = asset_details.asset_name if asset_details.item_code: - out.target_fixed_asset_account = get_asset_category_account('fixed_asset_account', item=asset_details.item_code, - company=company) + out.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=asset_details.item_code, company=company + ) else: out.target_fixed_asset_account = None @@ -530,7 +600,7 @@ def get_target_asset_details(asset=None, company=None): @frappe.whitelist() -def get_consumed_stock_item_details(args, get_valuation_rate=True): +def get_consumed_stock_item_details(args): if isinstance(args, string_types): args = json.loads(args) @@ -554,24 +624,29 @@ def get_consumed_stock_item_details(args, get_valuation_rate=True): item_defaults = get_item_defaults(item.name, args.company) item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + out.cost_center = get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ) - if get_valuation_rate: - if args.item_code and out.warehouse: - incoming_rate_args = frappe._dict({ - 'item_code': args.item_code, - 'warehouse': out.warehouse, - 'posting_date': args.posting_date, - 'posting_time': args.posting_time, - 'qty': -1 * flt(out.stock_qty), + if args.item_code and out.warehouse: + incoming_rate_args = frappe._dict( + { + "item_code": args.item_code, + "warehouse": out.warehouse, + "posting_date": args.posting_date, + "posting_time": args.posting_time, + "qty": -1 * flt(out.stock_qty), "voucher_type": args.doctype, "voucher_no": args.name, "company": args.company, - }) - out.update(get_warehouse_details(incoming_rate_args)) - else: - out.valuation_rate = 0 - out.actual_qty = 0 + "serial_no": args.serial_no, + "batch_no": args.batch_no, + } + ) + out.update(get_warehouse_details(incoming_rate_args)) + else: + out.valuation_rate = 0 + out.actual_qty = 0 return out @@ -587,13 +662,13 @@ def get_warehouse_details(args): if args.warehouse and args.item_code: out = { "actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0, - "valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False) + "valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False), } return out @frappe.whitelist() -def get_consumed_asset_details(args, get_asset_value=True): +def get_consumed_asset_details(args): if isinstance(args, string_types): args = json.loads(args) @@ -602,7 +677,9 @@ def get_consumed_asset_details(args, get_asset_value=True): asset_details = frappe._dict() if args.asset: - asset_details = frappe.db.get_value("Asset", args.asset, ['asset_name', 'item_code', 'item_name'], as_dict=1) + asset_details = frappe.db.get_value( + "Asset", args.asset, ["asset_name", "item_code", "item_name"], as_dict=1 + ) if not asset_details: frappe.throw(_("Asset {0} does not exist").format(args.asset)) @@ -610,19 +687,22 @@ def get_consumed_asset_details(args, get_asset_value=True): out.asset_name = asset_details.asset_name out.item_name = asset_details.item_name - if get_asset_value: - if args.asset: - out.current_asset_value = flt(get_current_asset_value(args.asset, finance_book=args.finance_book)) - out.asset_value = get_value_after_depreciation_on_disposal_date(args.asset, args.posting_date, - finance_book=args.finance_book) - else: - out.current_asset_value = 0 - out.asset_value = 0 + if args.asset: + out.current_asset_value = flt( + get_current_asset_value(args.asset, finance_book=args.finance_book) + ) + out.asset_value = get_value_after_depreciation_on_disposal_date( + args.asset, args.posting_date, finance_book=args.finance_book + ) + else: + out.current_asset_value = 0 + out.asset_value = 0 # Account if asset_details.item_code: - out.fixed_asset_account = get_asset_category_account('fixed_asset_account', item=asset_details.item_code, - company=args.company) + out.fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=asset_details.item_code, company=args.company + ) else: out.fixed_asset_account = None @@ -632,7 +712,9 @@ def get_consumed_asset_details(args, get_asset_value=True): item_defaults = get_item_defaults(item.name, args.company) item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + out.cost_center = get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ) return out @@ -657,7 +739,11 @@ def get_service_item_details(args): item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - out.expense_account = get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) - out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + out.expense_account = get_default_expense_account( + args, item_defaults, item_group_defaults, brand_defaults + ) + out.cost_center = get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ) return out diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 7046de6f837..86861f0b165 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -22,9 +22,12 @@ class TestAssetCapitalization(unittest.TestCase): create_asset_capitalization_data() frappe.db.sql("delete from `tabTax Rule`") - def test_capitalization(self): + def test_capitalization_with_perpetual_inventory(self): + company = "_Test Company with perpetual inventory" + set_depreciation_settings_in_company(company=company) + # Variables - consumed_asset_value = 100_000 + consumed_asset_value = 100000 stock_rate = 1000 stock_qty = 2 @@ -34,23 +37,39 @@ class TestAssetCapitalization(unittest.TestCase): service_qty = 2 service_amount = 1000 - total_amount = 103_000 + total_amount = 103000 # Create assets - target_asset = create_asset(asset_name='Asset Capitalization Target Asset', submit=1) - consumed_asset = create_asset(asset_name='Asset Capitalization Consumable Asset', asset_value=consumed_asset_value, - submit=1) + target_asset = create_asset( + asset_name="Asset Capitalization Target Asset", + submit=1, + warehouse="Stores - TCP1", + company=company, + ) + consumed_asset = create_asset( + asset_name="Asset Capitalization Consumable Asset", + asset_value=consumed_asset_value, + submit=1, + warehouse="Stores - TCP1", + company=company, + ) # Create and submit Asset Captitalization - asset_capitalization = create_asset_capitalization(target_asset=target_asset.name, - stock_qty=stock_qty, stock_rate=stock_rate, + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + target_asset=target_asset.name, + stock_qty=stock_qty, + stock_rate=stock_rate, consumed_asset=consumed_asset.name, - service_qty=service_qty, service_rate=service_rate, - service_expense_account='Expenses Included In Asset Valuation - _TC', - submit=1) + service_qty=service_qty, + service_rate=service_rate, + service_expense_account="Expenses Included In Asset Valuation - TCP1", + company=company, + submit=1, + ) # Test Asset Capitalization values - self.assertEqual(asset_capitalization.entry_type, 'Capitalization') + self.assertEqual(asset_capitalization.entry_type, "Capitalization") self.assertEqual(asset_capitalization.target_qty, 1) self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) @@ -72,13 +91,13 @@ class TestAssetCapitalization(unittest.TestCase): self.assertEqual(target_asset.purchase_receipt_amount, total_amount) # Test Consumed Asset values - self.assertEqual(consumed_asset.db_get('status'), 'Capitalized') + self.assertEqual(consumed_asset.db_get("status"), "Capitalized") # Test General Ledger Entries expected_gle = { - '_Test Fixed Asset - _TC': 3000, - 'Expenses Included In Asset Valuation - _TC': -1000, - 'Stock In Hand - _TC' : -2000 + "_Test Fixed Asset - TCP1": 3000, + "Expenses Included In Asset Valuation - TCP1": -1000, + "_Test Warehouse - TCP1": -2000, } actual_gle = get_actual_gle_dict(asset_capitalization.name) @@ -86,25 +105,121 @@ class TestAssetCapitalization(unittest.TestCase): # Test Stock Ledger Entries expected_sle = { - ('Capitalization Source Stock Item', '_Test Warehouse - _TC'): { - 'actual_qty': -stock_qty, 'stock_value_difference': -stock_amount + ("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): { + "actual_qty": -stock_qty, + "stock_value_difference": -stock_amount, } } actual_sle = get_actual_sle_dict(asset_capitalization.name) - self.assertEqual(actual_sle, expected_sle) # Cancel Asset Capitalization and make test entries and status are reversed asset_capitalization.cancel() - self.assertEqual(consumed_asset.db_get('status'), 'Submitted') + self.assertEqual(consumed_asset.db_get("status"), "Submitted") + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + + def test_capitalization_with_periodical_inventory(self): + company = "_Test Company" + # Variables + consumed_asset_value = 100000 + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + service_rate = 500 + service_qty = 2 + service_amount = 1000 + + total_amount = 103000 + + # Create assets + target_asset = create_asset( + asset_name="Asset Capitalization Target Asset", + submit=1, + warehouse="Stores - _TC", + company=company, + ) + consumed_asset = create_asset( + asset_name="Asset Capitalization Consumable Asset", + asset_value=consumed_asset_value, + submit=1, + warehouse="Stores - _TC", + company=company, + ) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + target_asset=target_asset.name, + stock_qty=stock_qty, + stock_rate=stock_rate, + consumed_asset=consumed_asset.name, + service_qty=service_qty, + service_rate=service_rate, + service_expense_account="Expenses Included In Asset Valuation - _TC", + company=company, + submit=1, + ) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, "Capitalization") + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value) + self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value) + + self.assertEqual(asset_capitalization.service_items[0].amount, service_amount) + self.assertEqual(asset_capitalization.service_items_total, service_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset.reload() + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test Consumed Asset values + self.assertEqual(consumed_asset.db_get("status"), "Capitalized") + + # Test General Ledger Entries + default_expense_account = frappe.db.get_value("Company", company, "default_expense_account") + expected_gle = { + "_Test Fixed Asset - _TC": 3000, + "Expenses Included In Asset Valuation - _TC": -1000, + default_expense_account: -2000, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ("Capitalization Source Stock Item", "_Test Warehouse - _TC"): { + "actual_qty": -stock_qty, + "stock_value_difference": -stock_amount, + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertEqual(consumed_asset.db_get("status"), "Submitted") self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) def test_decapitalization_with_depreciation(self): # Variables - purchase_date = '2020-01-01' - depreciation_start_date = '2020-12-31' - capitalization_date = '2021-06-30' + purchase_date = "2020-01-01" + depreciation_start_date = "2020-12-31" + capitalization_date = "2021-06-30" total_number_of_depreciations = 3 expected_value_after_useful_life = 10_000 @@ -126,29 +241,38 @@ class TestAssetCapitalization(unittest.TestCase): # Create assets consumed_asset = create_depreciation_asset( - asset_name='Asset Capitalization Consumable Asset', + asset_name="Asset Capitalization Consumable Asset", asset_value=consumed_asset_purchase_value, purchase_date=purchase_date, depreciation_start_date=depreciation_start_date, - depreciation_method='Straight Line', + depreciation_method="Straight Line", total_number_of_depreciations=total_number_of_depreciations, frequency_of_depreciation=12, expected_value_after_useful_life=expected_value_after_useful_life, - submit=1) + company="_Test Company with perpetual inventory", + submit=1, + ) # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( + entry_type="Decapitalization", posting_date=capitalization_date, # half a year target_item_code="Capitalization Target Stock Item", target_qty=target_qty, consumed_asset=consumed_asset.name, - submit=1) + company="_Test Company with perpetual inventory", + submit=1, + ) # Test Asset Capitalization values - self.assertEqual(asset_capitalization.entry_type, 'Decapitalization') + self.assertEqual(asset_capitalization.entry_type, "Decapitalization") - self.assertEqual(asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value) - self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal) + self.assertEqual( + asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value + ) + self.assertEqual( + asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal + ) self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value_before_disposal) self.assertEqual(asset_capitalization.total_value, consumed_asset_value_before_disposal) @@ -156,38 +280,45 @@ class TestAssetCapitalization(unittest.TestCase): # Test Consumed Asset values consumed_asset.reload() - self.assertEqual(consumed_asset.status, 'Decapitalized') + self.assertEqual(consumed_asset.status, "Decapitalized") - consumed_depreciation_schedule = [d for d in consumed_asset.schedules - if getdate(d.schedule_date) == getdate(capitalization_date)] - self.assertTrue(consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry) - self.assertEqual(consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount) + consumed_depreciation_schedule = [ + d for d in consumed_asset.schedules if getdate(d.schedule_date) == getdate(capitalization_date) + ] + self.assertTrue( + consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry + ) + self.assertEqual( + consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount + ) # Test General Ledger Entries expected_gle = { - 'Stock In Hand - _TC': consumed_asset_value_before_disposal, - '_Test Accumulated Depreciations - _TC': accumulated_depreciation, - '_Test Fixed Asset - _TC': -consumed_asset_purchase_value, + "_Test Warehouse - TCP1": consumed_asset_value_before_disposal, + "_Test Accumulated Depreciations - TCP1": accumulated_depreciation, + "_Test Fixed Asset - TCP1": -consumed_asset_purchase_value, } actual_gle = get_actual_gle_dict(asset_capitalization.name) - self.assertEqual(actual_gle, expected_gle) # Cancel Asset Capitalization and make test entries and status are reversed asset_capitalization.reload() asset_capitalization.cancel() - self.assertEqual(consumed_asset.db_get('status'), 'Partially Depreciated') + self.assertEqual(consumed_asset.db_get("status"), "Partially Depreciated") self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) def create_asset_capitalization_data(): - create_item("Capitalization Target Stock Item", - is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) - create_item("Capitalization Source Stock Item", - is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) - create_item("Capitalization Source Service Item", - is_stock_item=0, is_fixed_asset=0, is_purchase_item=0) + create_item( + "Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0 + ) + create_item( + "Capitalization Source Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0 + ) + create_item( + "Capitalization Source Service Item", is_stock_item=0, is_fixed_asset=0, is_purchase_item=0 + ) def create_asset_capitalization(**args): @@ -204,43 +335,55 @@ def create_asset_capitalization(**args): source_warehouse = args.source_warehouse or warehouse asset_capitalization = frappe.new_doc("Asset Capitalization") - asset_capitalization.update({ - "company": company, - "posting_date": args.posting_date or now.strftime('%Y-%m-%d'), - "posting_time": args.posting_time or now.strftime('%H:%M:%S.%f'), - "target_item_code": target_item_code, - "target_asset": target_asset.name, - "target_warehouse": target_warehouse, - "target_qty": flt(args.target_qty) or 1, - "target_batch_no": args.target_batch_no, - "target_serial_no": args.target_serial_no, - "finance_book": args.finance_book - }) + asset_capitalization.update( + { + "entry_type": args.entry_type or "Capitalization", + "company": company, + "posting_date": args.posting_date or now.strftime("%Y-%m-%d"), + "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), + "target_item_code": target_item_code, + "target_asset": target_asset.name, + "target_warehouse": target_warehouse, + "target_qty": flt(args.target_qty) or 1, + "target_batch_no": args.target_batch_no, + "target_serial_no": args.target_serial_no, + "finance_book": args.finance_book, + } + ) if args.posting_date or args.posting_time: asset_capitalization.set_posting_time = 1 if flt(args.stock_rate): - asset_capitalization.append("stock_items", { - "item_code": args.stock_item or "Capitalization Source Stock Item", - "warehouse": source_warehouse, - "stock_qty": flt(args.stock_qty) or 1, - "batch_no": args.stock_batch_no, - "serial_no": args.stock_serial_no, - }) + asset_capitalization.append( + "stock_items", + { + "item_code": args.stock_item or "Capitalization Source Stock Item", + "warehouse": source_warehouse, + "stock_qty": flt(args.stock_qty) or 1, + "batch_no": args.stock_batch_no, + "serial_no": args.stock_serial_no, + }, + ) if args.consumed_asset: - asset_capitalization.append("asset_items", { - "asset": args.consumed_asset, - }) + asset_capitalization.append( + "asset_items", + { + "asset": args.consumed_asset, + }, + ) if flt(args.service_rate): - asset_capitalization.append("service_items", { - "item_code": args.service_item or "Capitalization Source Service Item", - "expense_account": args.service_expense_account, - "qty": flt(args.service_qty) or 1, - "rate": flt(args.service_rate) - }) + asset_capitalization.append( + "service_items", + { + "item_code": args.service_item or "Capitalization Source Service Item", + "expense_account": args.service_expense_account, + "qty": flt(args.service_qty) or 1, + "rate": flt(args.service_rate), + }, + ) if args.submit: create_stock_reconciliation(asset_capitalization, stock_rate=args.stock_rate) @@ -255,17 +398,23 @@ def create_asset_capitalization(**args): def create_stock_reconciliation(asset_capitalization, stock_rate=0): from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + EmptyStockReconciliationItemsError, create_stock_reconciliation, ) - if not asset_capitalization.get('stock_items'): + + if not asset_capitalization.get("stock_items"): return - return create_stock_reconciliation( - item_code=asset_capitalization.stock_items[0].item_code, - warehouse=asset_capitalization.stock_items[0].warehouse, - qty=flt(asset_capitalization.stock_items[0].stock_qty), - rate=flt(stock_rate), - company=asset_capitalization.company) + try: + create_stock_reconciliation( + item_code=asset_capitalization.stock_items[0].item_code, + warehouse=asset_capitalization.stock_items[0].warehouse, + qty=flt(asset_capitalization.stock_items[0].stock_qty), + rate=flt(stock_rate), + company=asset_capitalization.company, + ) + except EmptyStockReconciliationItemsError: + pass def create_depreciation_asset(**args): @@ -281,15 +430,15 @@ def create_depreciation_asset(**args): asset.asset_name = args.asset_name or asset.item_code asset.location = args.location or "Test Location" - asset.purchase_date = args.purchase_date or '2020-01-01' + asset.purchase_date = args.purchase_date or "2020-01-01" asset.available_for_use_date = args.available_for_use_date or asset.purchase_date asset.gross_purchase_amount = args.asset_value or 100000 asset.purchase_receipt_amount = asset.gross_purchase_amount - finance_book = asset.append('finance_books') - finance_book.depreciation_start_date = args.depreciation_start_date or '2020-12-31' - finance_book.depreciation_method = args.depreciation_method or 'Straight Line' + finance_book = asset.append("finance_books") + finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31" + finance_book.depreciation_method = args.depreciation_method or "Straight Line" finance_book.total_number_of_depreciations = cint(args.total_number_of_depreciations) or 3 finance_book.frequency_of_depreciation = cint(args.frequency_of_depreciation) or 12 finance_book.expected_value_after_useful_life = flt(args.expected_value_after_useful_life) @@ -305,17 +454,23 @@ def create_depreciation_asset(**args): def get_actual_gle_dict(name): - return dict(frappe.db.sql(""" + return dict( + frappe.db.sql( + """ select account, sum(debit-credit) as diff from `tabGL Entry` where voucher_type = 'Asset Capitalization' and voucher_no = %s group by account having diff != 0 - """, name)) + """, + name, + ) + ) def get_actual_sle_dict(name): - sles = frappe.db.sql(""" + sles = frappe.db.sql( + """ select item_code, warehouse, sum(actual_qty) as actual_qty, @@ -324,12 +479,16 @@ def get_actual_sle_dict(name): where voucher_type = 'Asset Capitalization' and voucher_no = %s group by item_code, warehouse having actual_qty != 0 - """, name, as_dict=1) + """, + name, + as_dict=1, + ) sle_dict = {} for d in sles: sle_dict[(d.item_code, d.warehouse)] = { - 'actual_qty': d.actual_qty, 'stock_value_difference': d.stock_value_difference + "actual_qty": d.actual_qty, + "stock_value_difference": d.stock_value_difference, } return sle_dict diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 6e06f52ac65..2786349f7b6 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -129,18 +129,6 @@ class TestAssetRepair(unittest.TestCase): def test_gl_entries_with_perpetual_inventory(self): set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") - asset_category = frappe.get_doc("Asset Category", "Computers") - asset_category.append( - "accounts", - { - "company_name": "_Test Company with perpetual inventory", - "fixed_asset_account": "_Test Fixed Asset - TCP1", - "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", - "depreciation_expense_account": "_Test Depreciations - TCP1", - }, - ) - asset_category.save() - asset_repair = create_asset_repair( capitalize_repair_cost=1, stock_consumption=1, diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 191c03f5f1c..4e76ae781f9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -727,7 +727,12 @@ def create_stock_reconciliation(**args): sr.set_posting_time = 1 sr.company = args.company or "_Test Company" sr.expense_account = args.expense_account or ( - "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC" + ( + frappe.get_cached_value("Company", sr.company, "stock_adjustment_account") + or "Stock Adjustment - _TC" + ) + if frappe.get_all("Stock Ledger Entry", {"company": sr.company}) + else "Temporary Opening - _TC" ) sr.cost_center = ( args.cost_center From 38488c13e616d164097a60e08c229bbeedf3bab3 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 13 Sep 2022 21:52:58 +0530 Subject: [PATCH 20/54] fix: unknown column error while updating value of maintain-stock in item master (cherry picked from commit 7b878ea3d8f2a1da6c8bd9d4994e638c630e59ba) --- erpnext/stock/doctype/item/item.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 87fa72d74f0..143fe408c34 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -945,7 +945,12 @@ class Item(Document): if doctype == "Product Bundle": filters = {"new_item_code": self.name} - if doctype in ( + if linked_doc := frappe.db.get_value( + doctype, filters, ["new_item_code as docname"], as_dict=True + ): + return linked_doc.update({"doctype": doctype}) + + elif doctype in ( "Purchase Invoice Item", "Sales Invoice Item", ): From 2fe72af359c74ffb6264108fa04dd9b3469f8113 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 13 Sep 2022 22:12:56 +0530 Subject: [PATCH 21/54] test: add test case for item master maintain-stock (cherry picked from commit bf1fa014f472e4b614c0b18fab9a72277fa147a2) --- erpnext/stock/doctype/item/test_item.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 89da72195fc..1cee553be5b 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -786,6 +786,36 @@ class TestItem(FrappeTestCase): item.save() self.assertTrue(len(item.customer_code) > 140) + def test_update_is_stock_item(self): + # Step - 1: Create an Item with Maintain Stock enabled + item = make_item(properties={"is_stock_item": 1}) + + # Step - 2: Disable Maintain Stock + item.is_stock_item = 0 + item.save() + item.reload() + self.assertEqual(item.is_stock_item, 0) + + # Step - 3: Create Product Bundle + pb = frappe.new_doc("Product Bundle") + pb.new_item_code = item.name + pb.flags.ignore_mandatory = True + pb.save() + + # Step - 4: Try to enable Maintain Stock, should throw a validation error + item.is_stock_item = 1 + self.assertRaises(frappe.ValidationError, item.save) + item.reload() + + # Step - 5: Delete Product Bundle + pb.delete() + + # Step - 6: Again try to enable Maintain Stock + item.is_stock_item = 1 + item.save() + item.reload() + self.assertEqual(item.is_stock_item, 1) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") From 3b79e24c7edf77c471b89da2c349ebe47c66d572 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 14 Sep 2022 12:55:49 +0530 Subject: [PATCH 22/54] fix: always set default expense account in company --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index dc698886a01..490504a7c9d 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -389,6 +389,7 @@ class Company(NestedSet): "capital_work_in_progress_account": "Capital Work in Progress", "asset_received_but_not_billed": "Asset Received But Not Billed", "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation", + "default_expense_account": "Cost of Goods Sold", } if self.enable_perpetual_inventory: @@ -398,7 +399,6 @@ class Company(NestedSet): "default_inventory_account": "Stock", "stock_adjustment_account": "Stock Adjustment", "expenses_included_in_valuation": "Expenses Included In Valuation", - "default_expense_account": "Cost of Goods Sold", } ) From b9a249918a644b48896b3aecaf9d38e8bf6b4519 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Wed, 14 Sep 2022 11:55:03 +0530 Subject: [PATCH 23/54] fix: pending accrual entries (cherry picked from commit f2209045f8e0deceaa4e743c40ac2fe037d85e3a) --- .../doctype/loan_repayment/loan_repayment.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9b7603fc3b3..a69660dabf3 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -516,6 +516,8 @@ def get_accrued_interest_entries(against_loan, posting_date=None): if not posting_date: posting_date = getdate() + precision = cint(frappe.db.get_default("currency_precision")) or 2 + unpaid_accrued_entries = frappe.db.sql( """ SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount, @@ -536,6 +538,13 @@ def get_accrued_interest_entries(against_loan, posting_date=None): as_dict=1, ) + # Skip entries with zero interest amount & payable principal amount + unpaid_accrued_entries = [ + d + for d in unpaid_accrued_entries + if flt(d.interest_amount, precision) > 0 or flt(d.payable_principal_amount, precision) > 0 + ] + return unpaid_accrued_entries From 153ef5f164d2b361ac5c428076fcc8c7bba8aee2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 14 Sep 2022 17:20:19 +0530 Subject: [PATCH 24/54] fix: test cases --- .../stock_reconciliation/test_stock_reconciliation.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 4e76ae781f9..7b984d38475 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -729,15 +729,19 @@ def create_stock_reconciliation(**args): sr.expense_account = args.expense_account or ( ( frappe.get_cached_value("Company", sr.company, "stock_adjustment_account") - or "Stock Adjustment - _TC" + or frappe.get_cached_value( + "Account", {"account_type": "Stock Adjustment", "company": sr.company}, "name" + ) ) if frappe.get_all("Stock Ledger Entry", {"company": sr.company}) - else "Temporary Opening - _TC" + else frappe.get_cached_value( + "Account", {"account_type": "Temporary", "company": sr.company}, "name" + ) ) sr.cost_center = ( args.cost_center or frappe.get_cached_value("Company", sr.company, "cost_center") - or "_Test Cost Center - _TC" + or frappe.get_cached_value("Cost Center", filters={"is_group": 0, "company": sr.company}) ) sr.append( From f126e88e5e7b2bf71be4a22eb08fe0aba0083780 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 14 Sep 2022 18:31:15 +0530 Subject: [PATCH 25/54] fix: test cases --- .../assets/doctype/asset_repair/test_asset_repair.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 2786349f7b6..6e06f52ac65 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -129,6 +129,18 @@ class TestAssetRepair(unittest.TestCase): def test_gl_entries_with_perpetual_inventory(self): set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") + asset_category = frappe.get_doc("Asset Category", "Computers") + asset_category.append( + "accounts", + { + "company_name": "_Test Company with perpetual inventory", + "fixed_asset_account": "_Test Fixed Asset - TCP1", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", + "depreciation_expense_account": "_Test Depreciations - TCP1", + }, + ) + asset_category.save() + asset_repair = create_asset_repair( capitalize_repair_cost=1, stock_consumption=1, From 64aad8868414864d089bc552a196c843aae1c02c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 14 Sep 2022 19:21:40 +0530 Subject: [PATCH 26/54] fix: correct sql output format in CRM patch (backport #32213) (#32215) fix: correct sql output format in CRM patch (#32213) (cherry picked from commit 97977cdb4ba42a1a20a6e747cc5586ae92e8954b) Co-authored-by: Ankush Menat --- .../v14_0/migrate_existing_lead_notes_as_per_the_new_format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py b/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py index 032aeccc23d..ec72527552c 100644 --- a/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py +++ b/erpnext/patches/v14_0/migrate_existing_lead_notes_as_per_the_new_format.py @@ -12,7 +12,7 @@ def execute(): frappe.qb.from_(dt) .select(dt.name, dt.notes, dt.modified_by, dt.modified) .where(dt.notes.isnotnull() & dt.notes != "") - ).run() + ).run(as_dict=True) for d in records: if strip_html(cstr(d.notes)).strip(): From f752822bb34d1c41fcc40dfeb2604ce140e25d9b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 15 Sep 2022 12:09:18 +0530 Subject: [PATCH 27/54] fix: abbreviation issue on renaming cost center (cherry picked from commit af21a11e1e3b2e372f19190f9854a26d3e8488bd) --- erpnext/accounts/doctype/account/account.py | 2 +- .../accounts/doctype/cost_center/cost_center.py | 2 +- erpnext/accounts/utils.py | 14 ++++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 2610c8655ef..9dff1168fde 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -37,7 +37,7 @@ class Account(NestedSet): def autoname(self): from erpnext.accounts.utils import get_autoname_with_number - self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) + self.name = get_autoname_with_number(self.account_number, self.account_name, self.company) def validate(self): from erpnext.accounts.utils import validate_field_number diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 31055c3fb42..e8b34bbf034 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -16,7 +16,7 @@ class CostCenter(NestedSet): from erpnext.accounts.utils import get_autoname_with_number self.name = get_autoname_with_number( - self.cost_center_number, self.cost_center_name, None, self.company + self.cost_center_number, self.cost_center_name, self.company ) def validate(self): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f61e8ac960b..c5eb7d8733f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1037,7 +1037,7 @@ def update_cost_center(docname, cost_center_name, cost_center_number, company, m frappe.db.set_value("Cost Center", docname, "cost_center_name", cost_center_name.strip()) - new_name = get_autoname_with_number(cost_center_number, cost_center_name, docname, company) + new_name = get_autoname_with_number(cost_center_number, cost_center_name, company) if docname != new_name: frappe.rename_doc("Cost Center", docname, new_name, force=1, merge=merge) return new_name @@ -1060,16 +1060,14 @@ def validate_field_number(doctype_name, docname, number_value, company, field_na ) -def get_autoname_with_number(number_value, doc_title, name, company): +def get_autoname_with_number(number_value, doc_title, company): """append title with prefix as number and suffix as company's abbreviation separated by '-'""" - if name: - name_split = name.split("-") - parts = [doc_title.strip(), name_split[len(name_split) - 1].strip()] - else: - abbr = frappe.get_cached_value("Company", company, ["abbr"], as_dict=True) - parts = [doc_title.strip(), abbr.abbr] + company_abbr = frappe.get_cached_value("Company", company, "abbr") + parts = [doc_title.strip(), company_abbr] + if cstr(number_value).strip(): parts.insert(0, cstr(number_value).strip()) + return " - ".join(parts) From f370c7b50b0d8902c031b52ad4154bc896613915 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Sep 2022 11:47:10 +0530 Subject: [PATCH 28/54] fix: consider posting time for internal transfer PO (cherry picked from commit cb763938dced524f69f28341c57d4cf191f6dacd) --- erpnext/controllers/buying_controller.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 584266d53b1..5659ad0aa9b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -6,6 +6,7 @@ import frappe from frappe import ValidationError, _, msgprint from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, cstr, flt, getdate +from frappe.utils.data import nowtime from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.party import get_party_details @@ -289,12 +290,16 @@ class BuyingController(SubcontractingController): # Get outgoing rate based on original item cost based on valuation method if not d.get(frappe.scrub(ref_doctype)): + posting_time = self.get("posting_time") + if not posting_time and self.doctype == "Purchase Order": + posting_time = nowtime() + outgoing_rate = get_incoming_rate( { "item_code": d.item_code, "warehouse": d.get("from_warehouse"), "posting_date": self.get("posting_date") or self.get("transation_date"), - "posting_time": self.get("posting_time"), + "posting_time": posting_time, "qty": -1 * flt(d.get("stock_qty")), "serial_no": d.get("serial_no"), "batch_no": d.get("batch_no"), From 442f54a988e209d6d0ecfefd3cb5e3d1d8dea48a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 15 Sep 2022 11:27:35 +0530 Subject: [PATCH 29/54] fix: No permission to read doctype (cherry picked from commit c0da948a4ef9f5fb1dd5764ee1908bd6f0475c7e) --- .../bank_clearance_summary/bank_clearance_summary.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 9d2deea523b..449ebdcd924 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -22,8 +22,7 @@ def get_columns(): { "label": _("Payment Document Type"), "fieldname": "payment_document_type", - "fieldtype": "Link", - "options": "Doctype", + "fieldtype": "Data", "width": 130, }, { @@ -33,15 +32,15 @@ def get_columns(): "options": "payment_document_type", "width": 140, }, - {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, {"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120}, - {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100}, + {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 120}, { "label": _("Against Account"), "fieldname": "against", "fieldtype": "Link", "options": "Account", - "width": 170, + "width": 200, }, {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, ] From 17ea6cc5a585464bf81010997a3bbd08b4aebba7 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 15 Sep 2022 13:11:53 +0530 Subject: [PATCH 30/54] fix: create dunning from sales invoice (cherry picked from commit 29db084dc314979e07288e34b43ac09292b5ff5b) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 608f0828fee..e51938b27f5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2429,7 +2429,6 @@ def create_dunning(source_name, target_doc=None): target.closing_text = letter_text.get("closing_text") target.language = letter_text.get("language") amounts = calculate_interest_and_amount( - target.posting_date, target.outstanding_amount, target.rate_of_interest, target.dunning_fee, From 5cab0aa1d7c620cd6a9f0c7ac5eab83bebd34461 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 16 Sep 2022 11:44:25 +0530 Subject: [PATCH 31/54] fix: disable cwip in asset repair tests --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 6e06f52ac65..6599c078231 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -130,6 +130,7 @@ class TestAssetRepair(unittest.TestCase): set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") asset_category = frappe.get_doc("Asset Category", "Computers") + asset_category.enable_cwip_accounting = 0 asset_category.append( "accounts", { From b700a0be1f6872535ae779afffc892470fb6eede Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 16 Sep 2022 09:45:12 +0530 Subject: [PATCH 32/54] refactor: rewrite Production Plan queries in QB (cherry picked from commit b8cf3b4c776534fe73cb94a9d67feb1a90c624a6) --- .../production_plan/production_plan.py | 464 +++++++++++------- 1 file changed, 283 insertions(+), 181 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 66d458bf750..aa5c50f30f2 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -8,6 +8,7 @@ import json import frappe from frappe import _, msgprint from frappe.model.document import Document +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, ceil, @@ -20,6 +21,7 @@ from frappe.utils import ( nowdate, ) from frappe.utils.csvutils import build_csv_response +from pypika.terms import ExistsCriterion from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children from erpnext.manufacturing.doctype.bom.bom import validate_bom_no @@ -100,39 +102,46 @@ class ProductionPlan(Document): @frappe.whitelist() def get_pending_material_requests(self): """Pull Material Requests that are pending based on criteria selected""" - mr_filter = item_filter = "" + + bom = frappe.qb.DocType("BOM") + mr = frappe.qb.DocType("Material Request") + mr_item = frappe.qb.DocType("Material Request Item") + + pending_mr_query = ( + frappe.qb.from_(mr) + .from_(mr_item) + .select(mr.name, mr.transaction_date) + .distinct() + .where( + (mr_item.parent == mr.name) + & (mr.material_request_type == "Manufacture") + & (mr.docstatus == 1) + & (mr.status != "Stopped") + & (mr.company == self.company) + & (mr_item.qty > IfNull(mr_item.ordered_qty, 0)) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == mr_item.item_code) & (bom.is_active == 1)) + ) + ) + ) + ) + if self.from_date: - mr_filter += " and mr.transaction_date >= %(from_date)s" + pending_mr_query = pending_mr_query.where(mr.transaction_date >= self.from_date) + if self.to_date: - mr_filter += " and mr.transaction_date <= %(to_date)s" + pending_mr_query = pending_mr_query.where(mr.transaction_date <= self.to_date) + if self.warehouse: - mr_filter += " and mr_item.warehouse = %(warehouse)s" + pending_mr_query = pending_mr_query.where(mr_item.warehouse == self.warehouse) if self.item_code: - item_filter += " and mr_item.item_code = %(item)s" + pending_mr_query = pending_mr_query.where(mr_item.item_code == self.item_code) - pending_mr = frappe.db.sql( - """ - select distinct mr.name, mr.transaction_date - from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where mr_item.parent = mr.name - and mr.material_request_type = "Manufacture" - and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s - and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1} - and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1)) - """.format( - mr_filter, item_filter - ), - { - "from_date": self.from_date, - "to_date": self.to_date, - "warehouse": self.warehouse, - "item": self.item_code, - "company": self.company, - }, - as_dict=1, - ) + pending_mr = pending_mr_query.run(as_dict=True) self.add_mr_in_table(pending_mr) @@ -160,16 +169,17 @@ class ProductionPlan(Document): so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)] return so_mr_list - def get_bom_item(self): + def get_bom_item_condition(self): """Check if Item or if its Template has a BOM.""" - bom_item = None + bom_item_condition = None has_bom = frappe.db.exists({"doctype": "BOM", "item": self.item_code, "docstatus": 1}) + if not has_bom: + bom = frappe.qb.DocType("BOM") template_item = frappe.db.get_value("Item", self.item_code, ["variant_of"]) - bom_item = ( - "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item - ) - return bom_item + bom_item_condition = bom.item == template_item or None + + return bom_item_condition def get_so_items(self): # Check for empty table or empty rows @@ -178,46 +188,73 @@ class ProductionPlan(Document): so_list = self.get_so_mr_list("sales_order", "sales_orders") - item_condition = "" - bom_item = "bom.item = so_item.item_code" - if self.item_code and frappe.db.exists("Item", self.item_code): - bom_item = self.get_bom_item() or bom_item - item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) + bom = frappe.qb.DocType("BOM") + so_item = frappe.qb.DocType("Sales Order Item") - items = frappe.db.sql( - """ - select - distinct parent, item_code, warehouse, - (qty - work_order_qty) * conversion_factor as pending_qty, - description, name - from - `tabSales Order Item` so_item - where - parent in (%s) and docstatus = 1 and qty > work_order_qty - and exists (select name from `tabBOM` bom where %s - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(so_list)), bom_item, item_condition), - tuple(so_list), - as_dict=1, + items_subquery = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1) + items_query = ( + frappe.qb.from_(so_item) + .select( + so_item.parent, + so_item.item_code, + so_item.warehouse, + ((so_item.qty - so_item.work_order_qty) * so_item.conversion_factor).as_("pending_qty"), + so_item.description, + so_item.name, + ) + .distinct() + .where( + (so_item.parent.isin(so_list)) + & (so_item.docstatus == 1) + & (so_item.qty > so_item.work_order_qty) + ) + ) + + if self.item_code and frappe.db.exists("Item", self.item_code): + items_query = items_query.where(so_item.item_code == self.item_code) + items_subquery = items_subquery.where( + self.get_bom_item_condition() or bom.item == so_item.item_code + ) + + items_query = items_query.where(ExistsCriterion(items_subquery)) + + items = items_query.run(as_dict=True) + + pi = frappe.qb.DocType("Packed Item") + + packed_items_query = ( + frappe.qb.from_(so_item) + .from_(pi) + .select( + pi.parent, + pi.item_code, + pi.warehouse.as_("warehouse"), + (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty).as_("pending_qty"), + pi.parent_item, + pi.description, + so_item.name, + ) + .distinct() + .where( + (so_item.parent == pi.parent) + & (so_item.docstatus == 1) + & (pi.parent_item == so_item.item_code) + & (so_item.parent.isin(so_list)) + & (so_item.qty > so_item.work_order_qty) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == pi.item_code) & (bom.is_active == 1)) + ) + ) + ) ) if self.item_code: - item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) + packed_items_query = packed_items_query.where(so_item.item_code == self.item_code) - packed_items = frappe.db.sql( - """select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, - (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty) - as pending_qty, pi.parent_item, pi.description, so_item.name - from `tabSales Order Item` so_item, `tabPacked Item` pi - where so_item.parent = pi.parent and so_item.docstatus = 1 - and pi.parent_item = so_item.item_code - and so_item.parent in (%s) and so_item.qty > so_item.work_order_qty - and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(so_list)), item_condition), - tuple(so_list), - as_dict=1, - ) + packed_items = packed_items_query.run(as_dict=True) self.add_items(items + packed_items) self.calculate_total_planned_qty() @@ -233,22 +270,39 @@ class ProductionPlan(Document): mr_list = self.get_so_mr_list("material_request", "material_requests") - item_condition = "" - if self.item_code: - item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code)) + bom = frappe.qb.DocType("BOM") + mr_item = frappe.qb.DocType("Material Request Item") - items = frappe.db.sql( - """select distinct parent, name, item_code, warehouse, description, - (qty - ordered_qty) * conversion_factor as pending_qty - from `tabMaterial Request Item` mr_item - where parent in (%s) and docstatus = 1 and qty > ordered_qty - and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(mr_list)), item_condition), - tuple(mr_list), - as_dict=1, + items_query = ( + frappe.qb.from_(mr_item) + .select( + mr_item.parent, + mr_item.name, + mr_item.item_code, + mr_item.warehouse, + mr_item.description, + ((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor).as_("pending_qty"), + ) + .distinct() + .where( + (mr_item.parent.isin(mr_list)) + & (mr_item.docstatus == 1) + & (mr_item.qty > mr_item.ordered_qty) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == mr_item.item_code) & (bom.is_active == 1)) + ) + ) + ) ) + if self.item_code: + items_query = items_query.where(mr_item.item_code == self.item_code) + + items = items_query.run(as_dict=True) + self.add_items(items) self.calculate_total_planned_qty() @@ -819,29 +873,45 @@ def download_raw_materials(doc, warehouses=None): def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1): - for d in frappe.db.sql( - """select bei.item_code, item.default_bom as bom, - ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, - bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, - item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, - item.purchase_uom, item_uom.conversion_factor, item.safety_stock - from - `tabBOM Explosion Item` bei - JOIN `tabBOM` bom ON bom.name = bei.parent - JOIN `tabItem` item ON item.name = bei.item_code - LEFT JOIN `tabItem Default` item_default - ON item_default.parent = item.name and item_default.company=%s - LEFT JOIN `tabUOM Conversion Detail` item_uom - ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom - where - bei.docstatus < 2 - and bom.name=%s and item.is_stock_item in (1, {0}) - group by bei.item_code, bei.stock_uom""".format( - 0 if include_non_stock_items else 1 - ), - (planned_qty, company, bom_no), - as_dict=1, - ): + bei = frappe.qb.DocType("BOM Explosion Item") + bom = frappe.qb.DocType("BOM") + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + item_uom = frappe.qb.DocType("UOM Conversion Detail") + + data = ( + frappe.qb.from_(bei) + .join(bom) + .on(bom.name == bei.parent) + .join(item) + .on(item.name == bei.item_code) + .left_join(item_default) + .on((item_default.parent == item.name) & (item_default.company == company)) + .left_join(item_uom) + .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom)) + .select( + (IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"), + item.item_name, + bei.description, + bei.stock_uom, + item.min_order_qty, + bei.source_warehouse, + item.default_material_request_type, + item.min_order_qty, + item_default.default_warehouse, + item.purchase_uom, + item_uom.conversion_factor, + item.safety_stock, + ) + .where( + (bei.docstatus < 2) + & (bom.name == bom_no) + & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) + ) + .groupby(bei.item_code, bei.stock_uom) + ).run(as_dict=True) + + for d in data: if not d.conversion_factor and d.purchase_uom: d.conversion_factor = get_uom_conversion_factor(d.item_code, d.purchase_uom) item_details.setdefault(d.get("item_code"), d) @@ -866,33 +936,47 @@ def get_subitems( parent_qty, planned_qty=1, ): - items = frappe.db.sql( - """ - SELECT - bom_item.item_code, default_material_request_type, item.item_name, - ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, - item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, - item.default_bom as default_bom, bom_item.description as description, - bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock, - item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor - FROM - `tabBOM Item` bom_item - JOIN `tabBOM` bom ON bom.name = bom_item.parent - JOIN `tabItem` item ON bom_item.item_code = item.name - LEFT JOIN `tabItem Default` item_default - ON item.name = item_default.parent and item_default.company = %(company)s - LEFT JOIN `tabUOM Conversion Detail` item_uom - ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom - where - bom.name = %(bom)s - and bom_item.docstatus < 2 - and item.is_stock_item in (1, {0}) - group by bom_item.item_code""".format( - 0 if include_non_stock_items else 1 - ), - {"bom": bom_no, "parent_qty": parent_qty, "planned_qty": planned_qty, "company": company}, - as_dict=1, - ) + bom_item = frappe.qb.DocType("BOM Item") + bom = frappe.qb.DocType("BOM") + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + item_uom = frappe.qb.DocType("UOM Conversion Detail") + + items = ( + frappe.qb.from_(bom_item) + .join(bom) + .on(bom.name == bom_item.parent) + .join(item) + .on(bom_item.item_code == item.name) + .left_join(item_default) + .on((item.name == item_default.parent) & (item_default.company == company)) + .left_join(item_uom) + .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom)) + .select( + bom_item.item_code, + item.default_material_request_type, + item.item_name, + IfNull(parent_qty * Sum(bom_item.stock_qty / IfNull(bom.quantity, 1)) * planned_qty, 0).as_( + "qty" + ), + item.is_sub_contracted_item.as_("is_sub_contracted"), + bom_item.source_warehouse, + item.default_bom.as_("default_bom"), + bom_item.description.as_("description"), + bom_item.stock_uom.as_("stock_uom"), + item.min_order_qty.as_("min_order_qty"), + item.safety_stock.as_("safety_stock"), + item_default.default_warehouse, + item.purchase_uom, + item_uom.conversion_factor, + ) + .where( + (bom.name == bom_no) + & (bom_item.docstatus < 2) + & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) + ) + .groupby(bom_item.item_code) + ).run(as_dict=True) for d in items: if not data.get("include_exploded_items") or not d.default_bom: @@ -980,48 +1064,69 @@ def get_material_request_items( def get_sales_orders(self): - so_filter = item_filter = "" - bom_item = "bom.item = so_item.item_code" + bom = frappe.qb.DocType("BOM") + pi = frappe.qb.DocType("Packed Item") + so = frappe.qb.DocType("Sales Order") + so_item = frappe.qb.DocType("Sales Order Item") + + open_so_subquery1 = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1) + + open_so_subquery2 = ( + frappe.qb.from_(pi) + .select(pi.name) + .where( + (pi.parent == so.name) + & (pi.parent_item == so_item.item_code) + & ( + ExistsCriterion( + frappe.qb.from_(bom).select(bom.name).where((bom.item == pi.item_code) & (bom.is_active == 1)) + ) + ) + ) + ) + + open_so_query = ( + frappe.qb.from_(so) + .from_(so_item) + .select(so.name, so.transaction_date, so.customer, so.base_grand_total) + .distinct() + .where( + (so_item.parent == so.name) + & (so.docstatus == 1) + & (so.status.notin(["Stopped", "Closed"])) + & (so.company == self.company) + & (so_item.qty > so_item.work_order_qty) + ) + ) date_field_mapper = { - "from_date": (">=", "so.transaction_date"), - "to_date": ("<=", "so.transaction_date"), - "from_delivery_date": (">=", "so_item.delivery_date"), - "to_delivery_date": ("<=", "so_item.delivery_date"), + "from_date": self.from_date >= so.transaction_date, + "to_date": self.to_date <= so.transaction_date, + "from_delivery_date": self.from_delivery_date >= so_item.delivery_date, + "to_delivery_date": self.to_delivery_date <= so_item.delivery_date, } for field, value in date_field_mapper.items(): if self.get(field): - so_filter += f" and {value[1]} {value[0]} %({field})s" + open_so_query = open_so_query.where(value) - for field in ["customer", "project", "sales_order_status"]: + for field in ("customer", "project", "sales_order_status"): if self.get(field): so_field = "status" if field == "sales_order_status" else field - so_filter += f" and so.{so_field} = %({field})s" + open_so_query = open_so_query.where(so[so_field] == self.get(field)) if self.item_code and frappe.db.exists("Item", self.item_code): - bom_item = self.get_bom_item() or bom_item - item_filter += " and so_item.item_code = %(item_code)s" + open_so_query = open_so_query.where(so_item.item_code == self.item_code) + open_so_subquery1 = open_so_subquery1.where( + self.get_bom_item_condition() or bom.item == so_item.item_code + ) - open_so = frappe.db.sql( - f""" - select distinct so.name, so.transaction_date, so.customer, so.base_grand_total - from `tabSales Order` so, `tabSales Order Item` so_item - where so_item.parent = so.name - and so.docstatus = 1 and so.status not in ('Stopped', 'Closed') - and so.company = %(company)s - and so_item.qty > so_item.work_order_qty {so_filter} {item_filter} - and (exists (select name from `tabBOM` bom where {bom_item} - and bom.is_active = 1) - or exists (select name from `tabPacked Item` pi - where pi.parent = so.name and pi.parent_item = so_item.item_code - and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1))) - """, - self.as_dict(), - as_dict=1, + open_so_query = open_so_query.where( + (ExistsCriterion(open_so_subquery1) | ExistsCriterion(open_so_subquery2)) ) + open_so = open_so_query.run(as_dict=True) + return open_so @@ -1030,37 +1135,34 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): if isinstance(row, str): row = frappe._dict(json.loads(row)) - company = frappe.db.escape(company) - conditions, warehouse = "", "" + bin = frappe.qb.DocType("Bin") + wh = frappe.qb.DocType("Warehouse") + + subquery = frappe.qb.from_(wh).select(wh.name).where(wh.company == company) - conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format( - company - ) if not all_warehouse: warehouse = for_warehouse or row.get("source_warehouse") or row.get("default_warehouse") if warehouse: lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - conditions = """ and warehouse in (select name from `tabWarehouse` - where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse and company = {2}) - """.format( - lft, rgt, company - ) + subquery = subquery.where((wh.lft >= lft) & (wh.rgt <= rgt) & (wh.name == bin.warehouse)) - return frappe.db.sql( - """ select ifnull(sum(projected_qty),0) as projected_qty, - ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, - ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, - ifnull(sum(planned_qty),0) as planned_qty - from `tabBin` where item_code = %(item_code)s {conditions} - group by item_code, warehouse - """.format( - conditions=conditions - ), - {"item_code": row["item_code"]}, - as_dict=1, + query = ( + frappe.qb.from_(bin) + .select( + bin.warehouse, + IfNull(Sum(bin.projected_qty), 0).as_("projected_qty"), + IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), + IfNull(Sum(bin.ordered_qty), 0).as_("ordered_qty"), + IfNull(Sum(bin.reserved_qty_for_production), 0).as_("reserved_qty_for_production"), + IfNull(Sum(bin.planned_qty), 0).as_("planned_qty"), + ) + .where((bin.item_code == row["item_code"]) & (bin.warehouse.isin(subquery))) + .groupby(bin.item_code, bin.warehouse) ) + return query.run(as_dict=True) + @frappe.whitelist() def get_so_details(sales_order): From 5f3caf697560dc37537b4653669f0621ce2cb71b Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 16 Sep 2022 14:39:39 +0530 Subject: [PATCH 33/54] fix: production plan pending-qty (cherry picked from commit 5be7d42dfd242a637a24501f12d0b855916f9e41) --- .../manufacturing/doctype/production_plan/production_plan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index aa5c50f30f2..f1d40c219cb 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -198,7 +198,9 @@ class ProductionPlan(Document): so_item.parent, so_item.item_code, so_item.warehouse, - ((so_item.qty - so_item.work_order_qty) * so_item.conversion_factor).as_("pending_qty"), + ( + (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor + ).as_("pending_qty"), so_item.description, so_item.name, ) From ff789063614a65dab87ba57668816460100a76fe Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 16 Sep 2022 14:55:57 +0530 Subject: [PATCH 34/54] test: update test case for production plan pending-qty (cherry picked from commit bd6af7c6137d7bc111e2e51502e2e63cb44dcaa7) --- .../production_plan/test_production_plan.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 1d2d1bd9a84..60e63980724 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( ) from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_se_from_wo +from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -610,15 +611,21 @@ class TestProductionPlan(FrappeTestCase): """ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + make_stock_entry(item_code="_Test Item", target="Work In Progress - _TC", qty=2, basic_rate=100) make_stock_entry( - item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 - ) - make_stock_entry( - item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100 + item_code="_Test Item Home Desktop 100", target="Work In Progress - _TC", qty=4, basic_rate=100 ) - item = "Test Production Item 1" - so = make_sales_order(item_code=item, qty=1) + item = "_Test FG Item" + + make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=1) + + so = make_sales_order(item_code=item, qty=2) + + dn = make_delivery_note(so.name) + dn.items[0].qty = 1 + dn.save() + dn.submit() pln = create_production_plan( company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True From 436b7e3b70558dd0ec8907cbdfc7c6139dbab44f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 16 Sep 2022 15:23:10 +0530 Subject: [PATCH 35/54] fix: suggestion threshold label and rule was not working for other items with min and max amount (cherry picked from commit f5bd3fa952b9feb6a8f2cc1641e7f2f898fa74b9) --- .../doctype/pricing_rule/pricing_rule.json | 14 +++-- .../doctype/pricing_rule/pricing_rule.py | 8 ++- .../doctype/pricing_rule/test_pricing_rule.py | 62 +++++++++++++++++++ .../accounts/doctype/pricing_rule/utils.py | 56 +++++++++-------- erpnext/controllers/accounts_controller.py | 5 ++ erpnext/public/js/controllers/transaction.js | 20 ++++-- 6 files changed, 125 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 99c5b34fa35..6e7ebd1414d 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -176,7 +176,7 @@ }, { "collapsible": 1, - "depends_on": "eval:doc.apply_on != 'Transaction'", + "depends_on": "eval:doc.apply_on != 'Transaction' && !doc.mixed_conditions", "fieldname": "section_break_18", "fieldtype": "Section Break", "label": "Discount on Other Item" @@ -297,12 +297,12 @@ { "fieldname": "min_qty", "fieldtype": "Float", - "label": "Min Qty" + "label": "Min Qty (As Per Stock UOM)" }, { "fieldname": "max_qty", "fieldtype": "Float", - "label": "Max Qty" + "label": "Max Qty (As Per Stock UOM)" }, { "fieldname": "column_break_21", @@ -481,7 +481,7 @@ "description": "System will notify to increase or decrease quantity or amount ", "fieldname": "threshold_percentage", "fieldtype": "Percent", - "label": "Threshold for Suggestion" + "label": "Threshold for Suggestion (In Percentage)" }, { "description": "Higher the number, higher the priority", @@ -583,10 +583,11 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2021-08-06 15:10:04.219321", + "modified": "2022-09-16 16:00:38.356266", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -642,5 +643,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 98e0a9b2158..9af3188e476 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -324,7 +324,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if isinstance(pricing_rule, str): pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule) - pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) + pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or [] if pricing_rule.get("suggestion"): continue @@ -337,7 +337,6 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other: item_details.update( { - "apply_rule_on_other_items": json.dumps(pricing_rule.apply_rule_on_other_items), "price_or_product_discount": pricing_rule.price_or_product_discount, "apply_rule_on": ( frappe.scrub(pricing_rule.apply_rule_on_other) @@ -347,6 +346,9 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa } ) + if pricing_rule.apply_rule_on_other_items: + item_details["apply_rule_on_other_items"] = json.dumps(pricing_rule.apply_rule_on_other_items) + if pricing_rule.coupon_code_based == 1 and args.coupon_code == None: return item_details @@ -492,7 +494,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra ) if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"): - items = get_pricing_rule_items(pricing_rule) + items = get_pricing_rule_items(pricing_rule, other_items=True) item_details.apply_on = ( frappe.scrub(pricing_rule.apply_rule_on_other) if pricing_rule.apply_rule_on_other diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 3bd0cd2e837..0a9db6b0f59 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -766,6 +766,68 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") + def test_pricing_rule_for_other_items_cond_with_amount(self): + item = make_item("Water Flask New") + other_item = make_item("Other Water Flask New") + make_item_price(item.name, "_Test Price List", 100) + make_item_price(other_item.name, "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Water Flask Rule", + "apply_on": "Item Code", + "apply_rule_on_other": "Item Code", + "price_or_product_discount": "Price", + "rate_or_discount": "Discount Percentage", + "other_item_code": other_item.name, + "items": [ + { + "item_code": item.name, + } + ], + "selling": 1, + "currency": "INR", + "min_amt": 200, + "discount_percentage": 10, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice(do_not_save=True, item_code=item.name) + si.append( + "items", + { + "item_code": other_item.name, + "item_name": other_item.item_name, + "description": other_item.description, + "stock_uom": other_item.stock_uom, + "uom": other_item.stock_uom, + "cost_center": si.items[0].cost_center, + "expense_account": si.items[0].expense_account, + "warehouse": si.items[0].warehouse, + "conversion_factor": 1, + "qty": 1, + }, + ) + si.selling_price_list = "_Test Price List" + si.save() + + self.assertEqual(si.items[0].discount_percentage, 0) + self.assertEqual(si.items[1].discount_percentage, 0) + + si.items[0].qty = 2 + si.save() + + self.assertEqual(si.items[0].discount_percentage, 0) + self.assertEqual(si.items[0].stock_qty, 2) + self.assertEqual(si.items[0].amount, 200) + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[1].discount_percentage, 10) + + si.delete() + rule.delete() + test_dependencies = ["Campaign"] diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 70926cfbd72..1f29d732ba5 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -252,12 +252,6 @@ def filter_pricing_rules(args, pricing_rules, doc=None): stock_qty = flt(args.get("stock_qty")) amount = flt(args.get("price_list_rate")) * flt(args.get("qty")) - if pricing_rules[0].apply_rule_on_other: - field = frappe.scrub(pricing_rules[0].apply_rule_on_other) - - if field and pricing_rules[0].get("other_" + field) != args.get(field): - return - pr_doc = frappe.get_cached_doc("Pricing Rule", pricing_rules[0].name) if pricing_rules[0].mixed_conditions and doc: @@ -274,7 +268,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): amount += data[1] if pricing_rules[0].apply_rule_on_other and not pricing_rules[0].mixed_conditions and doc: - pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules) or [] + pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, args) or [] else: pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, args) @@ -352,16 +346,14 @@ def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, tr if fieldname: msg = _( "If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item." - ).format( - type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description) - ) + ).format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.title)) if fieldname in ["min_amt", "max_amt"]: msg = _("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.").format( type_of_transaction, fmt_money(args.get(fieldname), currency=args.get("currency")), bold(item_code), - bold(args.rule_description), + bold(args.title), ) frappe.msgprint(msg) @@ -454,17 +446,29 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): return sum_qty, sum_amt, items -def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules): - items = get_pricing_rule_items(pr_doc) +def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item): + other_items = get_pricing_rule_items(pr_doc, other_items=True) + pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on")) + apply_on = frappe.scrub(pr_doc.get("apply_on")) + + items = [] + for d in pr_doc.get(pricing_rule_apply_on): + if apply_on == "item_group": + items.extend(get_child_item_groups(d.get(apply_on))) + else: + items.append(d.get(apply_on)) for row in doc.items: - if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items: - pricing_rules = filter_pricing_rules_for_qty_amount( - row.get("stock_qty"), row.get("amount"), pricing_rules, row - ) + if row.get(apply_on) in items: + if not row.get("qty"): + continue + + stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0) + amount = stock_qty * (row.get("price_list_rate") or row.get("rate")) + pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row) if pricing_rules and pricing_rules[0]: - pricing_rules[0].apply_rule_on_other_items = items + pricing_rules[0].apply_rule_on_other_items = other_items return pricing_rules @@ -658,21 +662,21 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values doc.append("items", args) -def get_pricing_rule_items(pr_doc): +def get_pricing_rule_items(pr_doc, other_items=False) -> list: apply_on_data = [] apply_on = frappe.scrub(pr_doc.get("apply_on")) pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on")) - for d in pr_doc.get(pricing_rule_apply_on): - if apply_on == "item_group": - apply_on_data.extend(get_child_item_groups(d.get(apply_on))) - else: - apply_on_data.append(d.get(apply_on)) - - if pr_doc.apply_rule_on_other: + if pr_doc.apply_rule_on_other and other_items: apply_on = frappe.scrub(pr_doc.apply_rule_on_other) apply_on_data.append(pr_doc.get("other_" + apply_on)) + else: + for d in pr_doc.get(pricing_rule_apply_on): + if apply_on == "item_group": + apply_on_data.extend(get_child_item_groups(d.get(apply_on))) + else: + apply_on_data.append(d.get(apply_on)) return list(set(apply_on_data)) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 938de63f33a..8686cb5cc09 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -572,6 +572,11 @@ class AccountsController(TransactionBase): # if user changed the discount percentage then set user's discount percentage ? if pricing_rule_args.get("price_or_product_discount") == "Price": item.set("pricing_rules", pricing_rule_args.get("pricing_rules")) + if pricing_rule_args.get("apply_rule_on_other_items"): + other_items = json.loads(pricing_rule_args.get("apply_rule_on_other_items")) + if other_items and item.item_code not in other_items: + return + item.set("discount_percentage", pricing_rule_args.get("discount_percentage")) item.set("discount_amount", pricing_rule_args.get("discount_amount")) if pricing_rule_args.get("pricing_rule_for") == "Rate": diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c0a8c9e088c..c17610b58a2 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1492,7 +1492,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.model.set_value(child.doctype, child.name, "rate", value); } + if (key === "pricing_rules") { + frappe.model.set_value(child.doctype, child.name, key, value); + } + if (key !== "free_item_data") { + if (child.apply_rule_on_other_items && JSON.parse(child.apply_rule_on_other_items).length) { + if (!in_list(JSON.parse(child.apply_rule_on_other_items), child.item_code)) { + continue; + } + } + frappe.model.set_value(child.doctype, child.name, key, value); } } @@ -1510,11 +1520,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.remove_pricing_rule(frappe.get_doc(child.doctype, child.name)); } - if (child.free_item_data.length > 0) { + if (child.free_item_data && child.free_item_data.length > 0) { this.apply_product_discount(child); } - if (child.apply_rule_on_other_items) { + if (child.apply_rule_on_other_items && JSON.parse(child.apply_rule_on_other_items).length) { items_rule_dict[child.name] = child; } } @@ -1530,11 +1540,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe for(var k in args) { let data = args[k]; - if (data && data.apply_rule_on_other_items) { + if (data && data.apply_rule_on_other_items && JSON.parse(data.apply_rule_on_other_items)) { me.frm.doc.items.forEach(d => { - if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { + if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on])) { for(var k in data) { - if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'price' || k === 'pricing_rules')) { + if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'Price' || k === 'pricing_rules')) { frappe.model.set_value(d.doctype, d.name, k, data[k]); } } From a15f0d427c76f3ba01aec854ea010f1996f09dc7 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Sat, 17 Sep 2022 14:29:42 +0530 Subject: [PATCH 36/54] fix: `sco_rm_detail` in Stock Entry (cherry picked from commit 2f97370b8e4dbef05d8431255e352dd15972519a) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 76bba8af646..75e8c6a8171 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2606,6 +2606,7 @@ def get_items_from_subcontracting_order(source_name, target_doc=None): "uom": item.stock_uom, "stock_uom": item.stock_uom, "conversion_factor": 1, + "sco_rm_detail": item.name, }, ) From cbaffb4858fee0678d823d47d4e3a3e89068ece6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 16 Sep 2022 22:44:23 +0530 Subject: [PATCH 37/54] fix: Parent Level project linkning on creating PO from project (cherry picked from commit 93e134aab028aaadb0d2fb66d25e3a4d5fa89286) --- erpnext/projects/doctype/project/project.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 4f19bbd5163..c48ed918024 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -152,6 +152,7 @@ function open_form(frm, doctype, child_doctype, parentfield) { new_child_doc.parentfield = parentfield; new_child_doc.parenttype = doctype; new_doc[parentfield] = [new_child_doc]; + new_doc.project = frm.doc.name; frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); From 908944b68bd75c88991011e747e02e66b91a2b78 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Sat, 17 Sep 2022 15:58:33 +0530 Subject: [PATCH 38/54] fix: make `po_detail` or `sco_rm_detail` mandatory for SE `Send to Subcontractor` (cherry picked from commit b90875575c20b05027aa1f9718db69e0d60ad133) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 75e8c6a8171..62f2acd2fd7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -919,6 +919,16 @@ class StockEntry(StockController): ) if order_rm_detail: se_item.db_set(self.subcontract_data.rm_detail_field, order_rm_detail) + else: + if not se_item.allow_alternative_item: + frappe.throw( + _("Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3}").format( + se_item.idx, + se_item.item_code, + self.subcontract_data.order_doctype, + self.get(self.subcontract_data.order_field), + ) + ) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: From c98413c9818df5f41231b23ba6bb86b70a8581b8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Sep 2022 16:20:35 +0530 Subject: [PATCH 39/54] fix: use default supplier currency if default supplier is enabled (cherry picked from commit 77fdc37cb75d465a7a5297fc89bba31b8193ebeb) --- erpnext/selling/doctype/sales_order/sales_order.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 09a9652cca6..25806d6ed86 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -880,6 +880,9 @@ def get_events(start, end, filters=None): @frappe.whitelist() def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): """Creates Purchase Order for each Supplier. Returns a list of doc objects.""" + + from erpnext.setup.utils import get_exchange_rate + if not selected_items: return @@ -888,6 +891,15 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t def set_missing_values(source, target): target.supplier = supplier + target.currency = frappe.db.get_value( + "Supplier", filters={"name": supplier}, fieldname=["default_currency"] + ) + company_currency = frappe.db.get_value( + "Company", filters={"name": target.company}, fieldname=["default_currency"] + ) + + target.conversion_rate = get_exchange_rate(target.currency, company_currency, args="for_buying") + target.apply_discount_on = "" target.additional_discount_percentage = 0.0 target.discount_amount = 0.0 From ff210c73eb44bef4e89a10826f3d5415f56b56d0 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 19 Sep 2022 18:47:46 +0530 Subject: [PATCH 40/54] fix: `po_detail` or `sco_rm_detail` not getting set while while mapping SE (cherry picked from commit 3a9c08e7c9c2647c5fe67adbf6061c127e61b276) --- .../doctype/purchase_order/purchase_order.js | 121 +--------------- .../controllers/subcontracting_controller.py | 137 +++++++++++------- .../stock/doctype/stock_entry/stock_entry.js | 8 +- .../stock/doctype/stock_entry/stock_entry.py | 51 ++----- .../subcontracting_order.js | 10 -- 5 files changed, 98 insertions(+), 229 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index fbb42fe2f64..fc99d776d4a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -295,131 +295,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e } make_stock_entry() { - var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }); - var me = this; - - if(items.length >= 1){ - me.raw_material_data = []; - me.show_dialog = 1; - let title = __('Transfer Material to Supplier'); - let fields = [ - {fieldtype:'Section Break', label: __('Raw Materials')}, - {fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), - fields: [ - { - fieldtype:'Data', - fieldname:'item_code', - label: __('Item'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Data', - fieldname:'rm_item_code', - label: __('Raw Material'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'qty', - label: __('Quantity'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Data', - read_only:1, - fieldname:'warehouse', - label: __('Reserve Warehouse'), - in_list_view:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'rate', - label: __('Rate'), - hidden:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'amount', - label: __('Amount'), - hidden:1 - }, - { - fieldtype:'Link', - read_only:1, - fieldname:'uom', - label: __('UOM'), - hidden:1 - } - ], - data: me.raw_material_data, - get_data: function() { - return me.raw_material_data; - } - } - ] - - me.dialog = new frappe.ui.Dialog({ - title: title, fields: fields - }); - - if (me.frm.doc['supplied_items']) { - me.frm.doc['supplied_items'].forEach((item, index) => { - if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { - me.raw_material_data.push ({ - 'name':item.name, - 'item_code': item.main_item_code, - 'rm_item_code': item.rm_item_code, - 'item_name': item.rm_item_code, - 'qty': item.required_qty - item.supplied_qty, - 'warehouse':item.reserve_warehouse, - 'rate':item.rate, - 'amount':item.amount, - 'stock_uom':item.stock_uom - }); - me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); - } - }) - } - - me.dialog.get_field('sub_con_rm_items').check_all_rows() - - me.dialog.show() - this.dialog.set_primary_action(__('Transfer'), function() { - me.values = me.dialog.get_values(); - if(me.values) { - me.values.sub_con_rm_items.map((row,i) => { - if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { - let row_id = i+1; - frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id])); - } - }) - me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()) - me.dialog.hide() - } - }); - } - - me.dialog.get_close_btn().on('click', () => { - me.dialog.hide(); - }); - - } - - _make_rm_stock_entry(rm_items) { frappe.call({ method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry", args: { subcontract_order: cur_frm.doc.name, - rm_items: rm_items, order_doctype: cur_frm.doc.doctype - } - , + }, callback: function(r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index bbd950ed37a..202a880750e 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -770,7 +770,7 @@ def get_item_details(items): item = frappe.qb.DocType("Item") item_list = ( frappe.qb.from_(item) - .select(item.item_code, item.description, item.allow_alternative_item) + .select(item.item_code, item.item_name, item.description, item.allow_alternative_item) .where(item.name.isin(items)) .run(as_dict=True) ) @@ -783,68 +783,93 @@ def get_item_details(items): @frappe.whitelist() -def make_rm_stock_entry(subcontract_order, rm_items, order_doctype="Subcontracting Order"): - rm_items_list = rm_items - - if isinstance(rm_items, str): - rm_items_list = json.loads(rm_items) - elif not rm_items: - frappe.throw(_("No Items available for transfer")) - - if rm_items_list: - fg_items = list(set(item["item_code"] for item in rm_items_list)) - else: - frappe.throw(_("No Items selected for transfer")) - +def make_rm_stock_entry( + subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None +): if subcontract_order: subcontract_order = frappe.get_doc(order_doctype, subcontract_order) - if fg_items: - items = tuple(set(item["rm_item_code"] for item in rm_items_list)) - item_wh = get_item_details(items) + if not rm_items: + if not subcontract_order.supplied_items: + frappe.throw(_("No item available for transfer.")) - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.purpose = "Send to Subcontractor" - if order_doctype == "Purchase Order": - stock_entry.purchase_order = subcontract_order.name - else: - stock_entry.subcontracting_order = subcontract_order.name - stock_entry.supplier = subcontract_order.supplier - stock_entry.supplier_name = subcontract_order.supplier_name - stock_entry.supplier_address = subcontract_order.supplier_address - stock_entry.address_display = subcontract_order.address_display - stock_entry.company = subcontract_order.company - stock_entry.to_warehouse = subcontract_order.supplier_warehouse - stock_entry.set_stock_entry_type() + rm_items = subcontract_order.supplied_items - if order_doctype == "Purchase Order": - rm_detail_field = "po_detail" - else: - rm_detail_field = "sco_rm_detail" + fg_item_code_list = list( + set(item.get("main_item_code") or item.get("item_code") for item in rm_items) + ) - for item_code in fg_items: - for rm_item_data in rm_items_list: - if rm_item_data["item_code"] == item_code: - rm_item_code = rm_item_data["rm_item_code"] - items_dict = { - rm_item_code: { - rm_detail_field: rm_item_data.get("name"), - "item_name": rm_item_data["item_name"], - "description": item_wh.get(rm_item_code, {}).get("description", ""), - "qty": rm_item_data["qty"], - "from_warehouse": rm_item_data["warehouse"], - "stock_uom": rm_item_data["stock_uom"], - "serial_no": rm_item_data.get("serial_no"), - "batch_no": rm_item_data.get("batch_no"), - "main_item_code": rm_item_data["item_code"], - "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + if fg_item_code_list: + rm_item_code_list = tuple(set(item.get("rm_item_code") for item in rm_items)) + item_wh = get_item_details(rm_item_code_list) + + field_no_map, rm_detail_field = "purchase_order", "sco_rm_detail" + if order_doctype == "Purchase Order": + field_no_map, rm_detail_field = "subcontracting_order", "po_detail" + + if target_doc and target_doc.get("items"): + target_doc.items = [] + + stock_entry = get_mapped_doc( + order_doctype, + subcontract_order.name, + { + order_doctype: { + "doctype": "Stock Entry", + "field_map": { + "to_warehouse": "supplier_warehouse", + }, + "field_no_map": [field_no_map], + "validation": { + "docstatus": ["=", 1], + }, + }, + }, + target_doc, + ignore_child_tables=True, + ) + + stock_entry.purpose = "Send to Subcontractor" + + if order_doctype == "Purchase Order": + stock_entry.purchase_order = subcontract_order.name + else: + stock_entry.subcontracting_order = subcontract_order.name + + stock_entry.set_stock_entry_type() + + for fg_item_code in fg_item_code_list: + for rm_item in rm_items: + + if rm_item.get("main_item_code") or rm_item.get("item_code") == fg_item_code: + rm_item_code = rm_item.get("rm_item_code") + + items_dict = { + rm_item_code: { + rm_detail_field: rm_item.get("name"), + "item_name": rm_item.get("item_name") + or item_wh.get(rm_item_code, {}).get("item_name", ""), + "description": item_wh.get(rm_item_code, {}).get("description", ""), + "qty": rm_item.get("qty") + or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0), + "from_warehouse": rm_item.get("warehouse") or rm_item.get("reserve_warehouse"), + "to_warehouse": subcontract_order.supplier_warehouse, + "stock_uom": rm_item.get("stock_uom"), + "serial_no": rm_item.get("serial_no"), + "batch_no": rm_item.get("batch_no"), + "main_item_code": fg_item_code, + "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + } } - } - stock_entry.add_to_stock_entry_detail(items_dict) - return stock_entry.as_dict() - else: - frappe.throw(_("No Items selected for transfer")) - return subcontract_order.name + + stock_entry.add_to_stock_entry_detail(items_dict) + + if target_doc: + return stock_entry + else: + return stock_entry.as_dict() + else: + frappe.throw(_("No Items selected for transfer.")) def add_items_in_ste( diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index a952a93ac72..266ea5f674f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -625,6 +625,12 @@ frappe.ui.form.on('Stock Entry', { purchase_order: (frm) => { if (frm.doc.purchase_order) { frm.set_value("subcontracting_order", ""); + erpnext.utils.map_current_doc({ + method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontract_order', + source_name: frm.doc.purchase_order, + target_doc: frm, + freeze: true, + }); } }, @@ -632,7 +638,7 @@ frappe.ui.form.on('Stock Entry', { if (frm.doc.subcontracting_order) { frm.set_value("purchase_order", ""); erpnext.utils.map_current_doc({ - method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order', + method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontract_order', source_name: frm.doc.subcontracting_order, target_doc: frm, freeze: true, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 62f2acd2fd7..738ac330e39 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1945,6 +1945,8 @@ class StockEntry(StockController): se_child.is_finished_item = item_row.get("is_finished_item", 0) se_child.is_scrap_item = item_row.get("is_scrap_item", 0) se_child.is_process_loss = item_row.get("is_process_loss", 0) + se_child.po_detail = item_row.get("po_detail") + se_child.sco_rm_detail = item_row.get("sco_rm_detail") for field in [ self.subcontract_data.rm_detail_field, @@ -2591,50 +2593,15 @@ def get_supplied_items( @frappe.whitelist() -def get_items_from_subcontracting_order(source_name, target_doc=None): - def post_process(source, target): - target.stock_entry_type = target.purpose = "Send to Subcontractor" - target.subcontracting_order = source_name +def get_items_from_subcontract_order(source_name, target_doc=None): + from erpnext.controllers.subcontracting_controller import make_rm_stock_entry - if target.items: - target.items = [] + if isinstance(target_doc, str): + target_doc = frappe.get_doc(json.loads(target_doc)) - warehouses = {} - for item in source.items: - warehouses[item.name] = item.warehouse - - for item in source.supplied_items: - target.append( - "items", - { - "s_warehouse": warehouses.get(item.reference_name), - "t_warehouse": source.supplier_warehouse, - "subcontracted_item": item.main_item_code, - "item_code": item.rm_item_code, - "qty": max(item.required_qty - item.total_supplied_qty, 0), - "transfer_qty": item.required_qty, - "uom": item.stock_uom, - "stock_uom": item.stock_uom, - "conversion_factor": 1, - "sco_rm_detail": item.name, - }, - ) - - target_doc = get_mapped_doc( - "Subcontracting Order", - source_name, - { - "Subcontracting Order": { - "doctype": "Stock Entry", - "field_no_map": ["purchase_order"], - "validation": { - "docstatus": ["=", 1], - }, - }, - }, - target_doc, - post_process, - ignore_child_tables=True, + order_doctype = "Purchase Order" if target_doc.purchase_order else "Subcontracting Order" + target_doc = make_rm_stock_entry( + subcontract_order=source_name, order_doctype=order_doctype, target_doc=target_doc ) return target_doc diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 40963f86373..15a2ac90912 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -205,20 +205,10 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll } make_stock_entry() { - frappe.model.open_mapped_doc({ - method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order', - source_name: cur_frm.doc.name, - freeze: true, - freeze_message: __('Creating Stock Entry ...') - }); - } - - make_rm_stock_entry(rm_items) { frappe.call({ method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry', args: { subcontract_order: cur_frm.doc.name, - rm_items: rm_items, order_doctype: cur_frm.doc.doctype }, callback: (r) => { From 887663129e95544023b9cd2d5cf506e94f6c0263 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 16 Sep 2022 14:14:14 +0530 Subject: [PATCH 41/54] fix: fetch description only if empty on the payment schedule added fetch_if_empty on description field of payment_schedule. (cherry picked from commit f4b64686ae5a649f09211ef01a84400ea26f12dd) --- .../accounts/doctype/payment_schedule/payment_schedule.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index 6ed7a3154e5..dde9980ce53 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -39,6 +39,7 @@ { "columns": 2, "fetch_from": "payment_term.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Small Text", "in_list_view": 1, @@ -159,7 +160,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-28 05:41:35.084233", + "modified": "2022-09-16 13:57:06.382859", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Schedule", @@ -168,5 +169,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From dcfc11df7a007c4e61eef1238707162e7de15500 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 10 Sep 2022 16:16:38 +0530 Subject: [PATCH 42/54] fix: incorrect gl if tax on multi currency payment entry (cherry picked from commit f0ae77b23b8e474f222d50a1f4aeef08b204b0d2) --- .../doctype/payment_entry/payment_entry.js | 35 +++++++++++++++---- .../doctype/payment_entry/payment_entry.py | 31 +++++++++++----- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 0f530794030..6039bdfe95f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1091,7 +1091,7 @@ frappe.ui.form.on('Payment Entry', { $.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; }); - frm.doc.paid_amount_after_tax = frm.doc.paid_amount; + frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount; }); }, @@ -1182,7 +1182,7 @@ frappe.ui.form.on('Payment Entry', { } cumulated_tax_fraction += tax.tax_fraction_for_current_item; - frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction)) + frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction)) }); }, @@ -1214,6 +1214,7 @@ frappe.ui.form.on('Payment Entry', { frm.doc.total_taxes_and_charges = 0.0; frm.doc.base_total_taxes_and_charges = 0.0; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let actual_tax_dict = {}; // maintain actual tax rate based on idx @@ -1234,8 +1235,8 @@ frappe.ui.form.on('Payment Entry', { } } - tax.tax_amount = current_tax_amount; - tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate; + // tax accounts are only in company currency + tax.base_tax_amount = current_tax_amount; current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; if(i==0) { @@ -1244,9 +1245,29 @@ frappe.ui.form.on('Payment Entry', { tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax)); } - tax.base_total = tax.total * frm.doc.source_exchange_rate; - frm.doc.total_taxes_and_charges += current_tax_amount; - frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate; + // tac accounts are only in company currency + tax.base_total = tax.total + + // calculate total taxes and base total taxes + if(frm.doc.payment_type == "Pay") { + // tax accounts only have company currency + if(tax.currency != frm.doc.paid_to_account_currency) { + //total_taxes_and_charges has the target currency. so using target conversion rate + frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate); + + } else { + frm.doc.total_taxes_and_charges += current_tax_amount; + } + } else if(frm.doc.payment_type == "Receive") { + if(tax.currency != frm.doc.paid_from_account_currency) { + //total_taxes_and_charges has the target currency. so using source conversion rate + frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate); + } else { + frm.doc.total_taxes_and_charges += current_tax_amount; + } + } + + frm.doc.base_total_taxes_and_charges += tax.base_tax_amount; frm.refresh_field('taxes'); frm.refresh_field('total_taxes_and_charges'); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 4618d0807cb..7f245fd083a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -940,6 +940,13 @@ class PaymentEntry(AccountsController): ) if not d.included_in_paid_amount: + if get_account_currency(payment_account) != self.company_currency: + if self.payment_type == "Receive": + exchange_rate = self.target_exchange_rate + elif self.payment_type in ["Pay", "Internal Transfer"]: + exchange_rate = self.source_exchange_rate + base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount")) + gl_entries.append( self.get_gl_dict( { @@ -1033,7 +1040,7 @@ class PaymentEntry(AccountsController): for fieldname in tax_fields: tax.set(fieldname, 0.0) - self.paid_amount_after_tax = self.paid_amount + self.paid_amount_after_tax = self.base_paid_amount def determine_exclusive_rate(self): if not any(cint(tax.included_in_paid_amount) for tax in self.get("taxes")): @@ -1052,7 +1059,7 @@ class PaymentEntry(AccountsController): cumulated_tax_fraction += tax.tax_fraction_for_current_item - self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction)) + self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction)) def calculate_taxes(self): self.total_taxes_and_charges = 0.0 @@ -1075,7 +1082,7 @@ class PaymentEntry(AccountsController): current_tax_amount += actual_tax_dict[tax.idx] tax.tax_amount = current_tax_amount - tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate + tax.base_tax_amount = current_tax_amount if tax.add_deduct_tax == "Deduct": current_tax_amount *= -1.0 @@ -1089,14 +1096,20 @@ class PaymentEntry(AccountsController): self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax) ) - tax.base_total = tax.total * self.source_exchange_rate + tax.base_total = tax.total if self.payment_type == "Pay": - self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) - self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) - else: - self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) - self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + if tax.currency != self.paid_to_account_currency: + self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) + else: + self.total_taxes_and_charges += current_tax_amount + elif self.payment_type == "Receive": + if tax.currency != self.paid_from_account_currency: + self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + else: + self.total_taxes_and_charges += current_tax_amount + + self.base_total_taxes_and_charges += tax.base_tax_amount if self.get("taxes"): self.paid_amount_after_tax = self.get("taxes")[-1].base_total From 5ba5b7bf510716f170268a655a69bf046669b0e6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Sep 2022 15:57:58 +0530 Subject: [PATCH 43/54] test: gl entries of payments with advance tax (cherry picked from commit 5bd5dd726229dc101de757ed72bb40b94ffc33c9) --- .../payment_entry/test_payment_entry.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 02627eb0074..123b5dfd512 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe import qb from frappe.tests.utils import FrappeTestCase from frappe.utils import flt, nowdate @@ -722,6 +723,46 @@ class TestPaymentEntry(FrappeTestCase): flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2) ) + def test_gl_of_multi_currency_payment_with_taxes(self): + payment_entry = create_payment_entry( + party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True + ) + payment_entry.append( + "taxes", + { + "account_head": "_Test Account Service Tax - _TC", + "charge_type": "Actual", + "tax_amount": 100, + "add_deduct_tax": "Add", + "description": "Test", + }, + ) + payment_entry.target_exchange_rate = 80 + payment_entry.received_amount = 12.5 + payment_entry = payment_entry.submit() + gle = qb.DocType("GL Entry") + gl_entries = ( + qb.from_(gle) + .select( + gle.account, + gle.debit, + gle.credit, + gle.debit_in_account_currency, + gle.credit_in_account_currency, + ) + .orderby(gle.account) + .where(gle.voucher_no == payment_entry.name) + .run() + ) + + expected_gl_entries = ( + ("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0), + ("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0), + ("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0), + ) + + self.assertEqual(gl_entries, expected_gl_entries) + def test_payment_entry_against_onhold_purchase_invoice(self): pi = make_purchase_invoice() From a56b5ed8e50faf9265005267034001791d2764e5 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 20 Sep 2022 09:43:12 +0530 Subject: [PATCH 44/54] refactor: rewrite `Item Shortage Report` queries in QB (cherry picked from commit f0a78aa559c94e45564130f9571d269eefe6c551) --- .../item_shortage_report.py | 67 ++++++++----------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py index 03a3a6a0b83..9fafe91c3f9 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -8,8 +8,7 @@ from frappe import _ def execute(filters=None): columns = get_columns() - conditions = get_conditions(filters) - data = get_data(conditions, filters) + data = get_data(filters) if not data: return [], [], None, [] @@ -19,49 +18,39 @@ def execute(filters=None): return columns, data, None, chart_data -def get_conditions(filters): - conditions = "" +def get_data(filters): + bin = frappe.qb.DocType("Bin") + wh = frappe.qb.DocType("Warehouse") + item = frappe.qb.DocType("Item") - if filters.get("warehouse"): - conditions += "AND warehouse in %(warehouse)s" - if filters.get("company"): - conditions += "AND company = %(company)s" - - return conditions - - -def get_data(conditions, filters): - data = frappe.db.sql( - """ - SELECT + query = ( + frappe.qb.from_(bin) + .from_(wh) + .from_(item) + .select( bin.warehouse, bin.item_code, - bin.actual_qty , - bin.ordered_qty , - bin.planned_qty , - bin.reserved_qty , + bin.actual_qty, + bin.ordered_qty, + bin.planned_qty, + bin.reserved_qty, bin.reserved_qty_for_production, - bin.projected_qty , - warehouse.company, - item.item_name , - item.description - FROM - `tabBin` bin, - `tabWarehouse` warehouse, - `tabItem` item - WHERE - bin.projected_qty<0 - AND warehouse.name = bin.warehouse - AND bin.item_code=item.name - {0} - ORDER BY bin.projected_qty;""".format( - conditions - ), - filters, - as_dict=1, + bin.projected_qty, + wh.company, + item.item_name, + item.description, + ) + .where((bin.projected_qty < 0) & (wh.name == bin.warehouse) & (bin.item_code == item.name)) + .orderby(bin.projected_qty) ) - return data + if filters.get("warehouse"): + query = query.where(bin.warehouse.isin(filters.get("warehouse"))) + + if filters.get("company"): + query = query.where(wh.company == filters.get("company")) + + return query.run(as_dict=True) def get_chart_data(data): From 273ed40cfb0c629599156c165dbd5a798a3892f6 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 20 Sep 2022 10:46:28 +0530 Subject: [PATCH 45/54] test: add test cases for `Item Shortage Report` (cherry picked from commit 3dc754cac2f21cd738a896e6bb3bf9d54be1d6b1) --- .../test_item_shortage_report.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 erpnext/stock/report/item_shortage_report/test_item_shortage_report.py diff --git a/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py b/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py new file mode 100644 index 00000000000..5884c32acc7 --- /dev/null +++ b/erpnext/stock/report/item_shortage_report/test_item_shortage_report.py @@ -0,0 +1,51 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.report.item_shortage_report.item_shortage_report import ( + execute as item_shortage_report, +) + + +class TestItemShortageReport(FrappeTestCase): + def test_item_shortage_report(self): + item = make_item().name + so = make_sales_order(item_code=item) + + reserved_qty, projected_qty = frappe.db.get_value( + "Bin", + { + "item_code": item, + "warehouse": so.items[0].warehouse, + }, + ["reserved_qty", "projected_qty"], + ) + self.assertEqual(reserved_qty, so.items[0].qty) + self.assertEqual(projected_qty, -(so.items[0].qty)) + + filters = { + "company": so.company, + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertIn(item, item_code_list) + + filters = { + "company": so.company, + "warehouse": [so.items[0].warehouse], + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertIn(item, item_code_list) + + filters = { + "company": so.company, + "warehouse": ["Work In Progress - _TC"], + } + report_data = item_shortage_report(filters)[1] + item_code_list = [row.get("item_code") for row in report_data] + self.assertNotIn(item, item_code_list) From e8076629fa281b8a7666deb9e595be2ebfe7d6c4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Sep 2022 20:05:20 +0530 Subject: [PATCH 46/54] fix: Add child table for tax withheld vouchers (cherry picked from commit 246c1a9380238212b821951db6ed3e8ef88d9922) --- .../purchase_invoice/purchase_invoice.json | 21 ++++++-- .../doctype/tax_withheld_vouchers/__init__.py | 0 .../tax_withheld_vouchers.json | 48 +++++++++++++++++++ .../tax_withheld_vouchers.py | 9 ++++ 4 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py create mode 100644 erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json create mode 100644 erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 534b879e783..1d596514058 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -93,6 +93,8 @@ "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", + "tax_withheld_vouchers_section", + "tax_withheld_vouchers", "section_break_44", "apply_discount_on", "base_discount_amount", @@ -1367,7 +1369,7 @@ "width": "50px" }, { - "depends_on": "eval:doc.is_subcontracted", + "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", @@ -1426,13 +1428,25 @@ "hidden": 1, "label": "Is Old Subcontracting Flow", "read_only": 1 - } + }, + { + "fieldname": "tax_withheld_vouchers_section", + "fieldtype": "Section Break", + "label": "Tax Withheld Vouchers" + }, + { + "fieldname": "tax_withheld_vouchers", + "fieldtype": "Table", + "label": "Tax Withheld Vouchers", + "options": "Tax Withheld Vouchers", + "read_only": 1 + } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:40:58.527065", + "modified": "2022-09-13 16:22:04.103982", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1492,6 +1506,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title", "track_changes": 1 diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py b/erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json new file mode 100644 index 00000000000..cecc6fb20ab --- /dev/null +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2022-09-13 16:18:59.404842", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_name", + "taxable_amount" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher Name", + "options": "voucher_type" + }, + { + "fieldname": "taxable_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Taxable Amount" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-09-13 17:31:52.321034", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Withheld Vouchers", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py new file mode 100644 index 00000000000..ea54c5403a8 --- /dev/null +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class TaxWithheldVouchers(Document): + pass From f759c29d5576c8288872e000dfb12de54e2d889a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Sep 2022 20:31:31 +0530 Subject: [PATCH 47/54] fix: Fetch vouchers to show in Invoice (cherry picked from commit 3fb1595a4ecdaeac4a6d85be1d8bfa4a62f181d8) --- .../purchase_invoice/purchase_invoice.py | 15 +++- .../tax_withholding_category.py | 72 +++++++++---------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index fea81e9c272..d1853002891 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1519,7 +1519,7 @@ class PurchaseInvoice(BuyingController): if not self.tax_withholding_category: return - tax_withholding_details, advance_taxes = get_party_tax_withholding_details( + tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details( self, self.tax_withholding_category ) @@ -1548,6 +1548,19 @@ class PurchaseInvoice(BuyingController): for d in to_remove: self.remove(d) + ## Add pending vouchers on which tax was withheld + self.set("tax_withheld_vouchers", []) + + for voucher_no, voucher_details in voucher_wise_amount.items(): + self.append( + "tax_withheld_vouchers", + { + "voucher_name": voucher_no, + "voucher_type": voucher_details.get("voucher_type"), + "taxable_amount": voucher_details.get("amount"), + }, + ) + # calculate totals again after applying TDS self.calculate_taxes_and_totals() diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 6004e2b19b2..f2fa77004c1 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -109,7 +109,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): ).format(tax_withholding_category, inv.company, party) ) - tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount( + tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount( party_type, parties, inv, tax_details, posting_date, pan_no ) @@ -119,7 +119,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) if inv.doctype == "Purchase Invoice": - return tax_row, tax_deducted_on_advances + return tax_row, tax_deducted_on_advances, voucher_wise_amount else: return tax_row @@ -217,7 +217,9 @@ def get_lower_deduction_certificate(tax_details, pan_no): def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None): - vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type) + vouchers, voucher_wise_amount = get_invoice_vouchers( + parties, tax_details, inv.company, party_type=party_type + ) advance_vouchers = get_advance_vouchers( parties, company=inv.company, @@ -236,6 +238,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_deducted = get_deducted_tax(taxable_vouchers, tax_details) tax_amount = 0 + if party_type == "Supplier": ldc = get_lower_deduction_certificate(tax_details, pan_no) if tax_deducted: @@ -261,12 +264,13 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if cint(tax_details.round_off_tax_amount): tax_amount = round(tax_amount) - return tax_amount, tax_deducted, tax_deducted_on_advances + return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): - dr_or_cr = "credit" if party_type == "Supplier" else "debit" doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" + voucher_wise_amount = {} + vouchers = [] filters = { "company": company, @@ -281,29 +285,42 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} ) - invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""] + invoices_details = frappe.get_all( + doctype, filters=filters, fields=["name", "base_net_total"] + ) or [""] - journal_entries = frappe.db.sql( + for d in invoices_details: + vouchers.append(d.name) + voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}}) + + journal_entries_details = frappe.db.sql( """ - SELECT j.name + SELECT j.name, ja.credit - ja.debit AS amount FROM `tabJournal Entry` j, `tabJournal Entry Account` ja WHERE - j.docstatus = 1 + j.name = ja.parent + AND j.docstatus = 1 AND j.is_opening = 'No' AND j.posting_date between %s and %s - AND ja.{dr_or_cr} > 0 AND ja.party in %s - """.format( - dr_or_cr=dr_or_cr + AND j.apply_tds = 1 + AND j.tax_withholding_category = %s + """, + ( + tax_details.from_date, + tax_details.to_date, + tuple(parties), + tax_details.get("tax_withholding_category"), ), - (tax_details.from_date, tax_details.to_date, tuple(parties)), - as_list=1, + as_dict=1, ) - if journal_entries: - journal_entries = journal_entries[0] + if journal_entries_details: + for d in journal_entries_details: + vouchers.append(d.name) + voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}}) - return invoices + journal_entries + return vouchers, voucher_wise_amount def get_advance_vouchers( @@ -394,11 +411,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): supp_credit_amt += supp_jv_credit_amt supp_credit_amt += inv.net_total - debit_note_amount = get_debit_note_amount( - parties, tax_details.from_date, tax_details.to_date, inv.company - ) - supp_credit_amt -= debit_note_amount - threshold = tax_details.get("threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0) @@ -515,22 +527,6 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net return tds_amount -def get_debit_note_amount(suppliers, from_date, to_date, company=None): - - filters = { - "supplier": ["in", suppliers], - "is_return": 1, - "docstatus": 1, - "posting_date": ["between", (from_date, to_date)], - } - fields = ["abs(sum(net_total)) as net_total"] - - if company: - filters["company"] = company - - return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0 - - def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): if current_amount < (certificate_limit - deducted_amount): return current_amount * rate / 100 From 27fdd41a6e2affea48c56ae18fbeb1a2b9410b61 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 14 Sep 2022 09:13:02 +0530 Subject: [PATCH 48/54] test: Add tests (cherry picked from commit b6184ce4715111add6dad3acf839097639cb4b51) --- .../purchase_invoice/purchase_invoice.json | 6 +- .../tax_withheld_vouchers.json | 5 +- .../tax_withholding_category.py | 4 +- .../test_tax_withholding_category.py | 68 ++++++++++++++++++- erpnext/www/lms/__init__.py | 0 5 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 erpnext/www/lms/__init__.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 1d596514058..1eeaf13abc9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -83,6 +83,8 @@ "section_break_51", "taxes_and_charges", "taxes", + "tax_withheld_vouchers_section", + "tax_withheld_vouchers", "sec_tax_breakup", "other_charges_calculation", "totals", @@ -93,8 +95,6 @@ "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", - "tax_withheld_vouchers_section", - "tax_withheld_vouchers", "section_break_44", "apply_discount_on", "base_discount_amount", @@ -1446,7 +1446,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-09-13 16:22:04.103982", + "modified": "2022-09-13 23:39:54.525037", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json index cecc6fb20ab..ce8c0c37086 100644 --- a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json @@ -29,13 +29,14 @@ "fieldname": "taxable_amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Taxable Amount" + "label": "Taxable Amount", + "options": "Company:company:default_currency" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-09-13 17:31:52.321034", + "modified": "2022-09-13 23:40:41.479208", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withheld Vouchers", diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index f2fa77004c1..15f75d15103 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -285,9 +285,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} ) - invoices_details = frappe.get_all( - doctype, filters=filters, fields=["name", "base_net_total"] - ) or [""] + invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"]) for d in invoices_details: vouchers.append(d.name) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 3059f8d64b8..5c031a99542 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -148,7 +148,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(tcs_charged, 500) invoices.append(si) - # delete invoices to avoid clashing + # cancel invoices to avoid clashing for d in invoices: d.cancel() @@ -182,7 +182,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 4000) - # delete invoices to avoid clashing + # cancel invoices to avoid clashing for d in invoices: d.cancel() @@ -207,10 +207,52 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 250) - # delete invoices to avoid clashing + # cancel invoices to avoid clashing for d in invoices: d.cancel() + def test_tax_withholding_category_voucher_display(self): + frappe.db.set_value( + "Supplier", "Test TDS Supplier6", "tax_withholding_category", "Test Multi Invoice Category" + ) + invoices = [] + + pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True) + pi.apply_tds = 1 + pi.tax_withholding_category = "Test Multi Invoice Category" + pi.save() + pi.submit() + invoices.append(pi) + + pi1 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=2000, do_not_save=True) + pi1.apply_tds = 1 + pi1.is_return = 1 + pi1.items[0].qty = -1 + pi1.tax_withholding_category = "Test Multi Invoice Category" + pi1.save() + pi1.submit() + invoices.append(pi1) + + pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True) + pi2.apply_tds = 1 + pi2.tax_withholding_category = "Test Multi Invoice Category" + pi2.save() + pi2.submit() + invoices.append(pi2) + + pi2.load_from_db() + + self.assertTrue(pi2.taxes[0].tax_amount, 1100) + + self.assertTrue(pi2.tax_withheld_vouchers[0].voucher_name == pi1.name) + self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total) + self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name) + self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total) + + # cancel invoices to avoid clashing + for d in reversed(invoices): + d.cancel() + def cancel_invoices(): purchase_invoices = frappe.get_all( @@ -308,6 +350,7 @@ def create_records(): "Test TDS Supplier3", "Test TDS Supplier4", "Test TDS Supplier5", + "Test TDS Supplier6", ]: if frappe.db.exists("Supplier", name): continue @@ -498,3 +541,22 @@ def create_tax_with_holding_category(): "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], } ).insert() + + if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"): + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Test Multi Invoice Category", + "category_name": "Test Multi Invoice Category", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 5000, + "cumulative_threshold": 10000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() diff --git a/erpnext/www/lms/__init__.py b/erpnext/www/lms/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From eba46dae6c4c03d2fe4de8998039a0e64b3a9d8f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 16 Sep 2022 13:50:37 +0530 Subject: [PATCH 49/54] fix: TDS deduction via journal entry (cherry picked from commit 36d0906ea26b65b9a4111439e2046bcf16305273) --- .../doctype/journal_entry/journal_entry.py | 4 ++- .../tax_withholding_category.py | 30 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 63c6547f1d4..52690e1e662 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -184,7 +184,9 @@ class JournalEntry(AccountsController): } ) - tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category) + tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details( + inv, self.tax_withholding_category + ) if not tax_withholding_details: return diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 15f75d15103..0b5df9e0cc0 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -344,23 +344,25 @@ def get_advance_vouchers( def get_taxes_deducted_on_advances_allocated(inv, tax_details): - advances = [d.reference_name for d in inv.get("advances")] tax_info = [] - if advances: - pe = frappe.qb.DocType("Payment Entry").as_("pe") - at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + if inv.get("advances"): + advances = [d.reference_name for d in inv.get("advances")] - tax_info = ( - frappe.qb.from_(at) - .inner_join(pe) - .on(pe.name == at.parent) - .select(at.parent, at.name, at.tax_amount, at.allocated_amount) - .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category")) - .where(at.parent.isin(advances)) - .where(at.account_head == tax_details.account_head) - .run(as_dict=True) - ) + if advances: + pe = frappe.qb.DocType("Payment Entry").as_("pe") + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + tax_info = ( + frappe.qb.from_(at) + .inner_join(pe) + .on(pe.name == at.parent) + .select(at.parent, at.name, at.tax_amount, at.allocated_amount) + .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category")) + .where(at.parent.isin(advances)) + .where(at.account_head == tax_details.account_head) + .run(as_dict=True) + ) return tax_info From 302f2d10e2ae817512d14d1afac8827f831132b1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Sep 2022 09:06:18 +0530 Subject: [PATCH 50/54] chore: fix tests (cherry picked from commit 9aa1f84d4578901902bfdb3889c571fced8861c1) --- .../test_tax_withholding_category.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 5c031a99542..e80fe11ab30 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -52,7 +52,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): invoices.append(pi) # delete invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_single_threshold_tds(self): @@ -88,7 +88,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi.taxes_and_charges_deducted, 1000) # delete invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_tax_withholding_category_checks(self): @@ -114,7 +114,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): # TDS should be applied only on 1000 self.assertEqual(pi1.taxes[0].tax_amount, 1000) - for d in invoices: + for d in reversed(invoices): d.cancel() def test_cumulative_threshold_tcs(self): @@ -149,7 +149,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): invoices.append(si) # cancel invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_tds_calculation_on_net_total(self): @@ -183,7 +183,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 4000) # cancel invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_multi_category_single_supplier(self): @@ -208,7 +208,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 250) # cancel invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_tax_withholding_category_voucher_display(self): From 4939153f8c7e0d1a1488d6a698050d2ca147b4fa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 18 Sep 2022 19:41:05 +0530 Subject: [PATCH 51/54] fix: Depreciation posting date only when calculate depreciation is checked (cherry picked from commit fac82cf69be487f4bfba953c722700fcc515b044) --- erpnext/assets/doctype/asset/asset.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f414930d722..a43a16c9ec5 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -239,8 +239,10 @@ frappe.ui.form.on('Asset', { item_code: function(frm) { - if(frm.doc.item_code) { + if(frm.doc.item_code && frm.doc.calculate_depreciation) { frm.trigger('set_finance_book'); + } else { + frm.set_value('finance_books', []); } }, @@ -381,6 +383,11 @@ frappe.ui.form.on('Asset', { calculate_depreciation: function(frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); + if (frm.doc.item_code && frm.doc.calculate_depreciation ) { + frm.trigger("set_finance_book"); + } else { + frm.set_value("finance_books", []); + } }, gross_purchase_amount: function(frm) { From c0003195b8e097b32662cb3491b6d6d80f52a86b Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 16 Sep 2022 18:26:00 +0530 Subject: [PATCH 52/54] fix: remove no_copy for ignore_pricing_rule (cherry picked from commit 8c5b420aea221b001ea8537a8391654699757586) --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 4 ++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 3 +-- erpnext/buying/doctype/purchase_order/purchase_order.json | 3 +-- erpnext/selling/doctype/quotation/quotation.json | 3 +-- erpnext/selling/doctype/sales_order/sales_order.json | 3 +-- erpnext/stock/doctype/delivery_note/delivery_note.json | 3 +-- erpnext/stock/doctype/purchase_receipt/purchase_receipt.json | 3 +-- 7 files changed, 8 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 534b879e783..a93965e2a54 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -512,7 +512,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1432,7 +1431,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:40:58.527065", + "modified": "2022-09-16 17:45:25.345996", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1492,6 +1491,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title", "track_changes": 1 diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 1c9d3fbfb2d..2da515737a9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -649,7 +649,6 @@ "hide_days": 1, "hide_seconds": 1, "label": "Ignore Pricing Rule", - "no_copy": 1, "print_hide": 1 }, { @@ -2022,7 +2021,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-07-11 17:43:56.435382", + "modified": "2022-09-16 17:44:22.227332", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index acca380672d..fb8f25a0dfc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -441,7 +441,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1180,7 +1179,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-09-07 11:06:46.035093", + "modified": "2022-09-16 17:45:04.954055", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index bb2f95dd173..c58a46ba513 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -402,7 +402,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -986,7 +985,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-06-11 20:35:32.635804", + "modified": "2022-09-16 17:44:43.221804", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 74c5c07e47b..ff269d0e684 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -544,7 +544,6 @@ "hide_days": 1, "hide_seconds": 1, "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1549,7 +1548,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-06-10 03:52:22.212953", + "modified": "2022-09-16 17:43:57.007441", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index f9e934921d8..a8f907ed711 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -490,7 +490,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1336,7 +1335,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2022-06-10 03:52:04.197415", + "modified": "2022-09-16 17:46:17.701904", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index a70415dfc36..acaac920c9e 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -405,7 +405,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1158,7 +1157,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:43:40.664382", + "modified": "2022-09-16 17:45:58.430132", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From 181dccd8d85c2b54f68f3668078efd40e511b15b Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 20 Sep 2022 16:18:59 +0530 Subject: [PATCH 53/54] refactor: rewrite `BOM Stock Report` queries in `QB` (cherry picked from commit 8fd7c04920ddfb942451ec9151db36225f86f83b) --- .../bom_stock_report/bom_stock_report.py | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 34e9826305e..1e1b4356008 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -4,6 +4,8 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Floor, Sum +from pypika.terms import ExistsCriterion def execute(filters=None): @@ -11,7 +13,6 @@ def execute(filters=None): filters = {} columns = get_columns() - data = get_bom_stock(filters) return columns, data @@ -33,59 +34,57 @@ def get_columns(): def get_bom_stock(filters): - conditions = "" - bom = filters.get("bom") - - table = "`tabBOM Item`" - qty_field = "stock_qty" - - qty_to_produce = filters.get("qty_to_produce", 1) - if int(qty_to_produce) <= 0: + qty_to_produce = filters.get("qty_to_produce") or 1 + if int(qty_to_produce) < 0: frappe.throw(_("Quantity to Produce can not be less than Zero")) if filters.get("show_exploded_view"): - table = "`tabBOM Explosion Item`" + bom_item_table = "BOM Explosion Item" + else: + bom_item_table = "BOM Item" + + bin = frappe.qb.DocType("Bin") + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType(bom_item_table) + + query = ( + frappe.qb.from_(bom) + .inner_join(bom_item) + .on(bom.name == bom_item.parent) + .left_join(bin) + .on(bom_item.item_code == bin.item_code) + .select( + bom_item.item_code, + bom_item.description, + bom_item.stock_qty, + bom_item.stock_uom, + bom_item.stock_qty * qty_to_produce / bom.quantity, + Sum(bin.actual_qty).as_("actual_qty"), + Sum(Floor(bin.actual_qty / (bom_item.stock_qty * qty_to_produce / bom.quantity))), + ) + .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) + .groupby(bom_item.item_code) + ) if filters.get("warehouse"): warehouse_details = frappe.db.get_value( "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) + if warehouse_details: - conditions += ( - " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where( + (wh.lft >= warehouse_details.lft) + & (wh.rgt <= warehouse_details.rgt) + & (bin.warehouse == wh.name) + ) + ) ) else: - conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse")) + query = query.where(bin.warehouse == filters.get("warehouse")) - else: - conditions += "" - - return frappe.db.sql( - """ - SELECT - bom_item.item_code, - bom_item.description , - bom_item.{qty_field}, - bom_item.stock_uom, - bom_item.{qty_field} * {qty_to_produce} / bom.quantity, - sum(ledger.actual_qty) as actual_qty, - sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) - FROM - `tabBOM` AS bom INNER JOIN {table} AS bom_item - ON bom.name = bom_item.parent - LEFT JOIN `tabBin` AS ledger - ON bom_item.item_code = ledger.item_code - {conditions} - WHERE - bom_item.parent = {bom} and bom_item.parenttype='BOM' - - GROUP BY bom_item.item_code""".format( - qty_field=qty_field, - table=table, - conditions=conditions, - bom=frappe.db.escape(bom), - qty_to_produce=qty_to_produce or 1, - ) - ) + return query.run() From ed9a896f7294138879756cf5d539d0f999f0cb92 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 20 Sep 2022 16:25:20 +0530 Subject: [PATCH 54/54] fix: warehouse filter in `BOM Stock Calculated Report` (cherry picked from commit 390ce5719d290db38172aef69f542a862a662090) --- .../report/bom_stock_calculated/bom_stock_calculated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index ec4b25c859f..550445c1f77 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -146,7 +146,7 @@ def get_bom_data(filters): ) ) else: - query = query.where(bin.warehouse == frappe.db.escape(filters.get("warehouse"))) + query = query.where(bin.warehouse == filters.get("warehouse")) return query.run(as_dict=True)