diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 73aae33e936..d70977c07e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,8 +32,8 @@ repos: - id: black additional_dependencies: ['click==8.0.4'] - - repo: https://github.com/timothycrosley/isort - rev: 5.9.1 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - id: isort exclude: ".*setup.py$" diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 73e673ea38b..a8b8ec6d12e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -87,6 +87,7 @@ class JournalEntry(AccountsController): self.check_credit_limit() self.make_gl_entries() self.update_advance_paid() + self.update_asset_value() self.update_expense_claim() self.update_inter_company_jv() self.update_invoice_discounting() @@ -235,6 +236,34 @@ class JournalEntry(AccountsController): for d in to_remove: self.remove(d) + def update_asset_value(self): + if self.voucher_type != "Depreciation Entry": + return + + processed_assets = [] + + for d in self.get("accounts"): + if ( + d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets + ): + processed_assets.append(d.reference_name) + + asset = frappe.db.get_value( + "Asset", d.reference_name, ["calculate_depreciation", "value_after_depreciation"], as_dict=1 + ) + + if asset.calculate_depreciation: + continue + + depr_value = d.debit or d.credit + + frappe.db.set_value( + "Asset", + d.reference_name, + "value_after_depreciation", + asset.value_after_depreciation - depr_value, + ) + def update_inter_company_jv(self): if ( self.voucher_type == "Inter Company Journal Entry" @@ -293,19 +322,39 @@ class JournalEntry(AccountsController): d.db_update() def unlink_asset_reference(self): + if self.voucher_type != "Depreciation Entry": + return + + processed_assets = [] + for d in self.get("accounts"): - if d.reference_type == "Asset" and d.reference_name: + if ( + d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets + ): + processed_assets.append(d.reference_name) + asset = frappe.get_doc("Asset", d.reference_name) - for s in asset.get("schedules"): - if s.journal_entry == self.name: - s.db_set("journal_entry", None) - idx = cint(s.finance_book_id) or 1 - finance_books = asset.get("finance_books")[idx - 1] - finance_books.value_after_depreciation += s.depreciation_amount - finance_books.db_update() + if asset.calculate_depreciation: + for s in asset.get("schedules"): + if s.journal_entry == self.name: + s.db_set("journal_entry", None) - asset.set_status() + idx = cint(s.finance_book_id) or 1 + finance_books = asset.get("finance_books")[idx - 1] + finance_books.value_after_depreciation += s.depreciation_amount + finance_books.db_update() + + asset.set_status() + else: + depr_value = d.debit or d.credit + + frappe.db.set_value( + "Asset", + d.reference_name, + "value_after_depreciation", + asset.value_after_depreciation + depr_value, + ) def unlink_inter_company_jv(self): if ( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 70a5f9e526c..f4367cdafd6 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1255,6 +1255,7 @@ def get_outstanding_reference_documents(args): args.get("party_type"), args.get("party"), args.get("party_account"), + args.get("company"), filters=args, condition=condition, ) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 1e75471c848..1e99e4d791c 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -211,7 +211,7 @@ class PaymentReconciliation(Document): condition += " and cost_center = '{0}' ".format(self.cost_center) non_reconciled_invoices = get_outstanding_invoices( - self.party_type, self.party, self.receivable_payable_account, condition=condition + self.party_type, self.party, self.receivable_payable_account, self.company, condition=condition ) if self.invoice_limit: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index cd3434a7756..c0b1c596007 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1790,6 +1790,8 @@ "width": "50%" }, { + "fetch_from": "sales_partner.commission_rate", + "fetch_if_empty": 1, "fieldname": "commission_rate", "fieldtype": "Float", "hide_days": 1, @@ -2045,7 +2047,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-09-16 17:44:22.227332", + "modified": "2023-01-28 19:45:47.538163", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2101,4 +2103,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 6bd3ee5aa6b..77bc82590b8 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -585,10 +585,35 @@ class GrossProfitGenerator(object): return self.calculate_buying_amount_from_sle( row, my_sle, parenttype, parent, item_row, item_code ) + elif row.sales_order and row.so_detail: + incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code) + if incoming_amount: + return incoming_amount else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) - return 0.0 + return flt(row.qty) * self.get_average_buying_rate(row, item_code) + + def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code): + from frappe.query_builder.functions import Sum + + delivery_note = frappe.qb.DocType("Delivery Note") + delivery_note_item = frappe.qb.DocType("Delivery Note Item") + + query = ( + frappe.qb.from_(delivery_note) + .inner_join(delivery_note_item) + .on(delivery_note.name == delivery_note_item.parent) + .select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) + .where(delivery_note.docstatus == 1) + .where(delivery_note_item.item_code == item_code) + .where(delivery_note_item.against_sales_order == sales_order) + .where(delivery_note_item.so_detail == so_detail) + .groupby(delivery_note_item.item_code) + ) + + incoming_amount = query.run() + return flt(incoming_amount[0][0]) if incoming_amount else 0 def get_average_buying_rate(self, row, item_code): args = row @@ -665,7 +690,8 @@ class GrossProfitGenerator(object): `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, - `tabSales Invoice Item`.brand, `tabSales Invoice Item`.dn_detail, + `tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail, + `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return, diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index d9febb74fd4..06a173ee35a 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -301,3 +301,82 @@ class TestGrossProfit(FrappeTestCase): columns, data = execute(filters=filters) self.assertGreater(len(data), 0) + + def test_order_connected_dn_and_inv(self): + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + """ + Test gp calculation when invoice and delivery note aren't directly connected. + SO -- INV + | + DN + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=3, + basic_rate=100, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": item.item_code, + "s_warehouse": item.s_warehouse, + "t_warehouse": item.t_warehouse, + "qty": 10, + "basic_rate": 200, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + so = make_sales_order( + customer=self.customer, + company=self.company, + warehouse=self.warehouse, + item=self.item, + qty=4, + do_not_save=False, + do_not_submit=False, + ) + + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + + make_delivery_note(so.name).submit() + sinv = make_sales_invoice(so.name).submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 4.0, + "avg._selling_rate": 100.0, + "valuation_rate": 125.0, + "selling_amount": 400.0, + "buying_amount": 500.0, + "gross_profit": -100.0, + "gross_profit_%": -25.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry, gp_entry[0]) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b0b217684ea..faacc913a41 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -840,7 +840,7 @@ def remove_return_pos_invoices(party_type, party, invoice_list): return invoice_list -def get_outstanding_invoices(party_type, party, account, condition=None, filters=None): +def get_outstanding_invoices(party_type, party, account, company, condition=None, filters=None): outstanding_invoices = [] precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 @@ -892,61 +892,73 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters invoice_list = remove_return_pos_invoices(party_type, party, invoice_list) - payment_entries = frappe.db.sql( - """ - select against_voucher_type, against_voucher, - ifnull(sum({payment_dr_or_cr}), 0) as payment_amount - from `tabGL Entry` - where party_type = %(party_type)s and party = %(party)s - and account = %(account)s - and {payment_dr_or_cr} > 0 - and against_voucher is not null and against_voucher != '' - and is_cancelled=0 - group by against_voucher_type, against_voucher - """.format( - payment_dr_or_cr=payment_dr_or_cr - ), - {"party_type": party_type, "party": party, "account": account}, - as_dict=True, - ) + if invoice_list: + invoices = [d.voucher_no for d in invoice_list] + payment_entries = frappe.db.sql( + """ + select against_voucher_type, against_voucher, + ifnull(sum({payment_dr_or_cr}), 0) as payment_amount + from `tabGL Entry` + where + company = %(company)s + and party_type = %(party_type)s and party = %(party)s + and account = %(account)s + and {payment_dr_or_cr} > 0 + and ifnull(against_voucher, '') != '' + and is_cancelled=0 + and against_voucher in %(invoices)s + group by against_voucher_type, against_voucher + """.format( + payment_dr_or_cr=payment_dr_or_cr, + ), + { + "company": company, + "party_type": party_type, + "party": party, + "account": account, + "invoices": invoices, + }, + as_dict=True, + ) - pe_map = frappe._dict() - for d in payment_entries: - pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount) + pe_map = frappe._dict() + for d in payment_entries: + pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount) - for d in invoice_list: - payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0) - outstanding_amount = flt(d.invoice_amount - payment_amount, precision) - if outstanding_amount > 0.5 / (10**precision): - if ( - filters - and filters.get("outstanding_amt_greater_than") - and not ( - outstanding_amount >= filters.get("outstanding_amt_greater_than") - and outstanding_amount <= filters.get("outstanding_amt_less_than") - ) - ): - continue - - if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices: - outstanding_invoices.append( - frappe._dict( - { - "voucher_no": d.voucher_no, - "voucher_type": d.voucher_type, - "posting_date": d.posting_date, - "invoice_amount": flt(d.invoice_amount), - "payment_amount": payment_amount, - "outstanding_amount": outstanding_amount, - "due_date": d.due_date, - "currency": d.currency, - } + for d in invoice_list: + payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0) + outstanding_amount = flt(d.invoice_amount - payment_amount, precision) + if outstanding_amount > 0.5 / (10**precision): + if ( + filters + and filters.get("outstanding_amt_greater_than") + and not ( + outstanding_amount >= filters.get("outstanding_amt_greater_than") + and outstanding_amount <= filters.get("outstanding_amt_less_than") ) - ) + ): + continue + + if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices: + outstanding_invoices.append( + frappe._dict( + { + "voucher_no": d.voucher_no, + "voucher_type": d.voucher_type, + "posting_date": d.posting_date, + "invoice_amount": flt(d.invoice_amount), + "payment_amount": payment_amount, + "outstanding_amount": outstanding_amount, + "due_date": d.due_date, + "currency": d.currency, + } + ) + ) + + outstanding_invoices = sorted( + outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate()) + ) - outstanding_invoices = sorted( - outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate()) - ) return outstanding_invoices diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 587cb147b79..a4a9d6c8bc8 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -201,7 +201,7 @@ frappe.ui.form.on('Asset', { }) }, - setup_chart: function(frm) { + setup_chart: async function(frm) { if(frm.doc.finance_books.length > 1) { return } @@ -219,20 +219,34 @@ frappe.ui.form.on('Asset', { flt(frm.doc.opening_accumulated_depreciation)); } - $.each(frm.doc.schedules || [], function(i, v) { - x_intervals.push(v.schedule_date); - var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount); - if(v.journal_entry) { - last_depreciation_date = v.schedule_date; - asset_values.push(asset_value); - } else { - if (in_list(["Scrapped", "Sold"], frm.doc.status)) { - asset_values.push(null); + if(frm.doc.calculate_depreciation) { + $.each(frm.doc.schedules || [], function(i, v) { + x_intervals.push(v.schedule_date); + var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount); + if(v.journal_entry) { + last_depreciation_date = v.schedule_date; + asset_values.push(asset_value); } else { - asset_values.push(asset_value) + if (in_list(["Scrapped", "Sold"], frm.doc.status)) { + asset_values.push(null); + } else { + asset_values.push(asset_value) + } } - } - }); + }); + } else { + let depr_entries = (await frappe.call({ + method: "get_manual_depreciation_entries", + doc: frm.doc, + })).message; + + $.each(depr_entries || [], function(i, v) { + x_intervals.push(v.posting_date); + last_depreciation_date = v.posting_date; + let last_asset_value = asset_values[asset_values.length - 1] + asset_values.push(last_asset_value - v.value); + }); + } if(in_list(["Scrapped", "Sold"], frm.doc.status)) { x_intervals.push(frm.doc.disposal_date); diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 8132dbd411a..511afdf0854 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -504,9 +504,15 @@ "group": "Value", "link_doctype": "Asset Value Adjustment", "link_fieldname": "asset" + }, + { + "group": "Journal Entry", + "link_doctype": "Journal Entry", + "link_fieldname": "reference_name", + "table_fieldname": "accounts" } ], - "modified": "2023-01-17 00:28:37.789345", + "modified": "2023-01-31 01:03:09.467817", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index af32f93cf05..549241b8f10 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -27,6 +27,7 @@ from erpnext.accounts.general_ledger import make_reverse_gl_entries from erpnext.assets.doctype.asset.depreciation import ( get_depreciation_accounts, get_disposal_account_and_cost_center, + is_last_day_of_the_month, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.controllers.accounts_controller import AccountsController @@ -555,7 +556,9 @@ class Asset(AccountsController): if int(d.finance_book_id) not in finance_books: accumulated_depreciation = flt(self.opening_accumulated_depreciation) - value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id)) + value_after_depreciation = flt( + self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation + ) finance_books.append(int(d.finance_book_id)) depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) @@ -580,9 +583,6 @@ class Asset(AccountsController): accumulated_depreciation, d.precision("accumulated_depreciation_amount") ) - def get_value_after_depreciation(self, idx): - return flt(self.get("finance_books")[cint(idx) - 1].value_after_depreciation) - def validate_expected_value_after_useful_life(self): for row in self.get("finance_books"): accumulated_depreciation_after_full_schedule = [ @@ -637,15 +637,20 @@ class Asset(AccountsController): movement.cancel() def delete_depreciation_entries(self): - for d in self.get("schedules"): - if d.journal_entry: - frappe.get_doc("Journal Entry", d.journal_entry).cancel() - d.db_set("journal_entry", None) + if self.calculate_depreciation: + for d in self.get("schedules"): + if d.journal_entry: + frappe.get_doc("Journal Entry", d.journal_entry).cancel() + else: + depr_entries = self.get_manual_depreciation_entries() - self.db_set( - "value_after_depreciation", - (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)), - ) + for depr_entry in depr_entries or []: + frappe.get_doc("Journal Entry", depr_entry.name).cancel() + + self.db_set( + "value_after_depreciation", + (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)), + ) def set_status(self, status=None): """Get and update status""" @@ -676,6 +681,17 @@ class Asset(AccountsController): status = "Cancelled" return status + def get_value_after_depreciation(self, finance_book=None): + if not self.calculate_depreciation: + return self.value_after_depreciation + + if not finance_book: + return self.get("finance_books")[0].value_after_depreciation + + for row in self.get("finance_books"): + if finance_book == row.finance_book: + return row.value_after_depreciation + def get_default_finance_book_idx(self): if not self.get("default_finance_book") and self.company: self.default_finance_book = erpnext.get_default_finance_book(self.company) @@ -685,6 +701,24 @@ class Asset(AccountsController): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 + @frappe.whitelist() + def get_manual_depreciation_entries(self): + (_, _, depreciation_expense_account) = get_depreciation_accounts(self) + + gle = frappe.qb.DocType("GL Entry") + + records = ( + frappe.qb.from_(gle) + .select(gle.voucher_no.as_("name"), gle.debit.as_("value"), gle.posting_date) + .where(gle.against_voucher == self.name) + .where(gle.account == depreciation_expense_account) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .orderby(gle.posting_date) + ).run(as_dict=True) + + return records + def validate_make_gl_entry(self): purchase_document = self.get_purchase_document() if not purchase_document: @@ -849,7 +883,6 @@ def update_maintenance_status(): def make_post_gl_entry(): - asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"]) for asset_category in asset_categories: @@ -1002,7 +1035,7 @@ def make_journal_entry(asset_name): depreciation_expense_account, ) = get_depreciation_accounts(asset) - depreciation_cost_center, depreciation_series = frappe.db.get_value( + depreciation_cost_center, depreciation_series = frappe.get_cached_value( "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] ) depreciation_cost_center = asset.cost_center or depreciation_cost_center @@ -1069,6 +1102,13 @@ def is_cwip_accounting_enabled(asset_category): return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) +@frappe.whitelist() +def get_asset_value_after_depreciation(asset_name, finance_book=None): + asset = frappe.get_doc("Asset", asset_name) + + return asset.get_value_after_depreciation(finance_book) + + def get_total_days(date, frequency): period_start_date = add_months(date, cint(frequency) * -1) @@ -1078,12 +1118,6 @@ def get_total_days(date, frequency): return date_diff(date, period_start_date) -def is_last_day_of_the_month(date): - last_day_of_the_month = get_last_day(date) - - return getdate(last_day_of_the_month) == getdate(date) - - @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 66dd2e98dc5..e83b9b2080a 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,7 +4,16 @@ import frappe from frappe import _ -from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today +from frappe.utils import ( + add_months, + cint, + flt, + get_last_day, + get_link_to_form, + getdate, + nowdate, + today, +) from frappe.utils.user import get_users_with_role from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -372,6 +381,9 @@ def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_da finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) ) + if is_last_day_of_the_month(finance_book.depreciation_start_date): + orginal_schedule_date = get_last_day(orginal_schedule_date) + if orginal_schedule_date == posting_date_of_disposal: return True return False @@ -457,18 +469,8 @@ def get_asset_details(asset, finance_book=None): disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center - idx = 1 - if finance_book: - for d in asset.finance_books: - if d.finance_book == finance_book: - idx = d.idx - break + value_after_depreciation = asset.get_value_after_depreciation(finance_book) - value_after_depreciation = ( - asset.finance_books[idx - 1].value_after_depreciation - if asset.calculate_depreciation - else asset.value_after_depreciation - ) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) return ( @@ -508,3 +510,9 @@ 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 + + +def is_last_day_of_the_month(date): + last_day_of_the_month = get_last_day(date) + + return getdate(last_day_of_the_month) == getdate(date) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d133cebedb8..625a45b5098 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -15,9 +15,11 @@ from frappe.utils import ( nowdate, ) +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status from erpnext.assets.doctype.asset.depreciation import ( + is_last_day_of_the_month, post_depreciation_entries, restore_asset, scrap_asset, @@ -1409,6 +1411,36 @@ class TestDepreciationBasics(AssetSetup): for i, schedule in enumerate(asset.schedules): self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date)) + def test_manual_depreciation_for_existing_asset(self): + asset = create_asset( + item_code="Macbook Pro", + is_existing_asset=1, + purchase_date="2020-01-30", + available_for_use_date="2020-01-30", + submit=1, + ) + + self.assertEqual(asset.status, "Submitted") + self.assertEqual(asset.get("value_after_depreciation"), 100000) + + jv = make_journal_entry( + "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False + ) + for d in jv.accounts: + d.reference_type = "Asset" + d.reference_name = asset.name + jv.voucher_type = "Depreciation Entry" + jv.insert() + jv.submit() + + asset.reload() + self.assertEqual(asset.get("value_after_depreciation"), 99900) + + jv.cancel() + + asset.reload() + self.assertEqual(asset.get("value_after_depreciation"), 100000) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): @@ -1529,9 +1561,3 @@ def set_depreciation_settings_in_company(): def enable_cwip_accounting(asset_category, enable=1): frappe.db.set_value("Asset Category", asset_category, "enable_cwip_accounting", enable) - - -def is_last_day_of_the_month(dt): - last_day_of_the_month = get_last_day(dt) - - return getdate(dt) == getdate(last_day_of_the_month) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 4e7cf78090b..e33e800221c 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.utils import flt, nowdate +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.test_asset import ( create_asset, create_asset_data, @@ -105,20 +106,20 @@ class TestAssetRepair(unittest.TestCase): def test_increase_in_asset_value_due_to_stock_consumption(self): asset = create_asset(calculate_depreciation=1, submit=1) - initial_asset_value = get_asset_value(asset) + initial_asset_value = get_asset_value_after_depreciation(asset.name) asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1) asset.reload() - increase_in_asset_value = get_asset_value(asset) - initial_asset_value + increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): asset = create_asset(calculate_depreciation=1, submit=1) - initial_asset_value = get_asset_value(asset) + initial_asset_value = get_asset_value_after_depreciation(asset.name) asset_repair = create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) asset.reload() - increase_in_asset_value = get_asset_value(asset) - initial_asset_value + increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) def test_purchase_invoice(self): @@ -143,10 +144,6 @@ class TestAssetRepair(unittest.TestCase): ) -def get_asset_value(asset): - return asset.finance_books[0].value_after_depreciation - - def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index 36f510b18ee..ae0e1bda020 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -47,7 +47,7 @@ frappe.ui.form.on('Asset Value Adjustment', { set_current_asset_value: function(frm) { if (frm.doc.asset) { frm.call({ - method: "erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment.get_current_asset_value", + method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation", args: { asset: frm.doc.asset, finance_book: frm.doc.finance_book diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 9953c61a811..710c6cfacf3 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -10,7 +10,10 @@ from frappe.utils import cint, date_diff, flt, formatdate, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.assets.doctype.asset.asset import get_depreciation_amount +from erpnext.assets.doctype.asset.asset import ( + get_asset_value_after_depreciation, + get_depreciation_amount, +) from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.regional.india.utils import ( get_depreciation_amount as get_depreciation_amount_for_india, @@ -45,7 +48,7 @@ class AssetValueAdjustment(Document): def set_current_asset_value(self): if not self.current_asset_value and self.asset: - self.current_asset_value = get_current_asset_value(self.asset, self.finance_book) + self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book) def make_depreciation_entry(self): asset = frappe.get_doc("Asset", self.asset) @@ -148,12 +151,3 @@ class AssetValueAdjustment(Document): for asset_data in asset.schedules: if not asset_data.journal_entry: asset_data.db_update() - - -@frappe.whitelist() -def get_current_asset_value(asset, finance_book=None): - cond = {"parent": asset, "parenttype": "Asset"} - if finance_book: - cond.update({"finance_book": finance_book}) - - return frappe.db.get_value("Asset Finance Book", cond, "value_after_depreciation") diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index ebeb174d135..c79443b3a25 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -6,10 +6,8 @@ import unittest import frappe from frappe.utils import add_days, get_last_day, nowdate +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.test_asset import create_asset_data -from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( - get_current_asset_value, -) from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -43,7 +41,7 @@ class TestAssetValueAdjustment(unittest.TestCase): ) asset_doc.submit() - current_value = get_current_asset_value(asset_doc.name) + current_value = get_asset_value_after_depreciation(asset_doc.name) self.assertEqual(current_value, 100000.0) def test_asset_depreciation_value_adjustment(self): @@ -73,7 +71,7 @@ class TestAssetValueAdjustment(unittest.TestCase): ) asset_doc.submit() - current_value = get_current_asset_value(asset_doc.name) + current_value = get_asset_value_after_depreciation(asset_doc.name) adj_doc = make_asset_value_adjustment( asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 ) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index dd5dfca8a22..5bfd4840d73 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -4,13 +4,16 @@ import frappe from frappe import _ -from frappe.utils import cstr, flt, formatdate, getdate +from frappe.query_builder.functions import Sum +from frappe.utils import cstr, formatdate, getdate from erpnext.accounts.report.financial_statements import ( get_fiscal_year_data, get_period_list, validate_fiscal_year, ) +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation +from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts def execute(filters=None): @@ -85,6 +88,7 @@ def get_data(filters): "asset_name", "status", "department", + "company", "cost_center", "calculate_depreciation", "purchase_receipt", @@ -98,8 +102,25 @@ def get_data(filters): ] assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) + finance_book_filter = ("is", "not set") + if filters.finance_book: + finance_book_filter = ("=", filters.finance_book) + + assets_linked_to_fb = frappe.db.get_all( + doctype="Asset Finance Book", + filters={"finance_book": finance_book_filter}, + pluck="parent", + ) + for asset in assets_record: - asset_value = get_asset_value(asset, filters.finance_book) + if filters.finance_book: + if asset.asset_id not in assets_linked_to_fb: + continue + else: + if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb: + continue + + asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book) row = { "asset_id": asset.asset_id, "asset_name": asset.asset_name, @@ -110,7 +131,7 @@ def get_data(filters): or pi_supplier_map.get(asset.purchase_invoice), "gross_purchase_amount": asset.gross_purchase_amount, "opening_accumulated_depreciation": asset.opening_accumulated_depreciation, - "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, + "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters), "available_for_use_date": asset.available_for_use_date, "location": asset.location, "asset_category": asset.asset_category, @@ -122,21 +143,6 @@ def get_data(filters): return data -def get_asset_value(asset, finance_book=None): - if not asset.calculate_depreciation: - return flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - - finance_book_filter = ["finance_book", "is", "not set"] - if finance_book: - finance_book_filter = ["finance_book", "=", finance_book] - - return frappe.db.get_value( - doctype="Asset Finance Book", - filters=[["parent", "=", asset.asset_id], finance_book_filter], - fieldname="value_after_depreciation", - ) - - def prepare_chart_data(data, filters): labels_values_map = {} date_field = frappe.scrub(filters.date_based_on) @@ -182,6 +188,15 @@ def prepare_chart_data(data, filters): } +def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters): + if asset.calculate_depreciation: + depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0 + else: + depr_amount = get_manual_depreciation_amount_of_asset(asset, filters) + + return depr_amount + + def get_finance_book_value_map(filters): date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date @@ -201,6 +216,31 @@ def get_finance_book_value_map(filters): ) +def get_manual_depreciation_amount_of_asset(asset, filters): + date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date + + (_, _, depreciation_expense_account) = get_depreciation_accounts(asset) + + gle = frappe.qb.DocType("GL Entry") + + result = ( + frappe.qb.from_(gle) + .select(Sum(gle.debit)) + .where(gle.against_voucher == asset.asset_id) + .where(gle.account == depreciation_expense_account) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .where(gle.posting_date <= date) + ).run() + + if result and result[0] and result[0][0]: + depr_amount = result[0][0] + else: + depr_amount = 0 + + return depr_amount + + def get_purchase_receipt_supplier_map(): return frappe._dict( frappe.db.sql( diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f49e06675dd..ecbf1f8000c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -375,3 +375,4 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0}) erpnext.patches.v13_0.update_schedule_type_in_loans +erpnext.patches.v13_0.update_asset_value_for_manual_depr_entries diff --git a/erpnext/patches/v13_0/update_asset_value_for_manual_depr_entries.py b/erpnext/patches/v13_0/update_asset_value_for_manual_depr_entries.py new file mode 100644 index 00000000000..5d7b5cf19c1 --- /dev/null +++ b/erpnext/patches/v13_0/update_asset_value_for_manual_depr_entries.py @@ -0,0 +1,38 @@ +import frappe +from frappe.query_builder.functions import IfNull, Sum + + +def execute(): + asset = frappe.qb.DocType("Asset") + gle = frappe.qb.DocType("GL Entry") + aca = frappe.qb.DocType("Asset Category Account") + company = frappe.qb.DocType("Company") + + asset_total_depr_value_map = ( + frappe.qb.from_(gle) + .join(asset) + .on(gle.against_voucher == asset.name) + .join(aca) + .on((aca.parent == asset.asset_category) & (aca.company_name == asset.company)) + .join(company) + .on(company.name == asset.company) + .select(Sum(gle.debit).as_("value"), asset.name.as_("asset_name")) + .where( + gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) + ) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .where(asset.docstatus == 1) + .where(asset.calculate_depreciation == 0) + .groupby(asset.name) + ) + + frappe.qb.update(asset).join(asset_total_depr_value_map).on( + asset_total_depr_value_map.asset_name == asset.name + ).set( + asset.value_after_depreciation, asset.value_after_depreciation - asset_total_depr_value_map.value + ).where( + asset.docstatus == 1 + ).where( + asset.calculate_depreciation == 0 + ).run() diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0ca27c20fac..f703f1bcfdc 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1781,6 +1781,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ var me = this; var valid = true; + if (frappe.flags.ignore_company_party_validation) { + return valid; + } + $.each(["company", "customer"], function(i, fieldname) { if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && me.frm.doc.doctype != "Purchase Order") { if (!me.frm.doc[fieldname]) { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 396ade0ef13..0889baad4e2 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -466,7 +466,20 @@ erpnext.utils.update_child_items = function(opts) { const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`); const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision; - this.data = []; + this.data = frm.doc[opts.child_docname].map((d) => { + return { + "docname": d.name, + "name": d.name, + "item_code": d.item_code, + "delivery_date": d.delivery_date, + "schedule_date": d.schedule_date, + "conversion_factor": d.conversion_factor, + "qty": d.qty, + "rate": d.rate, + "uom": d.uom + } + }); + const fields = [{ fieldtype:'Data', fieldname:"docname", @@ -559,7 +572,7 @@ erpnext.utils.update_child_items = function(opts) { }) } - const dialog = new frappe.ui.Dialog({ + new frappe.ui.Dialog({ title: __("Update Items"), fields: [ { @@ -595,24 +608,7 @@ erpnext.utils.update_child_items = function(opts) { refresh_field("items"); }, primary_action_label: __('Update') - }); - - frm.doc[opts.child_docname].forEach(d => { - dialog.fields_dict.trans_items.df.data.push({ - "docname": d.name, - "name": d.name, - "item_code": d.item_code, - "delivery_date": d.delivery_date, - "schedule_date": d.schedule_date, - "conversion_factor": d.conversion_factor, - "qty": d.qty, - "rate": d.rate, - "uom": d.uom - }); - this.data = dialog.fields_dict.trans_items.df.data; - dialog.fields_dict.trans_items.grid.refresh(); - }) - dialog.show(); + }).show(); } erpnext.utils.map_current_doc = function(opts) { diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json index 879034a0dfc..364fe5df6d6 100644 --- a/erpnext/selling/workspace/selling/selling.json +++ b/erpnext/selling/workspace/selling/selling.json @@ -13,9 +13,10 @@ "docstatus": 0, "doctype": "Workspace", "extends_another_page": 0, - "hide_custom": 1, + "hide_custom": 0, "icon": "sell", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Selling", "links": [ @@ -515,7 +516,7 @@ "type": "Link" } ], - "modified": "2020-12-01 13:38:35.971277", + "modified": "2023-01-28 17:10:02.716760", "modified_by": "Administrator", "module": "Selling", "name": "Selling", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index aa626403362..0ffc18c15a3 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -849,6 +849,12 @@ function open_form(frm, doctype, child_doctype, parentfield) { new_child_doc.uom = frm.doc.stock_uom; new_child_doc.description = frm.doc.description; - frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + frappe.run_serially([ + () => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc), + () => { + frappe.flags.ignore_company_party_validation = true; + frappe.model.trigger("item_code", frm.doc.name, new_child_doc); + } + ]) }); } diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 9321c2c166b..2a9f091bd09 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -221,7 +221,7 @@ class QualityInspection(Document): def item_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond - from_doctype = cstr(filters.get("doctype")) + from_doctype = cstr(filters.get("from")) if not from_doctype or not frappe.db.exists("DocType", from_doctype): return []