mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-12 17:23:38 +00:00
Merge pull request #51363 from aerele/asset-partial-sales
fix(asset): handle partial asset sales by splitting remaining quantity
This commit is contained in:
@@ -33,6 +33,7 @@ from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import split_asset
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
@@ -468,6 +469,8 @@ class SalesInvoice(SellingController):
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
self.split_asset_based_on_sale_qty()
|
||||
|
||||
self.process_asset_depreciation()
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
@@ -1358,6 +1361,51 @@ class SalesInvoice(SellingController):
|
||||
):
|
||||
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):
|
||||
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
||||
self.depreciate_asset_on_sale()
|
||||
|
||||
@@ -111,7 +111,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.add_custom_button(
|
||||
__("Sell Asset"),
|
||||
function () {
|
||||
frm.trigger("make_sales_invoice");
|
||||
frm.trigger("sell_asset");
|
||||
},
|
||||
__("Manage")
|
||||
);
|
||||
@@ -521,22 +521,6 @@ frappe.ui.form.on("Asset", {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
},
|
||||
|
||||
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) {
|
||||
frappe.call({
|
||||
args: {
|
||||
@@ -585,6 +569,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) {
|
||||
const title = __("Split Asset");
|
||||
|
||||
|
||||
@@ -482,6 +482,9 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
||||
|
||||
def validate_linked_purchase_documents(self):
|
||||
if self.flags.is_split_asset:
|
||||
return
|
||||
|
||||
for fieldname, doctype in [
|
||||
("purchase_receipt", "Purchase Receipt"),
|
||||
("purchase_invoice", "Purchase Invoice"),
|
||||
@@ -1083,7 +1086,7 @@ def get_asset_naming_series():
|
||||
|
||||
|
||||
@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)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
si.company = company
|
||||
@@ -1098,7 +1101,7 @@ def make_sales_invoice(asset, item_code, company, serial_no=None, posting_date=N
|
||||
"income_account": disposal_account,
|
||||
"serial_no": serial_no,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"qty": 1,
|
||||
"qty": sell_qty,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1378,6 +1381,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)
|
||||
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.flags.is_split_asset = True
|
||||
|
||||
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)
|
||||
|
||||
@@ -330,7 +330,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
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.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
@@ -458,7 +460,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
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.due_date = nowdate()
|
||||
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.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):
|
||||
def test_schedule_for_straight_line_method(self):
|
||||
|
||||
@@ -51,7 +51,9 @@ class TestAssetRepair(IntegrationTestCase):
|
||||
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.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
|
||||
Reference in New Issue
Block a user