Compare commits

...

67 Commits

Author SHA1 Message Date
Sherin KR
9ce1bf6063 fix: item price not considering based on valid_upto
(cherry picked from commit dfda8e6241)

# Conflicts:
#	erpnext/selling/page/point_of_sale/point_of_sale.py
2025-12-22 07:36:12 +00:00
ruthra kumar
0507dae04a Merge pull request #51107 from frappe/mergify/bp/version-16-beta/pr-50837
feat(accounting-period): add role-based bypass for accounting period restrictions (backport #50837)
2025-12-15 20:52:11 +05:30
Jatin3128
fb96bbb8f8 feat(accounting-period): add role-based bypass for accounting period restrictions
(cherry picked from commit cfdbeb6a1a)
2025-12-15 12:07:08 +00:00
Smit Vora
a18ef85283 Merge pull request #51104 from frappe/mergify/bp/version-16-beta/pr-50782
fix: only show net balance as opening in general ledger (backport #50782)
2025-12-15 15:50:33 +05:30
Smit Vora
4d8ba5cca4 fix: only show net gl balance as opening in general ledger
(cherry picked from commit b7c7e0746e)
2025-12-15 10:00:47 +00:00
Sagar Vora
e930702faa Merge pull request #51060 from frappe/mergify/bp/version-16-beta/pr-51057
fix: re-calculate outstanding / write-off amount during submission (backport #51057)
2025-12-11 23:26:22 +05:30
Sagar Vora
16fe099317 fix: re-calculate outstanding / write-off amount during submission
(cherry picked from commit 09c9ac1b66)
2025-12-11 17:35:30 +00:00
Sagar Vora
626eb0d28e Merge pull request #51054 from frappe/mergify/bp/version-16-beta/pr-51051
fix: ensure fresh `grand_total_diff` is used for each calculation (backport #51051)
2025-12-11 18:07:57 +05:30
Sagar Vora
857574be69 fix: ensure fresh grand_total_diff is used for each calculation
(cherry picked from commit b3fdef8d19)
2025-12-11 12:35:29 +00:00
Khushi Rawat
f9ff06d926 Merge pull request #51008 from frappe/mergify/bp/version-16-beta/pr-50804
fix: correct logic for repair cost in asset repair (backport #50804)
2025-12-10 14:03:34 +05:30
ljain112
af9f4c7dd1 chore: remove unused import for depreciation schedule
(cherry picked from commit e1fd90f731)
2025-12-10 07:28:53 +00:00
ljain112
b81e1f8fe9 chore: remove unwanted strings
(cherry picked from commit 8ee2cbf259)
2025-12-10 07:28:53 +00:00
ljain112
5279bec1b2 refactor: linters
(cherry picked from commit 2a0ba84f69)
2025-12-10 07:28:52 +00:00
ljain112
0257fc42bb fix: add permission check
(cherry picked from commit 0c1df30771)
2025-12-10 07:28:52 +00:00
ljain112
fef76907c3 fix: add duplicate purchase invoice validation in asset repair
(cherry picked from commit ff9b392024)
2025-12-10 07:28:52 +00:00
ljain112
f12319b123 perf: enhance validation for purchase invoices to check submission status for all invoices
(cherry picked from commit 0b84d11600)
2025-12-10 07:28:52 +00:00
ljain112
945568183c perf: replace get_doc with get_lazy_doc for asset retrieval and optimize stock entry fetching
(cherry picked from commit c2810ea799)
2025-12-10 07:28:51 +00:00
ljain112
8fa785a45b fix: update repair cost logic to set value only for positive amounts
(cherry picked from commit 00ffdee928)
2025-12-10 07:28:51 +00:00
ljain112
e50ba37108 fix: remove unnecessary filtering by search text in get_expense_accounts
(cherry picked from commit b9aaae6343)
2025-12-10 07:28:51 +00:00
ljain112
c06e121cda fix: correct logic for repair cost in asset repair
(cherry picked from commit e6160d1b63)
2025-12-10 07:28:51 +00:00
Khushi Rawat
b4da06459d Merge pull request #51006 from frappe/mergify/bp/version-16-beta/pr-50999
fix: better manual budget distribution (backport #50999)
2025-12-10 12:20:29 +05:30
Khushi Rawat
b84531d4ea fix: conflicts 2025-12-10 11:59:14 +05:30
khushi8112
07c83246d0 fix: patch to set budget distribution total
(cherry picked from commit ed4c17d3a2)

# Conflicts:
#	erpnext/patches.txt
2025-12-10 06:27:36 +00:00
khushi8112
e1d46d4dad feat: show budget distribution total
(cherry picked from commit f194ac093c)
2025-12-10 06:27:35 +00:00
khushi8112
341ea89d16 fix: better manual budget distribution on update
(cherry picked from commit 1c82f42fa8)
2025-12-10 06:27:35 +00:00
khushi8112
88af49f6c0 fix: remove revise budget permission
(cherry picked from commit d42aad18a7)
2025-12-10 06:27:35 +00:00
khushi8112
7ea82ce2f0 fix: add company-based filter to account field
(cherry picked from commit 6a03fc6ede)
2025-12-10 06:27:35 +00:00
khushi8112
77f70b4c26 fix: make amount and percent field read only when distribute equally is enabled
(cherry picked from commit 75999a7ae4)
2025-12-10 06:27:34 +00:00
Smit Vora
a0c768d55d Merge pull request #50866 from frappe/mergify/bp/version-16-beta/pr-50733 2025-12-02 15:15:36 +05:30
Smit Vora
79181e307c fix: use ValueWrapper consistently
(cherry picked from commit a2fadd9347)
2025-12-02 09:13:52 +00:00
Smit Vora
9f07579a05 refactor: further changes to adapt to query builder changes
(cherry picked from commit 8235a551f0)
2025-12-02 09:13:52 +00:00
Khushi Rawat
7bfdd4b0a8 Merge pull request #50861 from frappe/mergify/bp/version-16-beta/pr-50794
fix: use asset in against_voucher while posting gl entries for capitalised asset repairs (backport #50794)
2025-12-02 12:46:09 +05:30
Khushi Rawat
a2242439c8 Merge pull request #50859 from frappe/mergify/bp/version-16-beta/pr-50793
fix: include accounting dimensions in stock entries created during asset repair. (backport #50793)
2025-12-02 12:23:37 +05:30
Navin-S-R
8440a5c68d chore: reload asset doc before assertEqual
(cherry picked from commit 8c35a6ecdd)
2025-12-02 06:37:52 +00:00
Navin-S-R
665a092cd0 test: add unit test to validate capitalized asset repair gl entries being booked against the asset
(cherry picked from commit bcf6deec9a)
2025-12-02 06:37:52 +00:00
Navin S R
a6b7985eb2 fix: use asset in against_voucher while posting gl entries for capitalized asset repairs
(cherry picked from commit a7e43eddad)
2025-12-02 06:37:52 +00:00
ljain112
30f368c4c0 refactor: show_general ledger for consistency with other doctyoes
(cherry picked from commit cdbe8b909b)
2025-12-02 06:27:36 +00:00
ljain112
be37d44d4c fix: include accounting dimensions in stock entries created during asset repair.
(cherry picked from commit 147a5ee953)
2025-12-02 06:27:36 +00:00
ruthra kumar
7ecf86af18 Merge pull request #50806 from frappe/mergify/bp/version-16-beta/pr-50802
fix(accounts-payable-summary): add Show GL Balance check similar to A… (backport #50802)
2025-11-29 12:06:33 +05:30
Jatin3128
895694f870 fix(accounts-payable-summary): add Show GL Balance check similar to Accounts Receivable Summary
(cherry picked from commit 8a7e5d0626)
2025-11-29 06:04:29 +00:00
Khushi Rawat
370ddb181b Merge pull request #50801 from frappe/mergify/bp/version-16-beta/pr-50792
fix: add Stock Entry link to Asset Repair doctype. (backport #50792)
2025-11-29 00:27:53 +05:30
ljain112
db446f33ad fix: add Stock Entry link to Asset Repair doctype.
(cherry picked from commit da7f28a3c3)
2025-11-28 12:05:40 +00:00
mergify[bot]
b7d831cc47 fix: incorrect positional param for get_field_precision util (backport #50764) (#50796) 2025-11-28 14:18:33 +05:30
Smit Vora
b8e5f3f996 fix: restore missing account number for Indirect Expenses in standard COA with Numbers (backport #50767) (#50784)
Co-authored-by: Aadhil <36843795+aadhilpm@users.noreply.github.com>
fix: restore missing account number for Indirect Expenses in standard COA with Numbers (#50767)
2025-11-27 19:41:58 +05:30
Aadhil
b8ab6403ab fix: restore missing account number for Indirect Expenses in standard COA with Numbers (#50767)
(cherry picked from commit 9145bf5563)
2025-11-27 13:54:25 +00:00
Smit Vora
0ade33e46a feat: add Account Category field to Account (Chart of Accounts) (backport #50766) (#50783)
Co-authored-by: Aadhil <36843795+aadhilpm@users.noreply.github.com>
2025-11-27 19:19:09 +05:30
Aadhil
6b877af91d feat: add Account Category field to Account (Chart of Accounts) (#50766)
(cherry picked from commit 355aa52cb8)
2025-11-27 13:11:14 +00:00
Rohit Waghchaure
4bf4df0f1d Merge branch 'develop' into version-16-beta 2025-11-21 22:08:39 +05:30
Lakshit Jain
1b32889eed Merge pull request #50682 from frappe/mergify/bp/version-16-beta/pr-50609
fix: handle empty item_tax_rate in ItemTax class (backport #50609)
2025-11-21 18:46:38 +05:30
Karm Soni
538b54a2dc fix: handle empty item_tax_rate in ItemTax class
(cherry picked from commit fc098a732b)
2025-11-21 12:57:48 +00:00
Lakshit Jain
11eda0e051 Merge pull request #50676 from frappe/mergify/bp/version-16-beta/pr-50675
fix: ignore chunk if no valid invoices found (backport #50675)
2025-11-21 18:27:34 +05:30
Smit Vora
cca315d072 Merge pull request #50679 from frappe/mergify/bp/version-16-beta/pr-50658
fix: handle zero rate actual taxes in calculate_taxes_and_totals (backport #50658)
2025-11-21 17:10:21 +05:30
ljain112
843a38acfd fix: handle zero rate actual taxes in calculate_taxes_and_totals
(cherry picked from commit ef37e6aa16)
2025-11-21 11:14:41 +00:00
Sagar Vora
434d05f9cf fix: ignore chunk if no valid invoices found
(cherry picked from commit f644c19760)
2025-11-21 08:53:52 +00:00
ruthra kumar
71611f81c9 Merge pull request #50630 from frappe/mergify/bp/version-16-beta/pr-50623
fix: replace `this` with function path (backport #50623)
2025-11-19 16:21:09 +05:30
Smit Vora
635047c290 fix: replace this with function path
(cherry picked from commit 2a1eb08b08)
2025-11-19 10:49:36 +00:00
Mihir Kandoi
82ba11e432 Merge pull request #50616 from frappe/mergify/bp/version-16-beta/pr-50614
fix: redundant message on bom save (backport #50614)
2025-11-19 12:55:56 +05:30
Mihir Kandoi
9374958473 fix: redundant message on bom save
(cherry picked from commit 074f07694f)
2025-11-19 07:08:17 +00:00
ruthra kumar
fe73f43d8b Merge pull request #50604 from frappe/mergify/bp/version-16-beta/pr-50524
fix: use dynamic account type to get average ratio balance (backport #50524)
2025-11-18 17:44:25 +05:30
ruthra kumar
dd93af80e0 Merge pull request #50602 from frappe/mergify/bp/version-16-beta/pr-50516
fix(general_ledger): add translation for accounting dimension (backport #50516)
2025-11-18 17:13:19 +05:30
Navin-S-R
a2fe72013f fix: correct profit after tax calculation by reducing expenses from income
(cherry picked from commit f420371a7e)
2025-11-18 11:22:47 +00:00
Navin-S-R
2b1f31bacb fix: use dynamic account type to get average ratio balance
(cherry picked from commit 9118f08e7b)
2025-11-18 11:22:47 +00:00
Logesh Periyasamy
e87e9b4769 fix(general_ledger): add translation for accounting dimension
(cherry picked from commit 113ff17c71)
2025-11-18 11:16:36 +00:00
ruthra kumar
3830dc9099 Merge pull request #50586 from frappe/mergify/bp/version-16-beta/pr-50529
fix: unintended backported depends_on expression (backport #50529)
2025-11-18 15:05:49 +05:30
rohitwaghchaure
42c1f95981 Merge pull request #50590 from frappe/mergify/bp/version-16-beta/pr-50585
fix: icons for workspace sidebar in ERPNext modules (backport #50585)
2025-11-18 14:35:56 +05:30
Rohit Waghchaure
052ce11d0f fix: icons for workspace sidebar in ERPNext modules
(cherry picked from commit 842546d917)
2025-11-18 08:42:07 +00:00
Kavin
9cafee3b59 fix: unintended backported depends_on expression (#50529)
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
(cherry picked from commit 81a16286a1)
2025-11-18 08:08:01 +00:00
26 changed files with 595 additions and 171 deletions

View File

@@ -160,6 +160,14 @@ frappe.treeview_settings["Account"] = {
.options,
description: __("Optional. This setting will be used to filter in various transactions."),
},
{
fieldtype: "Link",
fieldname: "account_category",
label: __("Account Category"),
options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_category")[0]
.options,
description: __("Optional. Used with Financial Report Template"),
},
{
fieldtype: "Float",
fieldname: "tax_rate",

View File

@@ -228,6 +228,7 @@ def get():
},
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
"account_number": "5200",
},
"root_type": "Expense",
"account_number": "5000",

View File

@@ -12,6 +12,7 @@
"column_break_4",
"company",
"disabled",
"exempted_role",
"section_break_7",
"closed_documents"
],
@@ -67,10 +68,18 @@
"label": "Closed Documents",
"options": "Closed Document",
"reqd": 1
},
{
"description": "Role allowed to bypass period restrictions.",
"fieldname": "exempted_role",
"fieldtype": "Link",
"label": "Exempted Role",
"link_filters": "[[\"Role\",\"disabled\",\"=\",0]]",
"options": "Role"
}
],
"links": [],
"modified": "2025-10-06 15:00:15.568067",
"modified": "2025-12-01 16:53:44.631299",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Period",

View File

@@ -30,6 +30,7 @@ class AccountingPeriod(Document):
company: DF.Link
disabled: DF.Check
end_date: DF.Date
exempted_role: DF.Link | None
period_name: DF.Data
start_date: DF.Date
# end: auto-generated types
@@ -113,7 +114,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
accounting_period = (
frappe.qb.from_(ap)
.from_(cd)
.select(ap.name)
.select(ap.name, ap.exempted_role)
.where(
(ap.name == cd.parent)
& (ap.company == doc.company)
@@ -126,6 +127,11 @@ def validate_accounting_period_on_doc_save(doc, method=None):
).run(as_dict=1)
if accounting_period:
if (
accounting_period[0].get("exempted_role")
and accounting_period[0].get("exempted_role") in frappe.get_roles()
):
return
frappe.throw(
_("You cannot create a {0} within the closed Accounting Period {1}").format(
doc.doctype, frappe.bold(accounting_period[0]["name"])

View File

@@ -12,6 +12,15 @@ frappe.ui.form.on("Budget", {
};
});
frm.set_query("account", function () {
return {
filters: {
is_group: 0,
company: frm.doc.company,
},
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
if (value) {
@@ -24,24 +33,16 @@ frappe.ui.form.on("Budget", {
frm.trigger("toggle_reqd_fields");
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
let exception_role = await frappe.db.get_value(
"Company",
frm.doc.company,
"exception_budget_approver_role"
frm.add_custom_button(
__("Revise Budget"),
function () {
frm.events.revise_budget_action(frm);
},
__("Actions")
);
const role = exception_role.message.exception_budget_approver_role;
if (role && frappe.user.has_role(role)) {
frm.add_custom_button(
__("Revise Budget"),
function () {
frm.events.revise_budget_action(frm);
},
__("Actions")
);
}
}
toggle_distribution_fields(frm);
},
budget_against: function (frm) {
@@ -54,10 +55,15 @@ frappe.ui.form.on("Budget", {
frm.doc.budget_distribution.forEach((row) => {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
});
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
distribute_equally: function (frm) {
toggle_distribution_fields(frm);
},
set_null_value: function (frm) {
if (frm.doc.budget_against == "Cost Center") {
frm.set_value("project", null);
@@ -100,6 +106,8 @@ frappe.ui.form.on("Budget Distribution", {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
@@ -107,7 +115,29 @@ frappe.ui.form.on("Budget Distribution", {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
});
function set_total_budget_amount(frm) {
let total = 0;
(frm.doc.budget_distribution || []).forEach((row) => {
total += flt(row.amount);
});
frm.set_value("budget_distribution_total", total);
}
function toggle_distribution_fields(frm) {
const grid = frm.fields_dict.budget_distribution.grid;
["amount", "percent"].forEach((field) => {
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
});
grid.refresh();
}

View File

@@ -25,6 +25,10 @@
"distribute_equally",
"section_break_fpdt",
"budget_distribution",
"section_break_wkqb",
"column_break_paum",
"column_break_nwor",
"budget_distribution_total",
"section_break_6",
"applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr",
@@ -222,7 +226,8 @@
},
{
"fieldname": "section_break_fpdt",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "budget_distribution",
@@ -303,13 +308,32 @@
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "section_break_wkqb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_paum",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_nwor",
"fieldtype": "Column Break"
},
{
"fieldname": "budget_distribution_total",
"fieldtype": "Currency",
"label": "Budget Distribution Total",
"no_copy": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-19 17:00:00.648224",
"modified": "2025-12-10 02:35:01.197613",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",

View File

@@ -53,6 +53,7 @@ class Budget(Document):
budget_against: DF.Literal["", "Cost Center", "Project"]
budget_amount: DF.Currency
budget_distribution: DF.Table[BudgetDistribution]
budget_distribution_total: DF.Currency
budget_end_date: DF.Date | None
budget_start_date: DF.Date | None
company: DF.Link
@@ -230,28 +231,49 @@ class Budget(Document):
def before_save(self):
self.allocate_budget()
self.budget_distribution_total = sum(flt(row.amount) for row in self.budget_distribution)
def on_update(self):
self.validate_distribution_totals()
def allocate_budget(self):
if self.revision_of:
if self._should_skip_allocation():
return
if self._should_recalculate_manual_distribution():
self._recalculate_manual_distribution()
return
if not self.should_regenerate_budget_distribution():
return
self.set("budget_distribution", [])
self._regenerate_distribution()
periods = self.get_budget_periods()
total_periods = len(periods)
row_percent = 100 / total_periods if total_periods else 0
def _should_skip_allocation(self):
return self.revision_of and not self.distribute_equally
for start_date, end_date in periods:
row = self.append("budget_distribution", {})
row.start_date = start_date
row.end_date = end_date
self.add_allocated_amount(row, row_percent)
def _should_recalculate_manual_distribution(self):
return (
not self.distribute_equally
and bool(self.budget_distribution)
and self._is_only_budget_amount_changed()
)
def _is_only_budget_amount_changed(self):
old = self.get_doc_before_save()
if not old:
return False
return (
old.budget_amount != self.budget_amount
and old.distribution_frequency == self.distribution_frequency
and old.budget_start_date == self.budget_start_date
and old.budget_end_date == self.budget_end_date
)
def _recalculate_manual_distribution(self):
for row in self.budget_distribution:
row.amount = flt((row.percent / 100) * self.budget_amount, 3)
def should_regenerate_budget_distribution(self):
"""Check whether budget distribution should be recalculated."""
@@ -265,7 +287,6 @@ class Budget(Document):
"to_fiscal_year",
"budget_amount",
"distribution_frequency",
"distribute_equally",
]
for field in changed_fields:
if old_doc.get(field) != self.get(field):
@@ -273,6 +294,21 @@ class Budget(Document):
return bool(self.distribute_equally)
def _regenerate_distribution(self):
self.set("budget_distribution", [])
periods = self.get_budget_periods()
total_periods = len(periods)
row_percent = 100 / total_periods if total_periods else 0
for start_date, end_date in periods:
row = self.append("budget_distribution", {})
row.start_date = start_date
row.end_date = end_date
self.add_allocated_amount(row, row_percent)
self.budget_distribution_total = self.budget_amount
def get_budget_periods(self):
"""Return list of (start_date, end_date) tuples based on frequency."""
frequency = self.distribution_frequency
@@ -312,12 +348,8 @@ class Budget(Document):
}.get(frequency, 1)
def add_allocated_amount(self, row, row_percent):
if not self.distribute_equally:
row.amount = 0
row.percent = 0
else:
row.amount = flt(self.budget_amount * row_percent / 100, 3)
row.percent = flt(row_percent, 3)
row.amount = flt(self.budget_amount * row_percent / 100, 3)
row.percent = flt(row_percent, 3)
def validate_distribution_totals(self):
if self.should_regenerate_budget_distribution():

View File

@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
currency=company_currency,
)
if account_details:

View File

@@ -442,7 +442,7 @@ def update_against_account(voucher_type, voucher_no):
if not entries:
return
company_currency = erpnext.get_company_currency(entries[0].company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
accounts_debited, accounts_credited = [], []
for d in entries:

View File

@@ -301,7 +301,9 @@ def merge_similar_entries(gl_map, precision=None):
company_currency = erpnext.get_company_currency(company)
if not precision:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
)
# filter zero debit and credit entries
merged_gl_map = filter(

View File

@@ -102,6 +102,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
label: __("Revaluation Journals"),
fieldtype: "Check",
},
{
fieldname: "show_gl_balance",
label: __("Show GL Balance"),
fieldtype: "Check",
},
],
onload: function (report) {

View File

@@ -53,7 +53,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company, self.account_type)
for party, party_dict in self.party_total.items():
if flt(party_dict.outstanding, self.currency_precision) == 0:
@@ -206,11 +206,15 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
def get_gl_balance(report_date, company):
def get_gl_balance(report_date, company, account_type):
if account_type == "Payable":
balance_calc_fields = ["party", {"SUM": [{"SUB": ["credit", "debit"]}], "as": "balance"}]
else:
balance_calc_fields = ["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}]
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}],
fields=balance_calc_fields,
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,

View File

@@ -500,7 +500,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
immutable_ledger = frappe.get_single_value("Accounts Settings", "enable_immutable_ledger")
def update_value_in_dict(data, key, gle):
def update_value_in_dict(data, key, gle, show_net_values=False):
data[key].debit += gle.debit
data[key].credit += gle.credit
@@ -511,10 +511,14 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
"Receivable",
"Payable",
):
if (
filters.get("show_net_values_in_party_account")
and account_type_map.get(data[key].account)
in (
"Receivable",
"Payable",
)
) or show_net_values:
net_value = data[key].debit - data[key].credit
net_value_in_account_currency = (
data[key].debit_in_account_currency - data[key].credit_in_account_currency
@@ -548,11 +552,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if not group_by_voucher_consolidated:
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)
update_value_in_dict(totals, "opening", gle)
update_value_in_dict(totals, "closing", gle)
update_value_in_dict(totals, "opening", gle, True)
update_value_in_dict(totals, "closing", gle, True)
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
if not group_by_voucher_consolidated:

View File

@@ -73,14 +73,7 @@ frappe.ui.form.on("Asset Repair", {
},
refresh: function (frm) {
if (frm.doc.docstatus) {
frm.add_custom_button(__("View General Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
};
frappe.set_route("query-report", "General Ledger");
});
}
frm.events.show_general_ledger(frm);
let sbb_field = frm.get_docfield("stock_items", "serial_and_batch_bundle");
if (sbb_field) {
@@ -124,24 +117,71 @@ frappe.ui.form.on("Asset Repair", {
frm.refresh_field("stock_items");
}
},
});
purchase_invoice: function (frm) {
if (frm.doc.purchase_invoice) {
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Purchase Invoice",
fieldname: "base_net_total",
filters: { name: frm.doc.purchase_invoice },
},
callback: function (r) {
if (r.message) {
frm.set_value("repair_cost", r.message.base_net_total);
frappe.ui.form.on("Asset Repair Purchase Invoice", {
purchase_invoice: function (frm, cdt, cdn) {
frappe.model.set_value(cdt, cdn, {
expense_account: "",
repair_cost: 0,
});
},
expense_account: function (frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (!row.purchase_invoice || !row.expense_account) {
frappe.model.set_value(cdt, cdn, "repair_cost", 0);
return;
}
frappe.call({
method: "erpnext.assets.doctype.asset_repair.asset_repair.get_unallocated_repair_cost",
args: {
purchase_invoice: row.purchase_invoice,
expense_account: row.expense_account,
},
callback: function (r) {
if (r.message !== undefined) {
if (r.message > 0) {
frappe.model.set_value(cdt, cdn, "repair_cost", r.message);
} else {
frappe.model.set_value(cdt, cdn, "repair_cost", 0);
let pi_link = frappe.utils.get_form_link(
"Purchase Invoice",
row.purchase_invoice,
true
);
frappe.msgprint({
message: __(
"Row {0}: The entire expense amount for account {1} in {2} has already been allocated.",
[row.idx, row.expense_account.bold(), pi_link]
),
indicator: "orange",
});
}
}
},
});
},
show_general_ledger: (frm) => {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Accounting Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
});
} else {
frm.set_value("repair_cost", 0);
__("View")
);
}
},
});

View File

@@ -260,8 +260,13 @@
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-04 23:06:43.644846",
"links": [
{
"link_doctype": "Stock Entry",
"link_fieldname": "asset_repair"
}
],
"modified": "2025-11-28 13:04:34.921098",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",

View File

@@ -4,14 +4,17 @@
import frappe
from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
reschedule_depreciation,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -56,7 +59,7 @@ class AssetRepair(AccountsController):
# end: auto-generated types
def validate(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
self.asset_doc = frappe.get_lazy_doc("Asset", self.asset)
self.validate_asset()
self.validate_dates()
self.validate_purchase_invoices()
@@ -81,60 +84,91 @@ class AssetRepair(AccountsController):
)
def validate_purchase_invoices(self):
self.validate_duplicate_purchase_invoices()
self.validate_purchase_invoice_status()
for d in self.invoices:
self.validate_purchase_invoice_status(d.purchase_invoice)
invoice_items = self.get_invoice_items(d.purchase_invoice)
self.validate_service_purchase_invoice(d.purchase_invoice, invoice_items)
self.validate_expense_account(d, invoice_items)
self.validate_purchase_invoice_repair_cost(d, invoice_items)
self.validate_expense_account(d)
self.validate_purchase_invoice_repair_cost(d)
def validate_purchase_invoice_status(self, purchase_invoice):
docstatus = frappe.db.get_value("Purchase Invoice", purchase_invoice, "docstatus")
if docstatus == 0:
frappe.throw(
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
get_link_to_form("Purchase Invoice", purchase_invoice)
)
def validate_duplicate_purchase_invoices(self):
# account wise duplicate check
purchase_invoices = set()
duplicates = []
for row in self.invoices:
key = (row.purchase_invoice, row.expense_account)
if key in purchase_invoices:
duplicates.append((row.idx, row.purchase_invoice, row.expense_account))
else:
purchase_invoices.add(key)
if duplicates:
duplicate_links = "".join(
[
f"<li>{_('Row #{0}:').format(idx)} {get_link_to_form('Purchase Invoice', pi)} - {frappe.bold(account)}</li>"
for idx, pi, account in duplicates
]
)
msg = _("The following rows are duplicates:") + f"<br><ul>{duplicate_links}</ul>"
frappe.throw(msg)
def get_invoice_items(self, pi):
invoice_items = frappe.get_all(
"Purchase Invoice Item",
filters={"parent": pi},
fields=["item_code", "expense_account", "base_net_amount"],
def validate_purchase_invoice_status(self):
pi_names = [row.purchase_invoice for row in self.invoices]
docstatus = frappe._dict(
frappe.db.get_all(
"Purchase Invoice",
filters={"name": ["in", pi_names]},
fields=["name", "docstatus"],
as_list=True,
)
)
return invoice_items
invalid_invoice = []
for row in self.invoices:
if docstatus.get(row.purchase_invoice) != 1:
invalid_invoice.append((row.idx, row.purchase_invoice))
def validate_service_purchase_invoice(self, purchase_invoice, invoice_items):
service_item_exists = False
for item in invoice_items:
if frappe.db.get_value("Item", item.item_code, "is_stock_item") == 0:
service_item_exists = True
break
if invalid_invoice:
invoice_links = "".join(
[
f"<li>{_('Row #{0}:').format(idx)} {get_link_to_form('Purchase Invoice', pi)}</li>"
for idx, pi in invalid_invoice
]
)
msg = _("The following Purchase Invoices are not submitted:") + f"<br><ul>{invoice_links}</ul>"
frappe.throw(msg)
if not service_item_exists:
def validate_expense_account(self, row):
"""Validate that the expense account exists in the purchase invoice for non-stock items."""
valid_accounts = _get_expense_accounts_for_purchase_invoice(row.purchase_invoice)
if row.expense_account not in valid_accounts:
frappe.throw(
_("Service item not present in Purchase Invoice {0}").format(
get_link_to_form("Purchase Invoice", purchase_invoice)
_(
"Row #{0}: Expense account {1} is not valid for Purchase Invoice {2}. "
"Only expense accounts from non-stock items are allowed."
).format(
row.idx,
frappe.bold(row.expense_account),
get_link_to_form("Purchase Invoice", row.purchase_invoice),
)
)
def validate_expense_account(self, row, invoice_items):
pi_expense_accounts = set([item.expense_account for item in invoice_items])
if row.expense_account not in pi_expense_accounts:
frappe.throw(
_("Expense account {0} not present in Purchase Invoice {1}").format(
row.expense_account, get_link_to_form("Purchase Invoice", row.purchase_invoice)
)
)
def validate_purchase_invoice_repair_cost(self, row):
"""Validate that repair cost doesn't exceed available amount."""
available_amount = get_unallocated_repair_cost(
row.purchase_invoice, row.expense_account, exclude_asset_repair=self.name
)
def validate_purchase_invoice_repair_cost(self, row, invoice_items):
pi_net_total = sum([flt(item.base_net_amount) for item in invoice_items])
if flt(row.repair_cost) > pi_net_total:
if flt(row.repair_cost) > available_amount:
frappe.throw(
_("Repair cost cannot be greater than purchase invoice base net total {0}").format(
pi_net_total
_(
"Row #{0}: Repair cost {1} exceeds available amount {2} for Purchase Invoice {3} and Account {4}"
).format(
row.idx,
frappe.bold(frappe.format_value(row.repair_cost, {"fieldtype": "Currency"})),
frappe.bold(frappe.format_value(available_amount, {"fieldtype": "Currency"})),
get_link_to_form("Purchase Invoice", row.purchase_invoice),
frappe.bold(row.expense_account),
)
)
@@ -196,7 +230,7 @@ class AssetRepair(AccountsController):
self.cancel_sabb()
def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status()
frappe.get_lazy_doc("Asset", self.asset).set_status()
def check_repair_status(self):
if self.repair_status == "Pending" and self.docstatus == 1:
@@ -231,6 +265,12 @@ class AssetRepair(AccountsController):
}
)
accounting_dimensions = {
"cost_center": self.cost_center,
"project": self.project,
**{dimension: self.get(dimension) for dimension in get_accounting_dimensions()},
}
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
@@ -242,8 +282,7 @@ class AssetRepair(AccountsController):
"qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate,
"serial_and_batch_bundle": stock_item.serial_and_batch_bundle,
"cost_center": self.cost_center,
"project": self.project,
**accounting_dimensions,
},
)
@@ -320,7 +359,8 @@ class AssetRepair(AccountsController):
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Purchase Invoice",
"against_voucher_type": "Asset",
"against_voucher": self.asset,
"company": self.company,
},
item=self,
@@ -332,7 +372,10 @@ class AssetRepair(AccountsController):
return
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
stock_entry = frappe.get_doc("Stock Entry", {"asset_repair": self.name})
stock_entry_name = frappe.db.get_value("Stock Entry", {"asset_repair": self.name}, "name")
stock_entry_items = frappe.get_all(
"Stock Entry Detail", filters={"parent": stock_entry_name}, fields=["expense_account", "amount"]
)
default_expense_account = None
if not erpnext.is_perpetual_inventory_enabled(self.company):
@@ -342,7 +385,7 @@ class AssetRepair(AccountsController):
if not default_expense_account:
frappe.throw(_("Please set default Expense Account in Company {0}").format(self.company))
for item in stock_entry.items:
for item in stock_entry_items:
if flt(item.amount) > 0:
gl_entries.append(
self.get_gl_dict(
@@ -373,7 +416,7 @@ class AssetRepair(AccountsController):
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Stock Entry",
"against_voucher": stock_entry.name,
"against_voucher": stock_entry_name,
"company": self.company,
},
item=self,
@@ -411,33 +454,152 @@ def get_downtime(failure_date, completion_date):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
PurchaseInvoice = DocType("Purchase Invoice")
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
Item = DocType("Item")
"""
Get Purchase Invoices that have expense accounts for non-stock items.
Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account.
"""
pi = DocType("Purchase Invoice")
pi_item = DocType("Purchase Invoice Item")
item = DocType("Item")
return (
frappe.qb.from_(PurchaseInvoice)
.join(PurchaseInvoiceItem)
.on(PurchaseInvoiceItem.parent == PurchaseInvoice.name)
.join(Item)
.on(Item.name == PurchaseInvoiceItem.item_code)
.select(PurchaseInvoice.name)
query = (
frappe.qb.from_(pi)
.join(pi_item)
.on(pi_item.parent == pi.name)
.left_join(item)
.on(item.name == pi_item.item_code)
.select(pi.name)
.distinct()
.where(
(Item.is_stock_item == 0)
& (Item.is_fixed_asset == 0)
& (PurchaseInvoice.company == filters.get("company"))
& (PurchaseInvoice.docstatus == 1)
(pi.company == filters.get("company"))
& (pi.docstatus == 1)
& (pi_item.is_fixed_asset == 0)
& (pi_item.expense_account.isnotnull())
& (pi_item.expense_account != "")
& ((pi_item.item_code.isnull()) | (item.is_stock_item == 0))
)
).run(as_list=1)
)
if txt:
query = query.where(pi.name.like(f"%{txt}%"))
return query.run(as_list=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters):
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
return (
frappe.qb.from_(PurchaseInvoiceItem)
.select(PurchaseInvoiceItem.expense_account)
.distinct()
.where(PurchaseInvoiceItem.parent == filters.get("purchase_invoice"))
).run(as_list=1)
"""
Get expense accounts for non-stock (service) items from the purchase invoice.
Used as a query function for link fields.
"""
purchase_invoice = filters.get("purchase_invoice")
if not purchase_invoice:
return []
expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice)
return [[account] for account in expense_accounts]
def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]:
"""
Get expense accounts for non-stock items from the purchase invoice.
"""
pi_items = frappe.db.get_all(
"Purchase Invoice Item",
filters={"parent": purchase_invoice},
fields=["item_code", "expense_account", "is_fixed_asset"],
)
if not pi_items:
return []
# Get list of stock item codes from the invoice
item_codes = {item.item_code for item in pi_items if item.item_code}
stock_items = set()
if item_codes:
stock_items = set(
frappe.db.get_all(
"Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name"
)
)
expense_accounts = set()
for item in pi_items:
# Skip stock items - they use warehouse accounts
if item.item_code and item.item_code in stock_items:
continue
# Skip fixed assets - they use asset accounts
if item.is_fixed_asset:
continue
# Use expense account from Purchase Invoice Item
if item.expense_account:
expense_accounts.add(item.expense_account)
return list(expense_accounts)
@frappe.whitelist()
def get_unallocated_repair_cost(
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
) -> float:
"""
Calculate the unused repair cost for a purchase invoice and expense account.
"""
if not purchase_invoice or not expense_account:
return 0.0
frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True)
used_amount = get_allocated_repair_cost(purchase_invoice, expense_account, exclude_asset_repair)
total_amount = get_total_expense_amount(purchase_invoice, expense_account)
return flt(total_amount - used_amount)
def get_allocated_repair_cost(
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
) -> float:
"""Get the total repair cost already allocated from submitted Asset Repairs."""
asset_repair_pi = DocType("Asset Repair Purchase Invoice")
query = (
frappe.qb.from_(asset_repair_pi)
.select(Sum(asset_repair_pi.repair_cost).as_("total"))
.where(
(asset_repair_pi.purchase_invoice == purchase_invoice)
& (asset_repair_pi.expense_account == expense_account)
& (asset_repair_pi.docstatus == 1)
)
)
if exclude_asset_repair:
query = query.where(asset_repair_pi.parent != exclude_asset_repair)
result = query.run(as_dict=True)
return flt(result[0].total) if result else 0.0
def get_total_expense_amount(purchase_invoice: str, expense_account: str) -> float:
"""Get the total expense amount from GL entries for a purchase invoice and account."""
gl_entry = DocType("GL Entry")
result = (
frappe.qb.from_(gl_entry)
.select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total"))
.where(
(gl_entry.voucher_type == "Purchase Invoice")
& (gl_entry.voucher_no == purchase_invoice)
& (gl_entry.account == expense_account)
& (gl_entry.is_cancelled == 0)
)
).run(as_dict=True)
return flt(result[0].total) if result else 0.0

View File

@@ -3,6 +3,8 @@
import unittest
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
@@ -173,6 +175,39 @@ class TestAssetRepair(IntegrationTestCase):
)
self.assertTrue(asset_repair.invoices)
def test_repair_cost_exceeds_available_amount(self):
"""Test that repair cost cannot exceed available amount from Purchase Invoice."""
asset_repair1 = create_asset_repair(
capitalize_repair_cost=1,
item="_Test Non Stock Item",
submit=1,
)
pi_name = asset_repair1.invoices[0].purchase_invoice
expense_account = asset_repair1.invoices[0].expense_account
asset_repair2 = frappe.new_doc("Asset Repair")
asset_repair2.update(
{
"asset": asset_repair1.asset,
"asset_name": asset_repair1.asset_name,
"failure_date": nowdate(),
"description": "Second Repair",
"company": asset_repair1.company,
"capitalize_repair_cost": 1,
}
)
asset_repair2.append(
"invoices",
{
"purchase_invoice": pi_name,
"expense_account": expense_account,
"repair_cost": 10, # PI already fully used, so this should fail
},
)
self.assertRaises(frappe.ValidationError, asset_repair2.save)
def test_gl_entries_with_perpetual_inventory(self):
set_depreciation_settings_in_company(company="_Test Company with perpetual inventory")
@@ -322,6 +357,31 @@ class TestAssetRepair(IntegrationTestCase):
stock_entry = frappe.get_last_doc("Stock Entry")
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
def test_gl_entries_with_capitalized_asset_repair(self):
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
asset_repair = create_asset_repair(
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
)
asset.reload()
GLEntry = qb.DocType("GL Entry")
res = (
qb.from_(GLEntry)
.select(Sum(GLEntry.debit_in_account_currency).as_("total_debit"))
.where(
(GLEntry.voucher_type == "Asset Repair")
& (GLEntry.voucher_no == asset_repair.name)
& (GLEntry.against_voucher_type == "Asset")
& (GLEntry.against_voucher == asset.name)
& (GLEntry.company == asset.company)
& (GLEntry.is_cancelled == 0)
)
).run(as_dict=True)
booked_value = res[0].total_debit if res else 0
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
self.assertEqual(booked_value, asset_repair.repair_cost)
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations + (

View File

@@ -186,7 +186,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
frappe.get_meta(doc.doctype + " Item").get_field(
"stock_qty" if doc.get("update_stock", "") else "qty"
),
company_currency,
currency=company_currency,
)
for column in fields:

View File

@@ -397,6 +397,9 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
# reset value from earlier calculations
self.grand_total_diff = 0
doc = self.doc
if not doc.get("taxes"):
return
@@ -683,7 +686,7 @@ class calculate_taxes_and_totals:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
grand_total_diff = self.grand_total_diff
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
@@ -924,12 +927,11 @@ class calculate_taxes_and_totals:
)
)
if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
def is_internal_invoice(self):
"""

View File

@@ -7,6 +7,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, flt, sbool
from pypika.terms import ValueWrapper
from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
@@ -373,7 +374,7 @@ def get_children(doctype=None, parent=None, **kwargs):
"parent as parent_id",
"qty",
"idx",
"'BOM Creator Item' as doctype",
ValueWrapper("BOM Creator Item").as_("doctype"),
"name",
"uom",
"rate",

View File

@@ -449,3 +449,4 @@ erpnext.patches.v16_0.set_company_wise_warehouses
erpnext.patches.v16_0.set_valuation_method_on_companies
erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table
erpnext.patches.v16_0.migrate_budget_records_to_new_structure
erpnext.patches.v16_0.populate_budget_distribution_total

View File

@@ -1,4 +1,5 @@
import frappe
from pypika.terms import ValueWrapper
from erpnext.accounts.general_ledger import make_reverse_gl_entries
@@ -39,7 +40,7 @@ def execute():
"payment_amount",
# at the time of creating this dunning, the full amount was outstanding
"payment_amount as outstanding",
"'0' as paid_amount",
ValueWrapper(0).as_("paid_amount"),
"discounted_amount",
],
)

View File

@@ -0,0 +1,11 @@
import frappe
from frappe.utils import flt
def execute():
budgets = frappe.get_all("Budget", filters={"docstatus": ["in", [0, 1]]}, fields=["name"])
for b in budgets:
doc = frappe.get_doc("Budget", b.name)
total = sum(flt(row.amount) for row in doc.budget_distribution)
doc.db_set("budget_distribution_total", total, update_modified=False)

View File

@@ -383,7 +383,7 @@ def get_timesheet_data(name, project):
data = frappe.get_all(
"Timesheet",
fields=[
"(total_billable_amount - total_billed_amount) as billing_amt",
{"SUB": ["total_billable_amount", "total_billed_amount"], "as": "billing_amt"},
"total_billable_hours as billing_hours",
],
filters={"name": name},

View File

@@ -380,6 +380,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
calculate_taxes() {
// reset value from earlier calculations
this.grand_total_diff = 0;
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
@@ -617,6 +620,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (diff && Math.abs(diff) <= 5.0 / Math.pow(10, precision("tax_amount", last_tax))) {
me.grand_total_diff = diff;
} else {
me.grand_total_diff = 0;
}
}
}
@@ -626,7 +631,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
const me = this;
const tax_count = this.frm.doc.taxes?.length;
const grand_total_diff = this.grand_total_diff || 0;
const grand_total_diff = this.grand_total_diff;
this.frm.doc.grand_total = flt(
tax_count ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff : this.frm.doc.net_total

View File

@@ -5,7 +5,12 @@
import json
import frappe
<<<<<<< HEAD
from frappe.utils import cint, get_datetime
=======
from frappe.query_builder import DocType, Order
from frappe.utils import cint
>>>>>>> dfda8e6241 (fix: item price not considering based on valid_upto)
from frappe.utils.nestedset import get_root_of
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_item_group, get_stock_availability
@@ -200,18 +205,24 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
for item in items_data:
item.actual_qty, _, is_negative_stock_allowed = get_stock_availability(item.item_code, warehouse)
item_prices = frappe.get_all(
"Item Price",
fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"],
filters={
"price_list": price_list,
"item_code": item.item_code,
"selling": True,
"valid_from": ["<=", current_date],
"valid_upto": ["in", [None, "", current_date]],
},
order_by="valid_from desc",
)
ItemPrice = DocType("Item Price")
item_prices = (
frappe.qb.from_(ItemPrice)
.select(
ItemPrice.price_list_rate,
ItemPrice.currency,
ItemPrice.uom,
ItemPrice.batch_no,
ItemPrice.valid_from,
ItemPrice.valid_upto,
)
.where(ItemPrice.price_list == price_list)
.where(ItemPrice.item_code == item.item_code)
.where(ItemPrice.selling == 1)
.where((ItemPrice.valid_from <= current_date) | (ItemPrice.valid_from.isnull()))
.where((ItemPrice.valid_upto >= current_date) | (ItemPrice.valid_upto.isnull()))
.orderby(ItemPrice.valid_from, order=Order.desc)
).run(as_dict=True)
stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {})
item_uom = item.stock_uom