Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-41429

This commit is contained in:
Deepesh Garg
2024-05-16 21:02:55 +05:30
committed by GitHub
33 changed files with 622 additions and 151 deletions

View File

@@ -106,7 +106,7 @@
},
{
"default": "0",
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
"description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
"fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness"
@@ -469,7 +469,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-05-11 23:19:44.673975",
"modified": "2024-03-15 12:11:36.085158",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -498,4 +498,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -454,7 +454,7 @@ class JournalEntry(AccountsController):
self.voucher_type == "Depreciation Entry"
and d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
and d.debit
):
asset = frappe.get_doc("Asset", d.reference_name)

View File

@@ -158,7 +158,7 @@ def set_ageing(doc, entry):
ageing_filters = frappe._dict(
{
"company": doc.company,
"report_date": doc.to_date,
"report_date": doc.posting_date,
"ageing_based_on": doc.ageing_based_on,
"range1": 30,
"range2": 60,

View File

@@ -340,10 +340,11 @@
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">30 Days</th>
<th style="width: 25%">60 Days</th>
<th style="width: 25%">90 Days</th>
<th style="width: 25%">120 Days</th>
<th style="width: 25%">0 - 30 Days</th>
<th style="width: 25%">30 - 60 Days</th>
<th style="width: 25%">60 - 90 Days</th>
<th style="width: 25%">90 - 120 Days</th>
<th style="width: 20%">Above 120 Days</th>
</tr>
</thead>
<tbody>
@@ -352,6 +353,7 @@
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>

View File

@@ -1182,7 +1182,7 @@ class PurchaseInvoice(BuyingController):
asset.name,
{
"gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
"purchase_amount": purchase_amount,
},
)

View File

@@ -1028,20 +1028,6 @@ class ReceivablePayableReport:
fieldtype="Link",
options="Contact",
)
if self.filters.party_type == "Customer":
self.add_column(
_("Customer Name"),
fieldname="customer_name",
fieldtype="Link",
options="Customer",
)
elif self.filters.party_type == "Supplier":
self.add_column(
_("Supplier Name"),
fieldname="supplier_name",
fieldtype="Link",
options="Supplier",
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")

View File

@@ -652,7 +652,7 @@ frappe.ui.form.on("Asset", {
);
frm.set_value("gross_purchase_amount", purchase_amount);
frm.set_value("purchase_receipt_amount", purchase_amount);
frm.set_value("purchase_amount", purchase_amount);
frm.set_value("asset_quantity", asset_quantity);
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
if (item.asset_location) {

View File

@@ -72,7 +72,7 @@
"status",
"booked_fixed_asset",
"column_break_51",
"purchase_receipt_amount",
"purchase_amount",
"default_finance_book",
"depr_entry_posting_status",
"amended_from",
@@ -408,15 +408,6 @@
"options": "Purchase Receipt",
"print_hide": 1
},
{
"fieldname": "purchase_receipt_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Purchase Receipt Amount",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
"fieldname": "purchase_invoice",
@@ -546,6 +537,15 @@
"label": "Additional Asset Cost",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "purchase_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Purchase Amount",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 72,
@@ -589,7 +589,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2024-01-15 17:35:49.226603",
"modified": "2024-04-18 16:45:47.306032",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -633,4 +633,4 @@
"states": [],
"title_field": "asset_name",
"track_changes": 1
}
}

View File

@@ -92,10 +92,10 @@ class Asset(AccountsController):
number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency
policy_number: DF.Data | None
purchase_amount: DF.Currency
purchase_date: DF.Date | None
purchase_invoice: DF.Link | None
purchase_receipt: DF.Link | None
purchase_receipt_amount: DF.Currency
split_from: DF.Link | None
status: DF.Literal[
"Draft",
@@ -354,7 +354,7 @@ class Asset(AccountsController):
if self.is_existing_asset:
return
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount:
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_amount:
error_message = _(
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
)
@@ -696,7 +696,7 @@ class Asset(AccountsController):
purchase_document = self.get_purchase_document()
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
if purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate():
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
gl_entries.append(
self.get_gl_dict(
{
@@ -704,8 +704,8 @@ class Asset(AccountsController):
"against": fixed_asset_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date,
"credit": self.purchase_receipt_amount,
"credit_in_account_currency": self.purchase_receipt_amount,
"credit": self.purchase_amount,
"credit_in_account_currency": self.purchase_amount,
"cost_center": self.cost_center,
},
item=self,
@@ -719,8 +719,8 @@ class Asset(AccountsController):
"against": cwip_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date,
"debit": self.purchase_receipt_amount,
"debit_in_account_currency": self.purchase_receipt_amount,
"debit": self.purchase_amount,
"debit_in_account_currency": self.purchase_amount,
"cost_center": self.cost_center,
},
item=self,
@@ -1116,8 +1116,8 @@ def create_new_asset_after_split(asset, split_qty):
)
new_asset.gross_purchase_amount = new_gross_purchase_amount
if asset.purchase_receipt_amount:
new_asset.purchase_receipt_amount = new_gross_purchase_amount
if asset.purchase_amount:
new_asset.purchase_amount = new_gross_purchase_amount
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name

View File

@@ -1000,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
depreciation_amount = get_depreciation_amount(
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
)
self.assertEqual(depreciation_amount, 30000)
@@ -1698,7 +1698,7 @@ def create_asset(**args):
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
"gross_purchase_amount": args.gross_purchase_amount or 100000,
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
"purchase_amount": args.purchase_amount or 100000,
"maintenance_required": args.maintenance_required or 0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": args.available_for_use_date or "2020-06-06",
@@ -1723,6 +1723,7 @@ def create_asset(**args):
"depreciation_start_date": args.depreciation_start_date,
"daily_prorata_based": args.daily_prorata_based or 0,
"shift_based": args.shift_based or 0,
"rate_of_depreciation": args.rate_of_depreciation or 0,
},
)

View File

@@ -145,7 +145,7 @@ class AssetCapitalization(StockController):
def on_trash(self):
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
super(AssetCapitalization, self).on_trash()
super().on_trash()
def cancel_target_asset(self):
if self.entry_type == "Capitalization" and self.target_asset:
@@ -616,8 +616,7 @@ class AssetCapitalization(StockController):
asset_doc.available_for_use_date = self.posting_date
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.purchase_receipt_amount = total_target_asset_value
asset_doc.purchase_amount = total_target_asset_value
asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True
asset_doc.flags.asset_created_via_asset_capitalization = True
@@ -653,7 +652,7 @@ class AssetCapitalization(StockController):
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.purchase_amount = total_target_asset_value
asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True
asset_doc.save()

View File

@@ -89,7 +89,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
self.assertEqual(target_asset.purchase_amount, total_amount)
# Test Consumed Asset values
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
@@ -179,7 +179,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
self.assertEqual(target_asset.purchase_amount, total_amount)
# Test Consumed Asset values
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
@@ -256,7 +256,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
self.assertEqual(target_asset.purchase_amount, total_amount)
# Test General Ledger Entries
expected_gle = {
@@ -526,7 +526,7 @@ def create_depreciation_asset(**args):
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
asset.purchase_amount = asset.gross_purchase_amount
finance_book = asset.append("finance_books")
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"

View File

@@ -285,6 +285,7 @@ class AssetDepreciationSchedule(Document):
number_of_pending_depreciations = final_number_of_depreciations - start
yearly_opening_wdv = value_after_depreciation
current_fiscal_year_end_date = None
prev_per_day_depr = True
for n in range(start, final_number_of_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
@@ -301,8 +302,7 @@ class AssetDepreciationSchedule(Document):
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
else:
prev_depreciation_amount = 0
depreciation_amount = get_depreciation_amount(
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
self,
asset_doc,
value_after_depreciation,
@@ -312,6 +312,7 @@ class AssetDepreciationSchedule(Document):
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
number_of_pending_depreciations,
prev_per_day_depr,
)
if not has_pro_rata or (
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
@@ -599,11 +600,12 @@ def get_depreciation_amount(
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
number_of_pending_depreciations=0,
prev_per_day_depr=0,
):
if fb_row.depreciation_method in ("Straight Line", "Manual"):
return get_straight_line_or_manual_depr_amount(
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
)
), None
else:
return get_wdv_or_dd_depr_amount(
asset,
@@ -614,6 +616,7 @@ def get_depreciation_amount(
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
prev_per_day_depr,
)
@@ -637,49 +640,14 @@ def get_straight_line_or_manual_depr_amount(
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
if row.daily_prorata_based:
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
total_days = (
date_diff(
get_last_day(
add_months(
row.depreciation_start_date,
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
* row.frequency_of_depreciation,
)
),
add_days(
get_last_day(
add_months(
row.depreciation_start_date,
flt(
row.total_number_of_depreciations
- asset.number_of_depreciations_booked
- number_of_pending_depreciations
- 1
)
* row.frequency_of_depreciation,
)
),
1,
),
)
+ 1
)
daily_depr_amount = amount / total_days
to_date = get_last_day(
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
return get_daily_prorata_based_straight_line_depr(
asset,
row,
schedule_idx,
number_of_pending_depreciations,
amount,
)
from_date = add_days(
get_last_day(
add_months(
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
)
),
1,
)
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
else:
return (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
@@ -692,40 +660,9 @@ def get_straight_line_or_manual_depr_amount(
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
)
total_days = (
date_diff(
get_last_day(
add_months(
row.depreciation_start_date,
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
* row.frequency_of_depreciation,
)
),
add_days(
get_last_day(
add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
),
1,
),
)
+ 1
return get_daily_prorata_based_straight_line_depr(
asset, row, schedule_idx, number_of_pending_depreciations, amount
)
daily_depr_amount = amount / total_days
to_date = get_last_day(
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
)
from_date = add_days(
get_last_day(
add_months(
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
)
),
1,
)
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
else:
return (
flt(asset.gross_purchase_amount)
@@ -734,6 +671,23 @@ def get_straight_line_or_manual_depr_amount(
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
def get_daily_prorata_based_straight_line_depr(
asset, row, schedule_idx, number_of_pending_depreciations, amount
):
total_years = flt(number_of_pending_depreciations * row.frequency_of_depreciation) / 12
every_year_depr = amount / total_years
year_start_date = add_years(
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
)
year_end_date = add_days(add_years(year_start_date, 1), -1)
daily_depr_amount = every_year_depr / (date_diff(year_end_date, year_start_date) + 1)
from_date, total_depreciable_days = _get_total_days(
row.depreciation_start_date, schedule_idx, row.frequency_of_depreciation
)
return daily_depr_amount * total_depreciable_days
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
return (
@@ -779,6 +733,7 @@ def get_wdv_or_dd_depr_amount(
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
prev_per_day_depr,
):
return get_default_wdv_or_dd_depr_amount(
asset,
@@ -788,6 +743,7 @@ def get_wdv_or_dd_depr_amount(
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
prev_per_day_depr,
)
@@ -799,6 +755,39 @@ def get_default_wdv_or_dd_depr_amount(
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
prev_per_day_depr,
):
if not fb_row.daily_prorata_based or cint(fb_row.frequency_of_depreciation) == 12:
return _get_default_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
), None
else:
return _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
prev_per_day_depr,
)
def _get_default_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
):
if cint(fb_row.frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
@@ -825,6 +814,75 @@ def get_default_wdv_or_dd_depr_amount(
return prev_depreciation_amount
def _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
prev_per_day_depr,
):
if has_wdv_or_dd_non_yearly_pro_rata: # If applicable days for ther first month is less than full month
if schedule_idx == 0:
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100), None
elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: # Year changes
return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
else:
return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
else:
if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: # year changes
return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
else:
return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
""" "
Returns monthly depreciation amount when year changes
1. Calculate per day depr based on new year
2. Calculate monthly amount based on new per day amount
"""
from_date, days_in_month = _get_total_days(
fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
)
per_day_depr = get_per_day_depr(fb_row, depreciable_value, from_date)
return (per_day_depr * days_in_month), per_day_depr
def get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr):
""" "
Returns monthly depreciation amount based on prev per day depr
Calculate per day depr only for the first month
"""
from_date, days_in_month = _get_total_days(
fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
)
return (prev_per_day_depr * days_in_month), prev_per_day_depr
def get_per_day_depr(
fb_row,
depreciable_value,
from_date,
):
to_date = add_days(add_years(from_date, 1), -1)
total_days = date_diff(to_date, from_date) + 1
per_day_depr = (flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)) / total_days
return per_day_depr
def _get_total_days(depreciation_start_date, schedule_idx, frequency_of_depreciation):
from_date = add_months(depreciation_start_date, (schedule_idx - 1) * frequency_of_depreciation)
to_date = add_months(from_date, frequency_of_depreciation)
if is_last_day_of_the_month(depreciation_start_date):
to_date = get_last_day(to_date)
from_date = add_days(get_last_day(from_date), 1)
return from_date, date_diff(to_date, from_date) + 1
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
asset_depr_schedules_names = []

View File

@@ -3,10 +3,12 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
get_depr_schedule,
)
@@ -25,3 +27,136 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
)
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
def test_daily_prorata_based_depr_on_sl_methond(self):
asset = create_asset(
calculate_depreciation=1,
depreciation_method="Straight Line",
daily_prorata_based=1,
available_for_use_date="2020-01-01",
depreciation_start_date="2020-01-31",
frequency_of_depreciation=1,
total_number_of_depreciations=24,
)
expected_schedules = [
["2020-01-31", 4234.97, 4234.97],
["2020-02-29", 3961.75, 8196.72],
["2020-03-31", 4234.97, 12431.69],
["2020-04-30", 4098.36, 16530.05],
["2020-05-31", 4234.97, 20765.02],
["2020-06-30", 4098.36, 24863.38],
["2020-07-31", 4234.97, 29098.35],
["2020-08-31", 4234.97, 33333.32],
["2020-09-30", 4098.36, 37431.68],
["2020-10-31", 4234.97, 41666.65],
["2020-11-30", 4098.36, 45765.01],
["2020-12-31", 4234.97, 49999.98],
["2021-01-31", 4246.58, 54246.56],
["2021-02-28", 3835.62, 58082.18],
["2021-03-31", 4246.58, 62328.76],
["2021-04-30", 4109.59, 66438.35],
["2021-05-31", 4246.58, 70684.93],
["2021-06-30", 4109.59, 74794.52],
["2021-07-31", 4246.58, 79041.1],
["2021-08-31", 4246.58, 83287.68],
["2021-09-30", 4109.59, 87397.27],
["2021-10-31", 4246.58, 91643.85],
["2021-11-30", 4109.59, 95753.44],
["2021-12-31", 4246.56, 100000.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
# Test for Written Down Value Method
# Frequency of deprciation = 3
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_3_months(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
depreciation_method="Written Down Value",
daily_prorata_based=1,
available_for_use_date="2021-02-20",
depreciation_start_date="2021-03-31",
frequency_of_depreciation=3,
total_number_of_depreciations=6,
rate_of_depreciation=40,
)
expected_schedules = [
["2021-03-31", 4383.56, 4383.56],
["2021-06-30", 9535.45, 13919.01],
["2021-09-30", 9640.23, 23559.24],
["2021-12-31", 9640.23, 33199.47],
["2022-03-31", 9430.66, 42630.13],
["2022-06-30", 5721.27, 48351.4],
["2022-08-20", 51648.6, 100000.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
# Frequency of deprciation = 6
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_6_months(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
depreciation_method="Written Down Value",
daily_prorata_based=1,
available_for_use_date="2020-02-20",
depreciation_start_date="2020-02-29",
frequency_of_depreciation=6,
total_number_of_depreciations=6,
rate_of_depreciation=40,
)
expected_schedules = [
["2020-02-29", 1092.90, 1092.90],
["2020-08-31", 19944.01, 21036.91],
["2021-02-28", 19618.83, 40655.74],
["2021-08-31", 11966.4, 52622.14],
["2022-02-28", 11771.3, 64393.44],
["2022-08-31", 7179.84, 71573.28],
["2023-02-20", 28426.72, 100000.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
# Frequency of deprciation = 12
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_12_months(self):
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
depreciation_method="Written Down Value",
daily_prorata_based=1,
available_for_use_date="2020-02-20",
depreciation_start_date="2020-03-31",
frequency_of_depreciation=12,
total_number_of_depreciations=4,
rate_of_depreciation=40,
)
expected_schedules = [
["2020-03-31", 4480.87, 4480.87],
["2021-03-31", 38207.65, 42688.52],
["2022-03-31", 22924.59, 65613.11],
["2023-03-31", 13754.76, 79367.87],
["2024-02-20", 20632.13, 100000],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)

View File

@@ -787,7 +787,7 @@ class BuyingController(SubcontractingController):
"supplier": self.supplier,
"purchase_date": self.posting_date,
"calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount,
"purchase_amount": purchase_amount,
"gross_purchase_amount": purchase_amount,
"asset_quantity": asset_quantity,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,

View File

@@ -205,6 +205,7 @@ class StockController(AccountsController):
"company": self.company,
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
"use_serial_batch_fields": row.use_serial_batch_fields,
"via_landed_cost_voucher": via_landed_cost_voucher,
"do_not_submit": True if not via_landed_cost_voucher else False,
}

View File

@@ -21,7 +21,8 @@ def get_exploded_items(bom, data, indent=0, qty=1):
exploded_items = frappe.get_all(
"BOM Item",
filters={"parent": bom},
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom"],
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom", "idx"],
order_by="idx ASC",
)
for item in exploded_items:

View File

@@ -93,4 +93,11 @@ frappe.query_reports["Exponential Smoothing Forecasting"] = {
},
},
],
formatter: function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname === "item_code" && value.includes("Total Quantity")) {
value = "<strong>" + value + "</strong>";
}
return value;
},
};

View File

@@ -144,7 +144,7 @@ class ForecastingReport(ExponentialSmoothingForecast):
if not self.data:
return
total_row = {"item_code": _(frappe.bold("Total Quantity"))}
total_row = {"item_code": _("Total Quantity")}
for value in self.data:
for period in self.period_list:

View File

@@ -363,3 +363,4 @@ erpnext.patches.v14_0.set_maintain_stock_for_bom_item
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount

View File

@@ -0,0 +1,8 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
frappe.reload_doc("assets", "doctype", "asset")
if frappe.db.has_column("Asset", "purchase_receipt_amount"):
rename_field("Asset", "purchase_receipt_amount", "purchase_amount")

View File

@@ -47,9 +47,14 @@ frappe.ui.form.on("Batch", {
},
make_dashboard: (frm) => {
if (!frm.is_new()) {
let for_stock_levels = 0;
if (!frm.doc.batch_qty && frm.doc.expiry_date) {
for_stock_levels = 1;
}
frappe.call({
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
args: { batch_no: frm.doc.name, item_code: frm.doc.item },
args: { batch_no: frm.doc.name, item_code: frm.doc.item, for_stock_levels: for_stock_levels },
callback: (r) => {
if (!r.message) {
return;

View File

@@ -199,6 +199,7 @@ def get_batch_qty(
posting_date=None,
posting_time=None,
ignore_voucher_nos=None,
for_stock_levels=False,
):
"""Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None
@@ -222,6 +223,7 @@ def get_batch_qty(
"posting_time": posting_time,
"batch_no": batch_no,
"ignore_voucher_nos": ignore_voucher_nos,
"for_stock_levels": for_stock_levels,
}
)

View File

@@ -3,8 +3,6 @@ frappe.listview_settings["Batch"] = {
get_indicator: (doc) => {
if (doc.disabled) {
return [__("Disabled"), "gray", "disabled,=,1"];
} else if (!doc.batch_qty) {
return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"];
} else if (
doc.expiry_date &&
frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0
@@ -14,6 +12,8 @@ frappe.listview_settings["Batch"] = {
"red",
"expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0",
];
} else if (!doc.batch_qty) {
return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"];
} else {
return [__("Active"), "green", "batch_qty,>,0|disabled,=,0"];
}

View File

@@ -5,7 +5,7 @@ import copy
import json
import frappe
from frappe import _
from frappe import _, bold
from frappe.model.document import Document
from frappe.query_builder import Interval
from frappe.query_builder.functions import Count, CurDate, UnixTimestamp
@@ -469,6 +469,13 @@ class Item(Document):
def validate_warehouse_for_reorder(self):
"""Validate Reorder level table for duplicate and conditional mandatory"""
warehouse_material_request_type: list[tuple[str, str]] = []
_warehouse_before_save = frappe._dict()
if not self.is_new() and self._doc_before_save:
_warehouse_before_save = {
d.name: d.warehouse for d in self._doc_before_save.get("reorder_levels") or []
}
for d in self.get("reorder_levels"):
if not d.warehouse_group:
d.warehouse_group = d.warehouse
@@ -485,6 +492,19 @@ class Item(Document):
if d.warehouse_reorder_level and not d.warehouse_reorder_qty:
frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx))
if d.warehouse_group and d.warehouse:
if _warehouse_before_save.get(d.name) == d.warehouse:
continue
child_warehouses = get_child_warehouses(d.warehouse_group)
if d.warehouse not in child_warehouses:
frappe.throw(
_(
"Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2}"
).format(d.idx, bold(d.warehouse), bold(d.warehouse_group)),
title=_("Incorrect Check in (group) Warehouse for Reorder"),
)
def stock_ledger_created(self):
if not hasattr(self, "_stock_ledger_created"):
self._stock_ledger_created = len(
@@ -1360,3 +1380,10 @@ def get_asset_naming_series():
from erpnext.assets.doctype.asset.asset import get_asset_naming_series
return get_asset_naming_series()
@frappe.request_cache
def get_child_warehouses(warehouse):
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
return get_child_warehouses(warehouse)

View File

@@ -862,6 +862,27 @@ class TestItem(FrappeTestCase):
self.assertEqual(data[0].description, item.description)
self.assertTrue("description" in data[0])
def test_group_warehouse_for_reorder_item(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
item_doc = make_item("_Test Group Warehouse For Reorder Item", {"is_stock_item": 1})
warehouse = create_warehouse("_Test Warehouse - _TC")
warehouse_doc = frappe.get_doc("Warehouse", warehouse)
warehouse_doc.db_set("parent_warehouse", "")
item_doc.append(
"reorder_levels",
{
"warehouse": warehouse,
"warehouse_reorder_level": 10,
"warehouse_reorder_qty": 100,
"material_request_type": "Purchase",
"warehouse_group": "_Test Warehouse Group - _TC",
},
)
self.assertRaises(frappe.ValidationError, item_doc.save)
def set_item_variant_settings(fields):
doc = frappe.get_doc("Item Variant Settings")

View File

@@ -946,6 +946,128 @@ class TestLandedCostVoucher(FrappeTestCase):
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
)
def test_do_not_validate_against_landed_cost_voucher_for_serial_for_legacy_pr(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_auto_batch_nos
frappe.flags.ignore_serial_batch_bundle_validation = True
frappe.flags.use_serial_and_batch_fields = True
sn_item = "Test Don't Validate Against LCV For Serial NO for Legacy PR"
sn_item_doc = make_item(
sn_item,
{
"has_serial_no": 1,
"serial_no_series": "SN-ALCVTDVLCVSNO-.####",
"is_stock_item": 1,
},
)
serial_nos = [
"SN-ALCVTDVLCVSNO-0001",
"SN-ALCVTDVLCVSNO-0002",
"SN-ALCVTDVLCVSNO-0003",
"SN-ALCVTDVLCVSNO-0004",
"SN-ALCVTDVLCVSNO-0005",
]
for sn in serial_nos:
if not frappe.db.exists("Serial No", sn):
sn_doc = frappe.get_doc(
{
"doctype": "Serial No",
"item_code": sn_item,
"serial_no": sn,
}
)
sn_doc.insert()
warehouse = "_Test Warehouse - _TC"
company = frappe.db.get_value("Warehouse", warehouse, "company")
pr = make_purchase_receipt(
company=company,
warehouse=warehouse,
item_code=sn_item,
qty=5,
rate=100,
uom=sn_item_doc.stock_uom,
stock_uom=sn_item_doc.stock_uom,
)
pr.reload()
for sn in serial_nos:
sn_doc = frappe.get_doc("Serial No", sn)
sn_doc.db_set(
{
"warehouse": warehouse,
"status": "Active",
}
)
for row in pr.items:
if row.item_code == sn_item:
row.db_set("serial_no", ", ".join(serial_nos))
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name})
for sle in stock_ledger_entries:
doc = frappe.get_doc("Stock Ledger Entry", sle.name)
if doc.item_code == sn_item:
doc.db_set("serial_no", ", ".join(serial_nos))
dn = create_delivery_note(
company=company,
warehouse=warehouse,
item_code=sn_item,
qty=5,
rate=100,
uom=sn_item_doc.stock_uom,
stock_uom=sn_item_doc.stock_uom,
)
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": dn.name})
for sle in stock_ledger_entries:
doc = frappe.get_doc("Stock Ledger Entry", sle.name)
if doc.item_code == sn_item:
doc.db_set("serial_no", ", ".join(serial_nos))
frappe.flags.ignore_serial_batch_bundle_validation = False
frappe.flags.use_serial_and_batch_fields = False
lcv = make_landed_cost_voucher(
company=pr.company,
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=20,
distribute_charges_based_on="Qty",
do_not_save=True,
)
lcv.get_items_from_purchase_receipts()
lcv.save()
lcv.submit()
pr.reload()
for row in pr.items:
self.assertEqual(row.valuation_rate, 104)
self.assertTrue(row.serial_and_batch_bundle)
self.assertEqual(
row.valuation_rate,
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
)
lcv.cancel()
pr.reload()
for row in pr.items:
self.assertEqual(row.valuation_rate, 100)
self.assertTrue(row.serial_and_batch_bundle)
self.assertEqual(
row.valuation_rate,
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
)
def make_landed_cost_voucher(**args):
args = frappe._dict(args)

View File

@@ -858,7 +858,7 @@ class PurchaseReceipt(BuyingController):
asset.name,
{
"gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
"purchase_amount": purchase_amount,
},
)
@@ -1163,7 +1163,12 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
qty = item_row.qty
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
qty = item_row.received_qty
pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
return pending_qty, 0
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
if returned_qty:
if returned_qty >= pending_qty:
@@ -1172,6 +1177,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
else:
pending_qty -= returned_qty
returned_qty = 0
return pending_qty, returned_qty
doclist = get_mapped_doc(

View File

@@ -895,6 +895,8 @@ class TestPurchaseReceipt(FrappeTestCase):
create_purchase_order,
)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
po = create_purchase_order()
pr = create_pr_against_po(po.name)
@@ -914,6 +916,7 @@ class TestPurchaseReceipt(FrappeTestCase):
po.cancel()
def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self):
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
pr1.append(
"items",
@@ -2783,6 +2786,84 @@ class TestPurchaseReceipt(FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def test_purchase_receipt_bill_for_rejected_quantity_in_purchase_invoice(self):
item_code = make_item(
"_Test Purchase Receipt Bill For Rejected Quantity",
properties={"is_stock_item": 1},
).name
pr = make_purchase_receipt(item_code=item_code, qty=5, rate=100)
return_pr = make_purchase_receipt(
item_code=item_code,
is_return=1,
return_against=pr.name,
qty=-2,
do_not_submit=1,
)
return_pr.items[0].purchase_receipt_item = pr.items[0].name
return_pr.submit()
old_value = frappe.db.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
pi = make_purchase_invoice(pr.name)
self.assertEqual(pi.items[0].qty, 3)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1)
pi = make_purchase_invoice(pr.name)
pi.submit()
self.assertEqual(pi.items[0].qty, 5)
frappe.db.set_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", old_value
)
def test_zero_valuation_rate_for_batched_item(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item = make_item(
"_Test Zero Valuation Rate For the Batch Item",
{
"is_purchase_item": 1,
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TZVRFORBATCH.#####",
"valuation_rate": 200,
},
)
pi = make_purchase_receipt(
qty=10,
rate=0,
item_code=item.name,
)
pi.reload()
batch_no = get_batch_from_bundle(pi.items[0].serial_and_batch_bundle)
se = make_stock_entry(
purpose="Material Issue",
item_code=item.name,
source=pi.items[0].warehouse,
qty=10,
batch_no=batch_no,
use_serial_batch_fields=0,
)
se.submit()
se.reload()
self.assertEqual(se.items[0].valuation_rate, 0)
self.assertEqual(se.items[0].basic_rate, 0)
sabb_doc = frappe.get_doc("Serial and Batch Bundle", se.items[0].serial_and_batch_bundle)
for row in sabb_doc.entries:
self.assertEqual(row.incoming_rate, 0)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -249,8 +249,7 @@ class SerialandBatchBundle(Document):
if self.has_serial_no:
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
else:
if sn_obj.batch_avg_rate.get(d.batch_no):
d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no)))
available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty"))
if self.docstatus == 1:
@@ -429,6 +428,9 @@ class SerialandBatchBundle(Document):
self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} should be submit first.")
def check_future_entries_exists(self):
if self.flags and self.flags.via_landed_cost_voucher:
return
if not self.has_serial_no:
return
@@ -1863,14 +1865,14 @@ def get_available_batches(kwargs):
batch_ledger.warehouse,
Sum(batch_ledger.qty).as_("qty"),
)
.where(
(batch_table.disabled == 0)
& ((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
)
.where(batch_table.disabled == 0)
.where(stock_ledger_entry.is_cancelled == 0)
.groupby(batch_ledger.batch_no, batch_ledger.warehouse)
)
if not kwargs.get("for_stock_levels"):
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
if kwargs.get("posting_date"):
if kwargs.get("posting_time") is None:
kwargs.posting_time = nowtime()

View File

@@ -220,7 +220,7 @@ def get_serial_nos(doctype, txt, searchfield, start, page_len, filters):
def get_batch_nos(doctype, txt, searchfield, start, page_len, filters):
query_filters = {}
if txt:
if filters.get("voucher_no") and txt:
query_filters["batch_no"] = ["like", f"%{txt}%"]
if filters.get("voucher_no"):
@@ -239,5 +239,8 @@ def get_batch_nos(doctype, txt, searchfield, start, page_len, filters):
)
else:
if txt:
query_filters["name"] = ["like", f"%{txt}%"]
query_filters["item"] = filters.get("item_code")
return frappe.get_all("Batch", filters=query_filters, as_list=True)

View File

@@ -840,6 +840,9 @@ class SerialBatchCreation:
self.set_auto_serial_batch_entries_for_inward()
self.add_serial_nos_for_batch_item()
if hasattr(self, "via_landed_cost_voucher") and self.via_landed_cost_voucher:
doc.flags.via_landed_cost_voucher = self.via_landed_cost_voucher
self.set_serial_batch_entries(doc)
if not doc.get("entries"):
return frappe._dict({})

View File

@@ -1,14 +1,14 @@
<div class="row">
<div class="row {% if df.bold %}important{% endif %} data-field">
{% if doc.flags.show_inclusive_tax_in_print %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _("Total (Without Tax)") }}</label></div>
<div class="col-xs-7 text-right">
<div class="col-xs-7 text-right value">
{{ doc.get_formatted("net_total", doc) }}
</div>
{% else %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _(df.label) }}</label></div>
<div class="col-xs-7 text-right">
<div class="col-xs-7 text-right value">
{{ doc.get_formatted("total", doc) }}
</div>
{% endif %}