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", "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", "fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness" "label": "Check Supplier Invoice Number Uniqueness"
@@ -469,7 +469,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-05-11 23:19:44.673975", "modified": "2024-03-15 12:11:36.085158",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",
@@ -498,4 +498,4 @@
"sort_order": "ASC", "sort_order": "ASC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1028,20 +1028,6 @@ class ReceivablePayableReport:
fieldtype="Link", fieldtype="Link",
options="Contact", 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=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", 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("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("asset_quantity", asset_quantity);
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center); frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
if (item.asset_location) { if (item.asset_location) {

View File

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

View File

@@ -92,10 +92,10 @@ class Asset(AccountsController):
number_of_depreciations_booked: DF.Int number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency opening_accumulated_depreciation: DF.Currency
policy_number: DF.Data | None policy_number: DF.Data | None
purchase_amount: DF.Currency
purchase_date: DF.Date | None purchase_date: DF.Date | None
purchase_invoice: DF.Link | None purchase_invoice: DF.Link | None
purchase_receipt: DF.Link | None purchase_receipt: DF.Link | None
purchase_receipt_amount: DF.Currency
split_from: DF.Link | None split_from: DF.Link | None
status: DF.Literal[ status: DF.Literal[
"Draft", "Draft",
@@ -354,7 +354,7 @@ class Asset(AccountsController):
if self.is_existing_asset: if self.is_existing_asset:
return 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 = _( error_message = _(
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset." "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() purchase_document = self.get_purchase_document()
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() 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( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
@@ -704,8 +704,8 @@ class Asset(AccountsController):
"against": fixed_asset_account, "against": fixed_asset_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date, "posting_date": self.available_for_use_date,
"credit": self.purchase_receipt_amount, "credit": self.purchase_amount,
"credit_in_account_currency": self.purchase_receipt_amount, "credit_in_account_currency": self.purchase_amount,
"cost_center": self.cost_center, "cost_center": self.cost_center,
}, },
item=self, item=self,
@@ -719,8 +719,8 @@ class Asset(AccountsController):
"against": cwip_account, "against": cwip_account,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"posting_date": self.available_for_use_date, "posting_date": self.available_for_use_date,
"debit": self.purchase_receipt_amount, "debit": self.purchase_amount,
"debit_in_account_currency": self.purchase_receipt_amount, "debit_in_account_currency": self.purchase_amount,
"cost_center": self.cost_center, "cost_center": self.cost_center,
}, },
item=self, item=self,
@@ -1116,8 +1116,8 @@ def create_new_asset_after_split(asset, split_qty):
) )
new_asset.gross_purchase_amount = new_gross_purchase_amount new_asset.gross_purchase_amount = new_gross_purchase_amount
if asset.purchase_receipt_amount: if asset.purchase_amount:
new_asset.purchase_receipt_amount = new_gross_purchase_amount new_asset.purchase_amount = new_gross_purchase_amount
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name 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") 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] asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
) )
self.assertEqual(depreciation_amount, 30000) self.assertEqual(depreciation_amount, 30000)
@@ -1698,7 +1698,7 @@ def create_asset(**args):
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0, "opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0, "number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
"gross_purchase_amount": args.gross_purchase_amount or 100000, "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, "maintenance_required": args.maintenance_required or 0,
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"available_for_use_date": args.available_for_use_date or "2020-06-06", "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, "depreciation_start_date": args.depreciation_start_date,
"daily_prorata_based": args.daily_prorata_based or 0, "daily_prorata_based": args.daily_prorata_based or 0,
"shift_based": args.shift_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): def on_trash(self):
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None) frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
super(AssetCapitalization, self).on_trash() super().on_trash()
def cancel_target_asset(self): def cancel_target_asset(self):
if self.entry_type == "Capitalization" and self.target_asset: 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.available_for_use_date = self.posting_date
asset_doc.purchase_date = self.posting_date asset_doc.purchase_date = self.posting_date
asset_doc.gross_purchase_amount = total_target_asset_value 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.purchase_receipt_amount = total_target_asset_value
asset_doc.capitalized_in = self.name asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True asset_doc.flags.ignore_validate = True
asset_doc.flags.asset_created_via_asset_capitalization = 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 = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount = total_target_asset_value 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.capitalized_in = self.name
asset_doc.flags.ignore_validate = True asset_doc.flags.ignore_validate = True
asset_doc.save() asset_doc.save()

View File

@@ -89,7 +89,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values # Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount) 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 # Test Consumed Asset values
self.assertEqual(consumed_asset.db_get("status"), "Capitalized") self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
@@ -179,7 +179,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values # Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount) 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 # Test Consumed Asset values
self.assertEqual(consumed_asset.db_get("status"), "Capitalized") self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
@@ -256,7 +256,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Test Target Asset values # Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount) 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 # Test General Ledger Entries
expected_gle = { 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.available_for_use_date = args.available_for_use_date or asset.purchase_date
asset.gross_purchase_amount = args.asset_value or 100000 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 = asset.append("finance_books")
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31" 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 number_of_pending_depreciations = final_number_of_depreciations - start
yearly_opening_wdv = value_after_depreciation yearly_opening_wdv = value_after_depreciation
current_fiscal_year_end_date = None current_fiscal_year_end_date = None
prev_per_day_depr = True
for n in range(start, final_number_of_depreciations): for n in range(start, final_number_of_depreciations):
# If depreciation is already completed (for double declining balance) # If depreciation is already completed (for double declining balance)
if skip_row: if skip_row:
@@ -301,8 +302,7 @@ class AssetDepreciationSchedule(Document):
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
else: else:
prev_depreciation_amount = 0 prev_depreciation_amount = 0
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
depreciation_amount = get_depreciation_amount(
self, self,
asset_doc, asset_doc,
value_after_depreciation, value_after_depreciation,
@@ -312,6 +312,7 @@ class AssetDepreciationSchedule(Document):
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
number_of_pending_depreciations, number_of_pending_depreciations,
prev_per_day_depr,
) )
if not has_pro_rata or ( if not has_pro_rata or (
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2 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, prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False, has_wdv_or_dd_non_yearly_pro_rata=False,
number_of_pending_depreciations=0, number_of_pending_depreciations=0,
prev_per_day_depr=0,
): ):
if fb_row.depreciation_method in ("Straight Line", "Manual"): if fb_row.depreciation_method in ("Straight Line", "Manual"):
return get_straight_line_or_manual_depr_amount( return get_straight_line_or_manual_depr_amount(
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
) ), None
else: else:
return get_wdv_or_dd_depr_amount( return get_wdv_or_dd_depr_amount(
asset, asset,
@@ -614,6 +616,7 @@ def get_depreciation_amount(
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule, 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: elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
if row.daily_prorata_based: if row.daily_prorata_based:
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) 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 return get_daily_prorata_based_straight_line_depr(
asset,
to_date = get_last_day( row,
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) 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: else:
return ( return (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) 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(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life) - flt(row.expected_value_after_useful_life)
) )
return get_daily_prorata_based_straight_line_depr(
total_days = ( asset, row, schedule_idx, number_of_pending_depreciations, amount
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
) )
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: else:
return ( return (
flt(asset.gross_purchase_amount) 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) ) / 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): def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation: if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
return ( return (
@@ -779,6 +733,7 @@ def get_wdv_or_dd_depr_amount(
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule, asset_depr_schedule,
prev_per_day_depr,
): ):
return get_default_wdv_or_dd_depr_amount( return get_default_wdv_or_dd_depr_amount(
asset, asset,
@@ -788,6 +743,7 @@ def get_wdv_or_dd_depr_amount(
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule, asset_depr_schedule,
prev_per_day_depr,
) )
@@ -799,6 +755,39 @@ def get_default_wdv_or_dd_depr_amount(
prev_depreciation_amount, prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule, 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: if cint(fb_row.frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) 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 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): def make_draft_asset_depr_schedules_if_not_present(asset_doc):
asset_depr_schedules_names = [] asset_depr_schedules_names = []

View File

@@ -3,10 +3,12 @@
import frappe import frappe
from frappe.tests.utils import FrappeTestCase 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.test_asset import create_asset, create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depr_schedule,
) )
@@ -25,3 +27,136 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
) )
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert) 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, "supplier": self.supplier,
"purchase_date": self.posting_date, "purchase_date": self.posting_date,
"calculate_depreciation": 0, "calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount, "purchase_amount": purchase_amount,
"gross_purchase_amount": purchase_amount, "gross_purchase_amount": purchase_amount,
"asset_quantity": asset_quantity, "asset_quantity": asset_quantity,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,

View File

@@ -205,6 +205,7 @@ class StockController(AccountsController):
"company": self.company, "company": self.company,
"is_rejected": 1 if row.get("rejected_warehouse") else 0, "is_rejected": 1 if row.get("rejected_warehouse") else 0,
"use_serial_batch_fields": row.use_serial_batch_fields, "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, "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( exploded_items = frappe.get_all(
"BOM Item", "BOM Item",
filters={"parent": bom}, 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: 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: if not self.data:
return return
total_row = {"item_code": _(frappe.bold("Total Quantity"))} total_row = {"item_code": _("Total Quantity")}
for value in self.data: for value in self.data:
for period in self.period_list: 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.delete_orphaned_asset_movement_item_records
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset 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) => { make_dashboard: (frm) => {
if (!frm.is_new()) { if (!frm.is_new()) {
let for_stock_levels = 0;
if (!frm.doc.batch_qty && frm.doc.expiry_date) {
for_stock_levels = 1;
}
frappe.call({ frappe.call({
method: "erpnext.stock.doctype.batch.batch.get_batch_qty", 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) => { callback: (r) => {
if (!r.message) { if (!r.message) {
return; return;

View File

@@ -199,6 +199,7 @@ def get_batch_qty(
posting_date=None, posting_date=None,
posting_time=None, posting_time=None,
ignore_voucher_nos=None, ignore_voucher_nos=None,
for_stock_levels=False,
): ):
"""Returns batch actual qty if warehouse is passed, """Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None or returns dict of qty by warehouse if warehouse is None
@@ -222,6 +223,7 @@ def get_batch_qty(
"posting_time": posting_time, "posting_time": posting_time,
"batch_no": batch_no, "batch_no": batch_no,
"ignore_voucher_nos": ignore_voucher_nos, "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) => { get_indicator: (doc) => {
if (doc.disabled) { if (doc.disabled) {
return [__("Disabled"), "gray", "disabled,=,1"]; return [__("Disabled"), "gray", "disabled,=,1"];
} else if (!doc.batch_qty) {
return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"];
} else if ( } else if (
doc.expiry_date && doc.expiry_date &&
frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0 frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0
@@ -14,6 +12,8 @@ frappe.listview_settings["Batch"] = {
"red", "red",
"expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0", "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 { } else {
return [__("Active"), "green", "batch_qty,>,0|disabled,=,0"]; return [__("Active"), "green", "batch_qty,>,0|disabled,=,0"];
} }

View File

@@ -5,7 +5,7 @@ import copy
import json import json
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Interval from frappe.query_builder import Interval
from frappe.query_builder.functions import Count, CurDate, UnixTimestamp from frappe.query_builder.functions import Count, CurDate, UnixTimestamp
@@ -469,6 +469,13 @@ class Item(Document):
def validate_warehouse_for_reorder(self): def validate_warehouse_for_reorder(self):
"""Validate Reorder level table for duplicate and conditional mandatory""" """Validate Reorder level table for duplicate and conditional mandatory"""
warehouse_material_request_type: list[tuple[str, str]] = [] 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"): for d in self.get("reorder_levels"):
if not d.warehouse_group: if not d.warehouse_group:
d.warehouse_group = d.warehouse d.warehouse_group = d.warehouse
@@ -485,6 +492,19 @@ class Item(Document):
if d.warehouse_reorder_level and not d.warehouse_reorder_qty: if d.warehouse_reorder_level and not d.warehouse_reorder_qty:
frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) 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): def stock_ledger_created(self):
if not hasattr(self, "_stock_ledger_created"): if not hasattr(self, "_stock_ledger_created"):
self._stock_ledger_created = len( 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 from erpnext.assets.doctype.asset.asset import get_asset_naming_series
return 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.assertEqual(data[0].description, item.description)
self.assertTrue("description" in data[0]) 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): def set_item_variant_settings(fields):
doc = frappe.get_doc("Item Variant Settings") 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"), 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): def make_landed_cost_voucher(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

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

View File

@@ -895,6 +895,8 @@ class TestPurchaseReceipt(FrappeTestCase):
create_purchase_order, create_purchase_order,
) )
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
po = create_purchase_order() po = create_purchase_order()
pr = create_pr_against_po(po.name) pr = create_pr_against_po(po.name)
@@ -914,6 +916,7 @@ class TestPurchaseReceipt(FrappeTestCase):
po.cancel() po.cancel()
def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self): 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 = make_purchase_receipt(qty=8, do_not_submit=True)
pr1.append( pr1.append(
"items", "items",
@@ -2783,6 +2786,84 @@ class TestPurchaseReceipt(FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) 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(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier 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: if self.has_serial_no:
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0)) d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
else: else:
if sn_obj.batch_avg_rate.get(d.batch_no): d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no)))
d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty")) available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty"))
if self.docstatus == 1: 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.") self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} should be submit first.")
def check_future_entries_exists(self): def check_future_entries_exists(self):
if self.flags and self.flags.via_landed_cost_voucher:
return
if not self.has_serial_no: if not self.has_serial_no:
return return
@@ -1863,14 +1865,14 @@ def get_available_batches(kwargs):
batch_ledger.warehouse, batch_ledger.warehouse,
Sum(batch_ledger.qty).as_("qty"), Sum(batch_ledger.qty).as_("qty"),
) )
.where( .where(batch_table.disabled == 0)
(batch_table.disabled == 0)
& ((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
)
.where(stock_ledger_entry.is_cancelled == 0) .where(stock_ledger_entry.is_cancelled == 0)
.groupby(batch_ledger.batch_no, batch_ledger.warehouse) .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_date"):
if kwargs.get("posting_time") is None: if kwargs.get("posting_time") is None:
kwargs.posting_time = nowtime() 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): def get_batch_nos(doctype, txt, searchfield, start, page_len, filters):
query_filters = {} query_filters = {}
if txt: if filters.get("voucher_no") and txt:
query_filters["batch_no"] = ["like", f"%{txt}%"] query_filters["batch_no"] = ["like", f"%{txt}%"]
if filters.get("voucher_no"): if filters.get("voucher_no"):
@@ -239,5 +239,8 @@ def get_batch_nos(doctype, txt, searchfield, start, page_len, filters):
) )
else: else:
if txt:
query_filters["name"] = ["like", f"%{txt}%"]
query_filters["item"] = filters.get("item_code") query_filters["item"] = filters.get("item_code")
return frappe.get_all("Batch", filters=query_filters, as_list=True) 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.set_auto_serial_batch_entries_for_inward()
self.add_serial_nos_for_batch_item() 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) self.set_serial_batch_entries(doc)
if not doc.get("entries"): if not doc.get("entries"):
return frappe._dict({}) 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 %} {% if doc.flags.show_inclusive_tax_in_print %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}"> <div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _("Total (Without Tax)") }}</label></div> <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) }} {{ doc.get_formatted("net_total", doc) }}
</div> </div>
{% else %} {% else %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}"> <div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _(df.label) }}</label></div> <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) }} {{ doc.get_formatted("total", doc) }}
</div> </div>
{% endif %} {% endif %}