mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-21 07:38:29 +00:00
* fix(asset): handle partial asset sales by splitting remaining quantity (cherry picked from commit9a2710b9d7) * fix: refactor older testcases (cherry picked from commita88fe2ecab) * test: validate asset partial sales (cherry picked from commit9eeccb765d) # Conflicts: # erpnext/assets/doctype/asset/test_asset.py * fix(asset): skip purchase document validation while splitting existing asset (cherry picked from commite7e6567792) * fix(asset): handle same asset being sold in multiple line items in sales invoice (cherry picked from commit23b094f151) * test: validate asset split for auto created asset from purchase voucher (cherry picked from commit4adeaedfde) # Conflicts: # erpnext/assets/doctype/asset/test_asset.py * fix: use new_asset instead of asset_doc when checking values after splitting (cherry picked from commitca97f34092) * fix: remove the redundant purchase receipt submit (cherry picked from commiteeb6d0e9bf) * chore: fix conflict --------- Co-authored-by: Navin-S-R <navin@aerele.in>
This commit is contained in:
@@ -33,6 +33,7 @@ from erpnext.accounts.utils import (
|
|||||||
get_account_currency,
|
get_account_currency,
|
||||||
update_voucher_outstanding,
|
update_voucher_outstanding,
|
||||||
)
|
)
|
||||||
|
from erpnext.assets.doctype.asset.asset import split_asset
|
||||||
from erpnext.assets.doctype.asset.depreciation import (
|
from erpnext.assets.doctype.asset.depreciation import (
|
||||||
depreciate_asset,
|
depreciate_asset,
|
||||||
get_gl_entries_on_asset_disposal,
|
get_gl_entries_on_asset_disposal,
|
||||||
@@ -480,6 +481,8 @@ class SalesInvoice(SellingController):
|
|||||||
self.update_stock_reservation_entries()
|
self.update_stock_reservation_entries()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
|
self.split_asset_based_on_sale_qty()
|
||||||
|
|
||||||
self.process_asset_depreciation()
|
self.process_asset_depreciation()
|
||||||
|
|
||||||
# this sequence because outstanding may get -ve
|
# this sequence because outstanding may get -ve
|
||||||
@@ -1402,6 +1405,51 @@ class SalesInvoice(SellingController):
|
|||||||
):
|
):
|
||||||
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
||||||
|
|
||||||
|
def split_asset_based_on_sale_qty(self):
|
||||||
|
asset_qty_map = self.get_asset_qty()
|
||||||
|
for asset, qty in asset_qty_map.items():
|
||||||
|
if qty["actual_qty"] < qty["sale_qty"]:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)."
|
||||||
|
).format(asset, qty["actual_qty"])
|
||||||
|
)
|
||||||
|
|
||||||
|
remaining_qty = qty["actual_qty"] - qty["sale_qty"]
|
||||||
|
if remaining_qty > 0:
|
||||||
|
split_asset(asset, remaining_qty)
|
||||||
|
|
||||||
|
def get_asset_qty(self):
|
||||||
|
asset_qty_map = {}
|
||||||
|
|
||||||
|
assets = {row.asset for row in self.items if row.is_fixed_asset and row.asset}
|
||||||
|
if not assets or self.is_return:
|
||||||
|
return asset_qty_map
|
||||||
|
|
||||||
|
asset_actual_qty = dict(
|
||||||
|
frappe.db.get_all(
|
||||||
|
"Asset",
|
||||||
|
{"name": ["in", list(assets)]},
|
||||||
|
["name", "asset_quantity"],
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for row in self.items:
|
||||||
|
if row.is_fixed_asset and row.asset:
|
||||||
|
actual_qty = asset_actual_qty.get(row.asset)
|
||||||
|
if row.asset in asset_qty_map.keys():
|
||||||
|
asset_qty_map[row.asset]["sale_qty"] += flt(row.qty)
|
||||||
|
else:
|
||||||
|
asset_qty_map.setdefault(
|
||||||
|
row.asset,
|
||||||
|
{
|
||||||
|
"sale_qty": flt(row.qty),
|
||||||
|
"actual_qty": flt(actual_qty),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return asset_qty_map
|
||||||
|
|
||||||
def process_asset_depreciation(self):
|
def process_asset_depreciation(self):
|
||||||
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
||||||
self.depreciate_asset_on_sale()
|
self.depreciate_asset_on_sale()
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ frappe.ui.form.on("Asset", {
|
|||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Sell Asset"),
|
__("Sell Asset"),
|
||||||
function () {
|
function () {
|
||||||
frm.trigger("make_sales_invoice");
|
frm.trigger("sell_asset");
|
||||||
},
|
},
|
||||||
__("Manage")
|
__("Manage")
|
||||||
);
|
);
|
||||||
@@ -523,22 +523,6 @@ frappe.ui.form.on("Asset", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
make_sales_invoice: function (frm) {
|
|
||||||
frappe.call({
|
|
||||||
args: {
|
|
||||||
asset: frm.doc.name,
|
|
||||||
item_code: frm.doc.item_code,
|
|
||||||
company: frm.doc.company,
|
|
||||||
serial_no: frm.doc.serial_no,
|
|
||||||
},
|
|
||||||
method: "erpnext.assets.doctype.asset.asset.make_sales_invoice",
|
|
||||||
callback: function (r) {
|
|
||||||
var doclist = frappe.model.sync(r.message);
|
|
||||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
create_asset_maintenance: function (frm) {
|
create_asset_maintenance: function (frm) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
args: {
|
args: {
|
||||||
@@ -587,6 +571,69 @@ frappe.ui.form.on("Asset", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sell_asset: function (frm) {
|
||||||
|
const make_sales_invoice = (sell_qty) => {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.assets.doctype.asset.asset.make_sales_invoice",
|
||||||
|
args: {
|
||||||
|
asset: frm.doc.name,
|
||||||
|
item_code: frm.doc.item_code,
|
||||||
|
company: frm.doc.company,
|
||||||
|
serial_no: frm.doc.serial_no,
|
||||||
|
sell_qty: sell_qty,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
var doclist = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Sell Asset"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "sell_qty",
|
||||||
|
fieldtype: "Int",
|
||||||
|
label: __("Sell Qty"),
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.set_primary_action(__("Sell"), function () {
|
||||||
|
const dialog_data = dialog.get_values();
|
||||||
|
const sell_qty = cint(dialog_data.sell_qty);
|
||||||
|
const asset_qty = cint(frm.doc.asset_quantity);
|
||||||
|
|
||||||
|
if (sell_qty <= 0) {
|
||||||
|
frappe.throw(__("Sell quantity must be greater than zero"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sell_qty > asset_qty) {
|
||||||
|
frappe.throw(__("Sell quantity cannot exceed the asset quantity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sell_qty < asset_qty) {
|
||||||
|
frappe.confirm(
|
||||||
|
__(
|
||||||
|
"The sell quantity is less than the total asset quantity. The remaining quantity will be split into a new asset. This action cannot be undone. <br><br><b>Do you want to continue?</b>"
|
||||||
|
),
|
||||||
|
() => {
|
||||||
|
make_sales_invoice(sell_qty);
|
||||||
|
dialog.hide();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
make_sales_invoice(sell_qty);
|
||||||
|
dialog.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
},
|
||||||
|
|
||||||
split_asset: function (frm) {
|
split_asset: function (frm) {
|
||||||
const title = __("Split Asset");
|
const title = __("Split Asset");
|
||||||
|
|
||||||
|
|||||||
@@ -484,6 +484,9 @@ class Asset(AccountsController):
|
|||||||
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
||||||
|
|
||||||
def validate_linked_purchase_documents(self):
|
def validate_linked_purchase_documents(self):
|
||||||
|
if self.flags.is_split_asset:
|
||||||
|
return
|
||||||
|
|
||||||
for fieldname, doctype in [
|
for fieldname, doctype in [
|
||||||
("purchase_receipt", "Purchase Receipt"),
|
("purchase_receipt", "Purchase Receipt"),
|
||||||
("purchase_invoice", "Purchase Invoice"),
|
("purchase_invoice", "Purchase Invoice"),
|
||||||
@@ -1085,7 +1088,7 @@ def get_asset_naming_series():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_sales_invoice(asset, item_code, company, serial_no=None, posting_date=None):
|
def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
|
||||||
asset_doc = frappe.get_doc("Asset", asset)
|
asset_doc = frappe.get_doc("Asset", asset)
|
||||||
si = frappe.new_doc("Sales Invoice")
|
si = frappe.new_doc("Sales Invoice")
|
||||||
si.company = company
|
si.company = company
|
||||||
@@ -1100,7 +1103,7 @@ def make_sales_invoice(asset, item_code, company, serial_no=None, posting_date=N
|
|||||||
"income_account": disposal_account,
|
"income_account": disposal_account,
|
||||||
"serial_no": serial_no,
|
"serial_no": serial_no,
|
||||||
"cost_center": depreciation_cost_center,
|
"cost_center": depreciation_cost_center,
|
||||||
"qty": 1,
|
"qty": sell_qty,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1380,6 +1383,7 @@ def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_a
|
|||||||
scaling_factor = flt(split_qty) / flt(existing_asset.asset_quantity)
|
scaling_factor = flt(split_qty) / flt(existing_asset.asset_quantity)
|
||||||
new_asset = frappe.copy_doc(existing_asset) if is_new_asset else splitted_asset
|
new_asset = frappe.copy_doc(existing_asset) if is_new_asset else splitted_asset
|
||||||
asset_doc = new_asset if is_new_asset else existing_asset
|
asset_doc = new_asset if is_new_asset else existing_asset
|
||||||
|
asset_doc.flags.is_split_asset = True
|
||||||
|
|
||||||
set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset)
|
set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset)
|
||||||
log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset)
|
log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset)
|
||||||
|
|||||||
@@ -330,7 +330,9 @@ class TestAsset(AssetSetup):
|
|||||||
|
|
||||||
post_depreciation_entries(date=add_months(purchase_date, 2))
|
post_depreciation_entries(date=add_months(purchase_date, 2))
|
||||||
|
|
||||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
si = make_sales_invoice(
|
||||||
|
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
|
||||||
|
)
|
||||||
si.customer = "_Test Customer"
|
si.customer = "_Test Customer"
|
||||||
si.due_date = date
|
si.due_date = date
|
||||||
si.get("items")[0].rate = 25000
|
si.get("items")[0].rate = 25000
|
||||||
@@ -458,7 +460,9 @@ class TestAsset(AssetSetup):
|
|||||||
|
|
||||||
post_depreciation_entries(date="2021-01-01")
|
post_depreciation_entries(date="2021-01-01")
|
||||||
|
|
||||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
si = make_sales_invoice(
|
||||||
|
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
|
||||||
|
)
|
||||||
si.customer = "_Test Customer"
|
si.customer = "_Test Customer"
|
||||||
si.due_date = nowdate()
|
si.due_date = nowdate()
|
||||||
si.get("items")[0].rate = 25000
|
si.get("items")[0].rate = 25000
|
||||||
@@ -698,6 +702,128 @@ class TestAsset(AssetSetup):
|
|||||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
|
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
|
||||||
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
|
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
|
||||||
|
|
||||||
|
def test_partial_asset_sale(self):
|
||||||
|
date = nowdate()
|
||||||
|
purchase_date = add_months(get_first_day(date), -2)
|
||||||
|
depreciation_start_date = add_months(get_last_day(date), -2)
|
||||||
|
|
||||||
|
# create an asset
|
||||||
|
asset = create_asset(
|
||||||
|
item_code="Macbook Pro",
|
||||||
|
is_existing_asset=1,
|
||||||
|
calculate_depreciation=1,
|
||||||
|
available_for_use_date=purchase_date,
|
||||||
|
purchase_date=purchase_date,
|
||||||
|
depreciation_start_date=depreciation_start_date,
|
||||||
|
net_purchase_amount=1000000.0,
|
||||||
|
purchase_amount=1000000.0,
|
||||||
|
asset_quantity=10,
|
||||||
|
total_number_of_depreciations=12,
|
||||||
|
frequency_of_depreciation=1,
|
||||||
|
submit=1,
|
||||||
|
)
|
||||||
|
asset_depr_schedule_before_sale = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||||
|
post_depreciation_entries(date)
|
||||||
|
asset.reload()
|
||||||
|
|
||||||
|
# check asset values before sale
|
||||||
|
self.assertEqual(asset.asset_quantity, 10)
|
||||||
|
self.assertEqual(asset.net_purchase_amount, 1000000)
|
||||||
|
self.assertEqual(asset.status, "Partially Depreciated")
|
||||||
|
self.assertEqual(
|
||||||
|
asset_depr_schedule_before_sale.depreciation_schedule[0].get("depreciation_amount"), 83333.33
|
||||||
|
)
|
||||||
|
|
||||||
|
# make a partial sales against the asset
|
||||||
|
si = make_sales_invoice(
|
||||||
|
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=5
|
||||||
|
)
|
||||||
|
si.customer = "_Test Customer"
|
||||||
|
si.due_date = date
|
||||||
|
si.get("items")[0].rate = 25000
|
||||||
|
si.insert()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
asset.reload()
|
||||||
|
asset_depr_schedule_after_sale = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||||
|
|
||||||
|
# check asset values after sales
|
||||||
|
self.assertEqual(asset.asset_quantity, 5)
|
||||||
|
self.assertEqual(asset.net_purchase_amount, 500000)
|
||||||
|
self.assertEqual(asset.status, "Sold")
|
||||||
|
self.assertEqual(
|
||||||
|
asset_depr_schedule_after_sale.depreciation_schedule[0].get("depreciation_amount"), 41666.66
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_asset_splitting_for_non_existing_asset(self):
|
||||||
|
date = nowdate()
|
||||||
|
purchase_date = add_months(get_first_day(date), -2)
|
||||||
|
depreciation_start_date = add_months(get_last_day(date), -2)
|
||||||
|
|
||||||
|
asset_qty = 10
|
||||||
|
asset_rate = 100000.0
|
||||||
|
asset_item = "Macbook Pro"
|
||||||
|
asset_location = "Test Location"
|
||||||
|
|
||||||
|
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 1)
|
||||||
|
|
||||||
|
# Inward asset via Purchase Receipt
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code="Macbook Pro",
|
||||||
|
posting_date=purchase_date,
|
||||||
|
qty=asset_qty,
|
||||||
|
rate=asset_rate,
|
||||||
|
location=asset_location,
|
||||||
|
supplier="_Test Supplier",
|
||||||
|
)
|
||||||
|
|
||||||
|
asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name")
|
||||||
|
asset_doc = frappe.get_doc("Asset", asset)
|
||||||
|
asset_doc.calculate_depreciation = 1
|
||||||
|
asset_doc.available_for_use_date = purchase_date
|
||||||
|
asset_doc.location = asset_location
|
||||||
|
asset_doc.append(
|
||||||
|
"finance_books",
|
||||||
|
{
|
||||||
|
"expected_value_after_useful_life": 0,
|
||||||
|
"depreciation_method": "Straight Line",
|
||||||
|
"total_number_of_depreciations": 12,
|
||||||
|
"frequency_of_depreciation": 1,
|
||||||
|
"depreciation_start_date": depreciation_start_date,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
asset_doc.submit()
|
||||||
|
|
||||||
|
# check asset values before splitting
|
||||||
|
asset_depr_schedule_before_splitting = get_asset_depr_schedule_doc(asset_doc.name, "Active")
|
||||||
|
self.assertEqual(asset_doc.asset_quantity, 10)
|
||||||
|
self.assertEqual(asset_doc.net_purchase_amount, 1000000)
|
||||||
|
self.assertEqual(
|
||||||
|
asset_depr_schedule_before_splitting.depreciation_schedule[0].get("depreciation_amount"), 83333.33
|
||||||
|
)
|
||||||
|
|
||||||
|
# initate asset split
|
||||||
|
new_asset = split_asset(asset_doc.name, 5)
|
||||||
|
asset_doc.reload()
|
||||||
|
asset_depr_schedule_after_sale = get_asset_depr_schedule_doc(asset_doc.name, "Active")
|
||||||
|
new_asset_depr_schedule = get_asset_depr_schedule_doc(new_asset.name, "Active")
|
||||||
|
|
||||||
|
# check asset values after splitting
|
||||||
|
self.assertEqual(asset_doc.asset_quantity, 5)
|
||||||
|
self.assertEqual(asset_doc.net_purchase_amount, 500000)
|
||||||
|
self.assertEqual(
|
||||||
|
asset_depr_schedule_after_sale.depreciation_schedule[0].get("depreciation_amount"), 41666.66
|
||||||
|
)
|
||||||
|
|
||||||
|
# check new asset values after splitting
|
||||||
|
self.assertEqual(new_asset.asset_quantity, 5)
|
||||||
|
self.assertEqual(new_asset.net_purchase_amount, 500000)
|
||||||
|
self.assertEqual(
|
||||||
|
new_asset_depr_schedule.depreciation_schedule[0].get("depreciation_amount"), 41666.66
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 0)
|
||||||
|
|
||||||
|
|
||||||
class TestDepreciationMethods(AssetSetup):
|
class TestDepreciationMethods(AssetSetup):
|
||||||
def test_schedule_for_straight_line_method(self):
|
def test_schedule_for_straight_line_method(self):
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ class TestAssetRepair(IntegrationTestCase):
|
|||||||
submit=1,
|
submit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
si = make_sales_invoice(
|
||||||
|
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
|
||||||
|
)
|
||||||
si.customer = "_Test Customer"
|
si.customer = "_Test Customer"
|
||||||
si.due_date = date
|
si.due_date = date
|
||||||
si.get("items")[0].rate = 25000
|
si.get("items")[0].rate = 25000
|
||||||
|
|||||||
Reference in New Issue
Block a user