mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-11 08:53:03 +00:00
Compare commits
67 Commits
assets-dev
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ce1bf6063 | ||
|
|
0507dae04a | ||
|
|
fb96bbb8f8 | ||
|
|
a18ef85283 | ||
|
|
4d8ba5cca4 | ||
|
|
e930702faa | ||
|
|
16fe099317 | ||
|
|
626eb0d28e | ||
|
|
857574be69 | ||
|
|
f9ff06d926 | ||
|
|
af9f4c7dd1 | ||
|
|
b81e1f8fe9 | ||
|
|
5279bec1b2 | ||
|
|
0257fc42bb | ||
|
|
fef76907c3 | ||
|
|
f12319b123 | ||
|
|
945568183c | ||
|
|
8fa785a45b | ||
|
|
e50ba37108 | ||
|
|
c06e121cda | ||
|
|
b4da06459d | ||
|
|
b84531d4ea | ||
|
|
07c83246d0 | ||
|
|
e1d46d4dad | ||
|
|
341ea89d16 | ||
|
|
88af49f6c0 | ||
|
|
7ea82ce2f0 | ||
|
|
77f70b4c26 | ||
|
|
a0c768d55d | ||
|
|
79181e307c | ||
|
|
9f07579a05 | ||
|
|
7bfdd4b0a8 | ||
|
|
a2242439c8 | ||
|
|
8440a5c68d | ||
|
|
665a092cd0 | ||
|
|
a6b7985eb2 | ||
|
|
30f368c4c0 | ||
|
|
be37d44d4c | ||
|
|
7ecf86af18 | ||
|
|
895694f870 | ||
|
|
370ddb181b | ||
|
|
db446f33ad | ||
|
|
b7d831cc47 | ||
|
|
b8e5f3f996 | ||
|
|
b8ab6403ab | ||
|
|
0ade33e46a | ||
|
|
6b877af91d | ||
|
|
4bf4df0f1d | ||
|
|
1b32889eed | ||
|
|
538b54a2dc | ||
|
|
11eda0e051 | ||
|
|
cca315d072 | ||
|
|
843a38acfd | ||
|
|
434d05f9cf | ||
|
|
71611f81c9 | ||
|
|
635047c290 | ||
|
|
82ba11e432 | ||
|
|
9374958473 | ||
|
|
fe73f43d8b | ||
|
|
dd93af80e0 | ||
|
|
a2fe72013f | ||
|
|
2b1f31bacb | ||
|
|
e87e9b4769 | ||
|
|
3830dc9099 | ||
|
|
42c1f95981 | ||
|
|
052ce11d0f | ||
|
|
9cafee3b59 |
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 + (
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
11
erpnext/patches/v16_0/populate_budget_distribution_total.py
Normal file
11
erpnext/patches/v16_0/populate_budget_distribution_total.py
Normal 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)
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user