Skip to content

Commit c6e7cf1

Browse files
authored
Merge pull request #53293 from frappe/version-15-hotfix
chore: release v15
2 parents 1ee03f4 + abe433c commit c6e7cf1

26 files changed

Lines changed: 587 additions & 165 deletions

File tree

erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,10 @@ def validate_purchase_receipt_if_update_stock(self):
728728
for item in self.get("items"):
729729
if item.purchase_receipt:
730730
frappe.throw(
731-
_("Stock cannot be updated against Purchase Receipt {0}").format(
732-
item.purchase_receipt
733-
)
731+
_(
732+
"Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice."
733+
).format(self.name, item.purchase_receipt),
734+
title=_("Stock Update Not Allowed"),
734735
)
735736

736737
def validate_for_repost(self):

erpnext/accounts/doctype/sales_invoice/sales_invoice.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,9 @@ def check_prev_docstatus(self):
11991199
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
12001200

12011201
def process_asset_depreciation(self):
1202+
if self.is_internal_transfer():
1203+
return
1204+
12021205
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
12031206
self.depreciate_asset_on_sale()
12041207
else:

erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ function get_filters() {
3737
});
3838
},
3939
},
40+
{
41+
fieldname: "party_type",
42+
label: __("Party Type"),
43+
fieldtype: "Link",
44+
options: "Party Type",
45+
width: 100,
46+
},
47+
{
48+
fieldname: "party",
49+
label: __("Party"),
50+
fieldtype: "Dynamic Link",
51+
options: "party_type",
52+
width: 100,
53+
},
4054
{
4155
fieldname: "voucher_no",
4256
label: __("Voucher No"),

erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ def get_gle(self):
6868
if self.filters.period_end_date:
6969
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
7070

71+
if self.filters.party_type:
72+
filter_criterion.append(gle.party_type.eq(self.filters.party_type))
73+
74+
if self.filters.party:
75+
filter_criterion.append(gle.party.eq(self.filters.party))
76+
7177
if acc_type == "receivable":
7278
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
7379
else:
@@ -111,6 +117,12 @@ def get_ple(self):
111117
if self.filters.period_end_date:
112118
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
113119

120+
if self.filters.party_type:
121+
filter_criterion.append(ple.party_type.eq(self.filters.party_type))
122+
123+
if self.filters.party:
124+
filter_criterion.append(ple.party.eq(self.filters.party))
125+
114126
self.account_types[acc_type].ple = (
115127
qb.from_(ple)
116128
.select(

erpnext/accounts/report/gross_profit/gross_profit.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ def get_average_rate_based_on_group_by(self):
649649
new_row = row
650650
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
651651
else:
652-
new_row.qty += flt(row.qty)
652+
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
653653
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
654654

655655
new_row = self.set_average_rate(new_row)
@@ -659,11 +659,17 @@ def get_average_rate_based_on_group_by(self):
659659
if i == 0:
660660
new_row = row
661661
else:
662-
new_row.qty += flt(row.qty)
663-
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
664-
new_row.base_amount += flt(row.base_amount, self.currency_precision)
662+
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
663+
new_row.buying_amount = flt(
664+
(new_row.buying_amount + row.buying_amount), self.currency_precision
665+
)
666+
new_row.base_amount = flt(
667+
(new_row.base_amount + row.base_amount), self.currency_precision
668+
)
665669
if self.filters.get("group_by") == "Sales Person":
666-
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
670+
new_row.allocated_amount = flt(
671+
(new_row.allocated_amount + row.allocated_amount), self.currency_precision
672+
)
667673
new_row = self.set_average_rate(new_row)
668674
self.grouped_data.append(new_row)
669675

erpnext/assets/doctype/asset_repair/asset_repair.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,12 @@ frappe.ui.form.on("Asset Repair", {
111111
purchase_invoice: function (frm) {
112112
if (frm.doc.purchase_invoice) {
113113
frappe.call({
114-
method: "frappe.client.get_value",
114+
method: "erpnext.assets.doctype.asset_repair.asset_repair.get_repair_cost_for_purchase_invoice",
115115
args: {
116-
doctype: "Purchase Invoice",
117-
fieldname: "base_net_total",
118-
filters: { name: frm.doc.purchase_invoice },
116+
purchase_invoice: frm.doc.purchase_invoice,
119117
},
120118
callback: function (r) {
121-
if (r.message) {
122-
frm.set_value("repair_cost", r.message.base_net_total);
123-
}
119+
frm.set_value("repair_cost", r.message || 0);
124120
},
125121
});
126122
} else {

erpnext/assets/doctype/asset_repair/asset_repair.py

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import frappe
55
from frappe import _
6+
from frappe.query_builder.functions import Sum
67
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
78

89
import erpnext
@@ -308,9 +309,14 @@ def get_gl_entries_for_repair_cost(self, gl_entries, fixed_asset_account):
308309
if flt(self.repair_cost) <= 0:
309310
return
310311

311-
pi_expense_account = (
312-
frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account
313-
)
312+
expense_accounts = _get_expense_accounts_for_purchase_invoice(self.purchase_invoice)
313+
314+
if not expense_accounts:
315+
frappe.throw(
316+
_("No expense accounts found for Purchase Invoice {0}").format(self.purchase_invoice)
317+
)
318+
319+
pi_expense_account = expense_accounts[0]
314320

315321
gl_entries.append(
316322
self.get_gl_dict(
@@ -473,3 +479,84 @@ def calculate_last_schedule_date_before_modification(self, asset, row, extra_mon
473479
def get_downtime(failure_date, completion_date):
474480
downtime = time_diff_in_hours(completion_date, failure_date)
475481
return round(downtime, 2)
482+
483+
484+
@frappe.whitelist()
485+
def get_repair_cost_for_purchase_invoice(purchase_invoice: str) -> float:
486+
"""
487+
Get the total repair cost from GL entries for a purchase invoice.
488+
Only considers expense accounts for non-stock, non-fixed-asset items.
489+
"""
490+
if not purchase_invoice:
491+
return 0.0
492+
493+
frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True)
494+
495+
expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice)
496+
497+
if not expense_accounts:
498+
return 0.0
499+
500+
return _get_total_expense_amount(purchase_invoice, expense_accounts)
501+
502+
503+
def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]:
504+
"""
505+
Get expense accounts for non-stock items from the purchase invoice.
506+
"""
507+
pi_items = frappe.get_all(
508+
"Purchase Invoice Item",
509+
filters={"parent": purchase_invoice},
510+
fields=["item_code", "expense_account", "is_fixed_asset"],
511+
)
512+
513+
if not pi_items:
514+
return []
515+
516+
# Get list of stock item codes from the invoice
517+
item_codes = {item.item_code for item in pi_items if item.item_code}
518+
stock_items = set()
519+
if item_codes:
520+
stock_items = set(
521+
frappe.db.get_all(
522+
"Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name"
523+
)
524+
)
525+
526+
expense_accounts = set()
527+
528+
for item in pi_items:
529+
# Skip stock items - they use warehouse accounts
530+
if item.item_code and item.item_code in stock_items:
531+
continue
532+
533+
# Skip fixed assets - they use asset accounts
534+
if item.is_fixed_asset:
535+
continue
536+
537+
# Use expense account from Purchase Invoice Item
538+
if item.expense_account:
539+
expense_accounts.add(item.expense_account)
540+
541+
return list(expense_accounts)
542+
543+
544+
def _get_total_expense_amount(purchase_invoice: str, expense_accounts: list[str]) -> float:
545+
"""Get the total expense amount from GL entries for a purchase invoice and accounts."""
546+
if not expense_accounts:
547+
return 0.0
548+
549+
gl_entry = frappe.qb.DocType("GL Entry")
550+
551+
result = (
552+
frappe.qb.from_(gl_entry)
553+
.select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total"))
554+
.where(
555+
(gl_entry.voucher_type == "Purchase Invoice")
556+
& (gl_entry.voucher_no == purchase_invoice)
557+
& (gl_entry.account.isin(expense_accounts))
558+
& (gl_entry.is_cancelled == 0)
559+
)
560+
).run(as_dict=True)
561+
562+
return flt(result[0].total) if result else 0.0

erpnext/assets/doctype/asset_repair/test_asset_repair.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from frappe.query_builder.functions import Sum
99
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
1010

11+
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
1112
from erpnext.assets.doctype.asset.asset import (
1213
get_asset_account,
1314
get_asset_value_after_depreciation,
@@ -21,6 +22,7 @@
2122
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
2223
get_asset_depr_schedule_doc,
2324
)
25+
from erpnext.assets.doctype.asset_repair.asset_repair import get_repair_cost_for_purchase_invoice
2426
from erpnext.stock.doctype.item.test_item import create_item
2527
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
2628
get_serial_nos_from_bundle,
@@ -321,6 +323,59 @@ def test_gl_entries_with_capitalized_asset_repair(self):
321323
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
322324
self.assertEqual(booked_value, asset_repair.repair_cost)
323325

326+
def test_repair_cost_fetches_only_service_item_amount(self):
327+
"""Test that repair cost only includes service (non-stock) item amounts from purchase invoice."""
328+
329+
company = "_Test Company with perpetual inventory"
330+
warehouse = "Stores - TCP1"
331+
332+
service_item = create_item(
333+
"_Test Service Item for Repair",
334+
is_stock_item=0,
335+
warehouse=warehouse,
336+
company=company,
337+
)
338+
339+
stock_item = create_item(
340+
"_Test Stock Item for Repair",
341+
is_stock_item=1,
342+
warehouse=warehouse,
343+
company=company,
344+
)
345+
346+
service_expense_account = "Miscellaneous Expenses - TCP1"
347+
cost_center = frappe.db.get_value("Company", company, "cost_center")
348+
349+
pi = make_purchase_invoice(
350+
item_code=service_item.name,
351+
qty=1,
352+
rate=500,
353+
expense_account=service_expense_account,
354+
cost_center=cost_center,
355+
warehouse=warehouse,
356+
update_stock=0,
357+
do_not_submit=1,
358+
company=company,
359+
)
360+
361+
pi.update_stock = 1
362+
pi.append(
363+
"items",
364+
{
365+
"item_code": stock_item.name,
366+
"qty": 2,
367+
"rate": 300,
368+
"warehouse": warehouse,
369+
"cost_center": cost_center,
370+
},
371+
)
372+
pi.save()
373+
pi.submit()
374+
375+
repair_cost = get_repair_cost_for_purchase_invoice(pi.name)
376+
377+
self.assertEqual(repair_cost, 500)
378+
324379

325380
def num_of_depreciations(asset):
326381
return asset.finance_books[0].total_number_of_depreciations
@@ -411,6 +466,7 @@ def create_asset_repair(**args):
411466
if asset.calculate_depreciation:
412467
asset_repair.increase_in_asset_life = 12
413468
pi = make_purchase_invoice(
469+
item=args.item or "_Test Non Stock Item",
414470
company=asset.company,
415471
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
416472
cost_center=asset_repair.cost_center,

erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_update_child_supplier_quotation_add_item(self):
3737
self.assertEqual(sq.get("items")[0].qty, 5)
3838
self.assertEqual(sq.get("items")[1].rate, 300)
3939

40-
def test_update_supplier_quotation_child_rate_disallow(self):
40+
def test_update_supplier_quotation_child_rate(self):
4141
sq = frappe.copy_doc(test_records[0])
4242
sq.submit()
4343
trans_item = json.dumps(
@@ -50,6 +50,22 @@ def test_update_supplier_quotation_child_rate_disallow(self):
5050
},
5151
]
5252
)
53+
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
54+
sq.reload()
55+
self.assertEqual(sq.get("items")[0].rate, 300)
56+
po = make_purchase_order(sq.name)
57+
po.schedule_date = add_days(today(), 1)
58+
po.submit()
59+
trans_item = json.dumps(
60+
[
61+
{
62+
"item_code": sq.items[0].item_code,
63+
"rate": 20,
64+
"qty": sq.items[0].qty,
65+
"docname": sq.items[0].name,
66+
},
67+
]
68+
)
5369
self.assertRaises(
5470
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
5571
)

0 commit comments

Comments
 (0)