Merge branch 'develop' of https://github.com/frappe/erpnext into support-53364

This commit is contained in:
Pugazhendhi Velu
2025-11-20 13:31:25 +00:00
87 changed files with 2104 additions and 595 deletions

View File

@@ -14,6 +14,7 @@ import openpyxl
from frappe import _ from frappe import _
from frappe.core.doctype.data_import.data_import import DataImport from frappe.core.doctype.data_import.data_import import DataImport
from frappe.core.doctype.data_import.importer import Importer, ImportFile from frappe.core.doctype.data_import.importer import Importer, ImportFile
from frappe.query_builder.functions import Count
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from frappe.utils.file_manager import get_file, save_file from frappe.utils.file_manager import get_file, save_file
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
@@ -371,7 +372,7 @@ def get_import_status(docname):
logs = frappe.get_all( logs = frappe.get_all(
"Data Import Log", "Data Import Log",
fields=["count(*) as count", "success"], fields=[{"COUNT": "*", "as": "count"}, "success"],
filters={"data_import": docname}, filters={"data_import": docname},
group_by="success", group_by="success",
) )

View File

@@ -4,16 +4,6 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Budget", { frappe.ui.form.on("Budget", {
onload: function (frm) { onload: function (frm) {
frm.set_query("account", "accounts", function () {
return {
filters: {
company: frm.doc.company,
report_type: "Profit and Loss",
is_group: 0,
},
};
});
frm.set_query("monthly_distribution", function () { frm.set_query("monthly_distribution", function () {
return { return {
filters: { filters: {
@@ -30,8 +20,28 @@ frappe.ui.form.on("Budget", {
}); });
}, },
refresh: function (frm) { refresh: async function (frm) {
frm.trigger("toggle_reqd_fields"); 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"
);
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")
);
}
}
}, },
budget_against: function (frm) { budget_against: function (frm) {
@@ -39,6 +49,15 @@ frappe.ui.form.on("Budget", {
frm.trigger("toggle_reqd_fields"); frm.trigger("toggle_reqd_fields");
}, },
budget_amount(frm) {
if (frm.doc.budget_distribution?.length) {
frm.doc.budget_distribution.forEach((row) => {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
});
frm.refresh_field("budget_distribution");
}
},
set_null_value: function (frm) { set_null_value: function (frm) {
if (frm.doc.budget_against == "Cost Center") { if (frm.doc.budget_against == "Cost Center") {
frm.set_value("project", null); frm.set_value("project", null);
@@ -51,4 +70,44 @@ frappe.ui.form.on("Budget", {
frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center"); frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center");
frm.toggle_reqd("project", frm.doc.budget_against == "Project"); frm.toggle_reqd("project", frm.doc.budget_against == "Project");
}, },
revise_budget_action: function (frm) {
frappe.confirm(
__(
"Are you sure you want to revise this budget? The current budget will be cancelled and a new draft will be created."
),
function () {
frappe.call({
method: "erpnext.accounts.doctype.budget.budget.revise_budget",
args: { budget_name: frm.doc.name },
callback: function (r) {
if (r.message) {
frappe.msgprint(__("New revised budget created successfully"));
frappe.set_route("Form", "Budget", r.message);
}
},
});
},
function () {
frappe.msgprint(__("Revision cancelled"));
}
);
},
});
frappe.ui.form.on("Budget Distribution", {
amount(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
frm.refresh_field("budget_distribution");
}
},
percent(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
frm.refresh_field("budget_distribution");
}
},
}); });

View File

@@ -12,10 +12,19 @@
"company", "company",
"cost_center", "cost_center",
"project", "project",
"fiscal_year", "account",
"column_break_3", "column_break_3",
"monthly_distribution",
"amended_from", "amended_from",
"from_fiscal_year",
"to_fiscal_year",
"budget_start_date",
"budget_end_date",
"distribution_frequency",
"budget_amount",
"section_break_nwug",
"distribute_equally",
"section_break_fpdt",
"budget_distribution",
"section_break_6", "section_break_6",
"applicable_on_material_request", "applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr", "action_if_annual_budget_exceeded_on_mr",
@@ -32,8 +41,8 @@
"applicable_on_cumulative_expense", "applicable_on_cumulative_expense",
"action_if_annual_exceeded_on_cumulative_expense", "action_if_annual_exceeded_on_cumulative_expense",
"action_if_accumulated_monthly_exceeded_on_cumulative_expense", "action_if_accumulated_monthly_exceeded_on_cumulative_expense",
"section_break_21", "section_break_kkan",
"accounts" "revision_of"
], ],
"fields": [ "fields": [
{ {
@@ -44,6 +53,7 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Budget Against", "label": "Budget Against",
"options": "\nCost Center\nProject", "options": "\nCost Center\nProject",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -53,6 +63,7 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -62,7 +73,8 @@
"in_global_search": 1, "in_global_search": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center" "options": "Cost Center",
"read_only_depends_on": "eval: doc.revision_of"
}, },
{ {
"depends_on": "eval:doc.budget_against == 'Project'", "depends_on": "eval:doc.budget_against == 'Project'",
@@ -70,28 +82,13 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Project", "label": "Project",
"options": "Project" "options": "Project",
}, "read_only_depends_on": "eval: doc.revision_of"
{
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Fiscal Year",
"options": "Fiscal Year",
"reqd": 1
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
"fieldname": "monthly_distribution",
"fieldtype": "Link",
"label": "Monthly Distribution",
"options": "Monthly Distribution"
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@@ -187,22 +184,12 @@
"options": "\nStop\nWarn\nIgnore" "options": "\nStop\nWarn\nIgnore"
}, },
{ {
"fieldname": "section_break_21", "default": "BUDGET-.########",
"fieldtype": "Section Break"
},
{
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Budget Accounts",
"options": "Budget Account",
"reqd": 1
},
{
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"no_copy": 1, "no_copy": 1,
"options": "BUDGET-.YYYY.-", "options": "BUDGET-.########",
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1,
"set_only_once": 1 "set_only_once": 1
@@ -232,13 +219,97 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense", "label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
"options": "\nStop\nWarn\nIgnore" "options": "\nStop\nWarn\nIgnore"
},
{
"fieldname": "section_break_fpdt",
"fieldtype": "Section Break"
},
{
"fieldname": "budget_distribution",
"fieldtype": "Table",
"label": "Budget Distribution",
"options": "Budget Distribution"
},
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "budget_amount",
"fieldtype": "Currency",
"label": "Budget Amount",
"reqd": 1
},
{
"fieldname": "section_break_kkan",
"fieldtype": "Section Break"
},
{
"fieldname": "revision_of",
"fieldtype": "Data",
"label": "Revision Of",
"no_copy": 1,
"read_only": 1
},
{
"default": "1",
"fieldname": "distribute_equally",
"fieldtype": "Check",
"label": "Distribute Equally"
},
{
"fieldname": "section_break_nwug",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "from_fiscal_year",
"fieldtype": "Link",
"label": "From Fiscal Year",
"options": "Fiscal Year",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "to_fiscal_year",
"fieldtype": "Link",
"label": "To Fiscal Year",
"options": "Fiscal Year",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "budget_start_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Budget Start Date"
},
{
"fieldname": "budget_end_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Budget End Date"
},
{
"default": "Monthly",
"fieldname": "distribution_frequency",
"fieldtype": "Select",
"label": "Distribution Frequency",
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-06-16 15:57:13.114981", "modified": "2025-11-19 17:00:00.648224",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget", "name": "Budget",

View File

@@ -2,10 +2,14 @@
# For license information, please see license.txt # For license information, please see license.txt
from datetime import date
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate from frappe.query_builder.functions import Sum
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate, month_diff
from frappe.utils.data import get_first_day, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@@ -30,9 +34,9 @@ class Budget(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
from erpnext.accounts.doctype.budget_account.budget_account import BudgetAccount from erpnext.accounts.doctype.budget_distribution.budget_distribution import BudgetDistribution
accounts: DF.Table[BudgetAccount] account: DF.Link
action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"] action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"] action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"] action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
@@ -47,73 +51,117 @@ class Budget(Document):
applicable_on_material_request: DF.Check applicable_on_material_request: DF.Check
applicable_on_purchase_order: DF.Check applicable_on_purchase_order: DF.Check
budget_against: DF.Literal["", "Cost Center", "Project"] budget_against: DF.Literal["", "Cost Center", "Project"]
budget_amount: DF.Currency
budget_distribution: DF.Table[BudgetDistribution]
budget_end_date: DF.Date | None
budget_start_date: DF.Date | None
company: DF.Link company: DF.Link
cost_center: DF.Link | None cost_center: DF.Link | None
fiscal_year: DF.Link distribute_equally: DF.Check
monthly_distribution: DF.Link | None distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
naming_series: DF.Literal["BUDGET-.YYYY.-"] from_fiscal_year: DF.Link
naming_series: DF.Literal["BUDGET-.########"]
project: DF.Link | None project: DF.Link | None
revision_of: DF.Data | None
to_fiscal_year: DF.Link
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
if not self.get(frappe.scrub(self.budget_against)): if not self.get(frappe.scrub(self.budget_against)):
frappe.throw(_("{0} is mandatory").format(self.budget_against)) frappe.throw(_("{0} is mandatory").format(self.budget_against))
self.validate_budget_amount()
self.validate_fiscal_year()
self.set_fiscal_year_dates()
self.validate_duplicate() self.validate_duplicate()
self.validate_accounts() self.validate_account()
self.set_null_value() self.set_null_value()
self.validate_applicable_for() self.validate_applicable_for()
self.validate_existing_expenses()
def validate_budget_amount(self):
if self.budget_amount <= 0:
frappe.throw(_("Budget Amount can not be {0}.").format(self.budget_amount))
def validate_fiscal_year(self):
if self.from_fiscal_year:
self.validate_fiscal_year_company(self.from_fiscal_year, self.company)
if self.to_fiscal_year:
self.validate_fiscal_year_company(self.to_fiscal_year, self.company)
def validate_fiscal_year_company(self, fiscal_year, company):
linked_companies = frappe.get_all(
"Fiscal Year Company", filters={"parent": fiscal_year}, pluck="company"
)
if linked_companies and company not in linked_companies:
frappe.throw(_("Fiscal Year {0} is not available for Company {1}.").format(fiscal_year, company))
def set_fiscal_year_dates(self):
if self.from_fiscal_year:
self.budget_start_date = frappe.get_cached_value(
"Fiscal Year", self.from_fiscal_year, "year_start_date"
)
if self.to_fiscal_year:
self.budget_end_date = frappe.get_cached_value(
"Fiscal Year", self.to_fiscal_year, "year_end_date"
)
if self.budget_start_date > self.budget_end_date:
frappe.throw(_("From Fiscal Year cannot be greater than To Fiscal Year"))
def validate_duplicate(self): def validate_duplicate(self):
budget_against_field = frappe.scrub(self.budget_against) budget_against_field = frappe.scrub(self.budget_against)
budget_against = self.get(budget_against_field) budget_against = self.get(budget_against_field)
account = self.account
if not account:
return
accounts = [d.account for d in self.accounts] or []
existing_budget = frappe.db.sql( existing_budget = frappe.db.sql(
""" f"""
select SELECT name, account
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba FROM `tabBudget`
where WHERE
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and docstatus < 2
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format( AND company = %s
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts)) AND {budget_against_field} = %s
), AND account = %s
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)), AND name != %s
as_dict=1, AND (
(SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
AND (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
)
""",
(self.company, budget_against, account, self.name, self.budget_end_date, self.budget_start_date),
as_dict=True,
) )
for d in existing_budget: if existing_budget:
d = existing_budget[0]
frappe.throw( frappe.throw(
_( _(
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}" "Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' with overlapping fiscal years."
).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), ).format(d.name, self.budget_against, budget_against, d.account),
DuplicateBudgetError, DuplicateBudgetError,
) )
def validate_accounts(self): def validate_account(self):
account_list = [] if not self.account:
for d in self.get("accounts"): frappe.throw(_("Account is mandatory"))
if d.account:
account_details = frappe.get_cached_value( account_details = frappe.get_cached_value(
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1 "Account", self.account, ["is_group", "company", "report_type"], as_dict=1
) )
if account_details.is_group: if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account)) frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(self.account))
elif account_details.company != self.company: elif account_details.company != self.company:
frappe.throw( frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
)
elif account_details.report_type != "Profit and Loss": elif account_details.report_type != "Profit and Loss":
frappe.throw( frappe.throw(
_( _("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
"Budget cannot be assigned against {0}, as it's not an Income or Expense account" self.account
).format(d.account) )
) )
if d.account in account_list:
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
else:
account_list.append(d.account)
def set_null_value(self): def set_null_value(self):
if self.budget_against == "Cost Center": if self.budget_against == "Cost Center":
@@ -139,30 +187,201 @@ class Budget(Document):
): ):
self.applicable_on_booking_actual_expenses = 1 self.applicable_on_booking_actual_expenses = 1
def validate_existing_expenses(self):
if self.is_new() and self.revision_of:
return
def validate_expense_against_budget(args, expense_amount=0): params = frappe._dict(
args = frappe._dict(args) {
"company": self.company,
"account": self.account,
"budget_start_date": self.budget_start_date,
"budget_end_date": self.budget_end_date,
"budget_against_field": frappe.scrub(self.budget_against),
"budget_against_doctype": frappe.unscrub(self.budget_against),
}
)
params[params.budget_against_field] = self.get(params.budget_against_field)
if frappe.get_cached_value("DocType", params.budget_against_doctype, "is_tree"):
params.is_tree = True
else:
params.is_tree = False
actual_spent = get_actual_expense(params)
if actual_spent > self.budget_amount:
frappe.throw(
_(
"Spending for Account {0} ({1}) between {2} and {3} "
"has already exceeded the new allocated budget. "
"Spent: {4}, Budget: {5}"
).format(
frappe.bold(self.account),
frappe.bold(self.company),
frappe.bold(self.budget_start_date),
frappe.bold(self.budget_end_date),
frappe.bold(frappe.utils.fmt_money(actual_spent)),
frappe.bold(frappe.utils.fmt_money(self.budget_amount)),
),
title=_("Budget Limit Exceeded"),
)
def before_save(self):
self.allocate_budget()
def on_update(self):
self.validate_distribution_totals()
def allocate_budget(self):
if self.revision_of:
return
if not self.should_regenerate_budget_distribution():
return
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)
def should_regenerate_budget_distribution(self):
"""Check whether budget distribution should be recalculated."""
old_doc = self.get_doc_before_save() if not self.is_new() else None
if not old_doc or not self.budget_distribution:
return True
if old_doc:
changed_fields = [
"from_fiscal_year",
"to_fiscal_year",
"budget_amount",
"distribution_frequency",
"distribute_equally",
]
for field in changed_fields:
if old_doc.get(field) != self.get(field):
return True
return bool(self.distribute_equally)
def get_budget_periods(self):
"""Return list of (start_date, end_date) tuples based on frequency."""
frequency = self.distribution_frequency
periods = []
start_date = getdate(self.budget_start_date)
end_date = getdate(self.budget_end_date)
while start_date <= end_date:
period_start = get_first_day(start_date)
period_end = self.get_period_end(period_start, frequency)
period_end = min(period_end, end_date)
periods.append((period_start, period_end))
start_date = add_months(period_start, self.get_month_increment(frequency))
return periods
def get_period_end(self, start_date, frequency):
"""Return the correct end date for a given frequency."""
if frequency == "Monthly":
return get_last_day(start_date)
elif frequency == "Quarterly":
return get_last_day(add_months(start_date, 2))
elif frequency == "Half-Yearly":
return get_last_day(add_months(start_date, 5))
else: # Yearly
return get_last_day(add_months(start_date, 11))
def get_month_increment(self, frequency):
"""Return how many months to move forward for the next period."""
return {
"Monthly": 1,
"Quarterly": 3,
"Half-Yearly": 6,
"Yearly": 12,
}.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)
def validate_distribution_totals(self):
if self.should_regenerate_budget_distribution():
return
total_amount = sum(d.amount for d in self.budget_distribution)
total_percent = sum(d.percent for d in self.budget_distribution)
if flt(abs(total_amount - self.budget_amount), 2) > 0.10:
frappe.throw(
_("Total distributed amount {0} must be equal to Budget Amount {1}").format(
flt(total_amount, 2), self.budget_amount
)
)
if flt(abs(total_percent - 100), 2) > 0.10:
frappe.throw(
_("Total distribution percent must equal 100 (currently {0})").format(round(total_percent, 2))
)
def validate_expense_against_budget(params, expense_amount=0):
params = frappe._dict(params)
if not frappe.db.count("Budget", cache=True): if not frappe.db.count("Budget", cache=True):
return return
if not args.fiscal_year: if not params.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] params.fiscal_year = get_fiscal_year(params.get("posting_date"), company=params.get("company"))[0]
if args.get("company"): posting_date = getdate(params.get("posting_date"))
frappe.flags.exception_approver_role = frappe.get_cached_value( posting_fiscal_year = get_fiscal_year(posting_date, company=params.get("company"))[0]
"Company", args.get("company"), "exception_budget_approver_role" year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year)
budget_exists = frappe.db.sql(
"""
select name
from `tabBudget`
where company = %s
and docstatus = 1
and (SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
and (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
limit 1
""",
(params.company, year_end_date, year_start_date),
) )
if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): if not budget_exists:
return return
if not args.account: if params.get("company"):
args.account = args.get("expense_account") frappe.flags.exception_approver_role = frappe.get_cached_value(
"Company", params.get("company"), "exception_budget_approver_role"
)
if not (args.get("account") and args.get("cost_center")) and args.item_code: if not params.account:
args.cost_center, args.account = get_item_details(args) params.account = params.get("expense_account")
if not args.account: if not params.get("expense_account") and params.get("account"):
params.expense_account = params.account
if not (params.get("account") and params.get("cost_center")) and params.item_code:
params.cost_center, params.account = get_item_details(params)
if not params.account:
return return
default_dimensions = [ default_dimensions = [
@@ -180,59 +399,78 @@ def validate_expense_against_budget(args, expense_amount=0):
budget_against = dimension.get("fieldname") budget_against = dimension.get("fieldname")
if ( if (
args.get(budget_against) params.get(budget_against)
and args.account and params.account
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense") and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense")
): ):
doctype = dimension.get("document_type") doctype = dimension.get("document_type")
if frappe.get_cached_value("DocType", doctype, "is_tree"): if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"]) lft, rgt = frappe.get_cached_value(doctype, params.get(budget_against), ["lft", "rgt"])
condition = f"""and exists(select name from `tab{doctype}` condition = f"""and exists(select name from `tab{doctype}`
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
args.is_tree = True params.is_tree = True
else: else:
condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}" condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}"
args.is_tree = False params.is_tree = False
args.budget_against_field = budget_against params.budget_against_field = budget_against
args.budget_against_doctype = doctype params.budget_against_doctype = doctype
budget_records = frappe.db.sql( budget_records = frappe.db.sql(
f""" f"""
select SELECT
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution, b.name,
ifnull(b.applicable_on_material_request, 0) as for_material_request, b.{budget_against} AS budget_against,
ifnull(applicable_on_purchase_order, 0) as for_purchase_order, b.budget_amount,
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses, b.from_fiscal_year,
b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded, b.to_fiscal_year,
b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr, b.budget_start_date,
b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po b.budget_end_date,
from IFNULL(b.applicable_on_material_request, 0) AS for_material_request,
`tabBudget` b, `tabBudget Account` ba IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order,
where IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses,
b.name=ba.parent and b.fiscal_year=%s b.action_if_annual_budget_exceeded,
and ba.account=%s and b.docstatus=1 b.action_if_accumulated_monthly_budget_exceeded,
b.action_if_annual_budget_exceeded_on_mr,
b.action_if_accumulated_monthly_budget_exceeded_on_mr,
b.action_if_annual_budget_exceeded_on_po,
b.action_if_accumulated_monthly_budget_exceeded_on_po
FROM
`tabBudget` b
WHERE
b.company = %s
AND b.docstatus = 1
AND %s BETWEEN b.budget_start_date AND b.budget_end_date
AND b.account = %s
{condition} {condition}
""", """,
(args.fiscal_year, args.account), (params.company, params.posting_date, params.account),
as_dict=True, as_dict=True,
) # nosec ) # nosec
if budget_records: if budget_records:
validate_budget_records(args, budget_records, expense_amount) validate_budget_records(params, budget_records, expense_amount)
def validate_budget_records(args, budget_records, expense_amount): def validate_budget_records(params, budget_records, expense_amount):
for budget in budget_records: for budget in budget_records:
if flt(budget.budget_amount): if flt(budget.budget_amount):
yearly_action, monthly_action = get_actions(args, budget) yearly_action, monthly_action = get_actions(params, budget)
args["for_material_request"] = budget.for_material_request params["for_material_request"] = budget.for_material_request
args["for_purchase_order"] = budget.for_purchase_order params["for_purchase_order"] = budget.for_purchase_order
params["from_fiscal_year"], params["to_fiscal_year"] = (
budget.from_fiscal_year,
budget.to_fiscal_year,
)
params["budget_start_date"], params["budget_end_date"] = (
budget.budget_start_date,
budget.budget_end_date,
)
if yearly_action in ("Stop", "Warn"): if yearly_action in ("Stop", "Warn"):
compare_expense_with_budget( compare_expense_with_budget(
args, params,
flt(budget.budget_amount), flt(budget.budget_amount),
_("Annual"), _("Annual"),
yearly_action, yearly_action,
@@ -241,14 +479,12 @@ def validate_budget_records(args, budget_records, expense_amount):
) )
if monthly_action in ["Stop", "Warn"]: if monthly_action in ["Stop", "Warn"]:
budget_amount = get_accumulated_monthly_budget( budget_amount = get_accumulated_monthly_budget(budget.name, params.posting_date)
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
)
args["month_end_date"] = get_last_day(args.posting_date) params["month_end_date"] = get_last_day(params.posting_date)
compare_expense_with_budget( compare_expense_with_budget(
args, params,
budget_amount, budget_amount,
_("Accumulated Monthly"), _("Accumulated Monthly"),
monthly_action, monthly_action,
@@ -257,38 +493,41 @@ def validate_budget_records(args, budget_records, expense_amount):
) )
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): def compare_expense_with_budget(params, budget_amount, action_for, action, budget_against, amount=0):
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0 params.actual_expense, params.requested_amount, params.ordered_amount = get_actual_expense(params), 0, 0
if not amount: if not amount:
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args) params.requested_amount, params.ordered_amount = (
get_requested_amount(params),
get_ordered_amount(params),
)
if args.get("doctype") == "Material Request" and args.for_material_request: if params.get("doctype") == "Material Request" and params.for_material_request:
amount = args.requested_amount + args.ordered_amount amount = params.requested_amount + params.ordered_amount
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order: elif params.get("doctype") == "Purchase Order" and params.for_purchase_order:
amount = args.ordered_amount amount = params.ordered_amount
total_expense = args.actual_expense + amount total_expense = params.actual_expense + amount
if total_expense > budget_amount: if total_expense > budget_amount:
if args.actual_expense > budget_amount: if params.actual_expense > budget_amount:
diff = args.actual_expense - budget_amount diff = params.actual_expense - budget_amount
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.") _msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.")
else: else:
diff = total_expense - budget_amount diff = total_expense - budget_amount
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will be exceeded by {5}.") _msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will be exceeded by {5}.")
currency = frappe.get_cached_value("Company", args.company, "default_currency") currency = frappe.get_cached_value("Company", params.company, "default_currency")
msg = _msg.format( msg = _msg.format(
_(action_for), _(action_for),
frappe.bold(args.account), frappe.bold(params.account),
frappe.unscrub(args.budget_against_field), frappe.unscrub(params.budget_against_field),
frappe.bold(budget_against), frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, currency=currency)), frappe.bold(fmt_money(budget_amount, currency=currency)),
frappe.bold(fmt_money(diff, currency=currency)), frappe.bold(fmt_money(diff, currency=currency)),
) )
msg += get_expense_breakup(args, currency, budget_against) msg += get_expense_breakup(params, currency, budget_against)
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles( if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
frappe.session.user frappe.session.user
@@ -301,14 +540,25 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
def get_expense_breakup(args, currency, budget_against): def get_expense_breakup(params, currency, budget_against):
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>" msg = "<hr> {} - <ul>".format(_("Total Expenses booked through"))
common_filters = frappe._dict( common_filters = frappe._dict(
{ {
args.budget_against_field: budget_against, params.budget_against_field: budget_against,
"account": args.account, "account": params.account,
"company": args.company, "company": params.company,
}
)
from_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
to_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
gl_filters = common_filters.copy()
gl_filters.update(
{
"from_date": from_date,
"to_date": to_date,
"is_cancelled": 0,
} }
) )
@@ -317,18 +567,23 @@ def get_expense_breakup(args, currency, budget_against):
+ frappe.utils.get_link_to_report( + frappe.utils.get_link_to_report(
"General Ledger", "General Ledger",
label=_("Actual Expenses"), label=_("Actual Expenses"),
filters=common_filters.copy().update( filters=gl_filters,
{
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
"is_cancelled": 0,
}
),
) )
+ " - " + " - "
+ frappe.bold(fmt_money(args.actual_expense, currency=currency)) + frappe.bold(fmt_money(params.actual_expense, currency=currency))
+ "</li>" + "</li>"
) )
mr_filters = common_filters.copy()
mr_filters.update(
{
"status": [["!=", "Stopped"]],
"docstatus": 1,
"material_request_type": "Purchase",
"schedule_date": [["between", [from_date, to_date]]],
"item_code": params.item_code,
"per_ordered": [["<", 100]],
}
)
msg += ( msg += (
"<li>" "<li>"
@@ -337,22 +592,24 @@ def get_expense_breakup(args, currency, budget_against):
label=_("Material Requests"), label=_("Material Requests"),
report_type="Report Builder", report_type="Report Builder",
doctype="Material Request", doctype="Material Request",
filters=common_filters.copy().update( filters=mr_filters,
{
"status": [["!=", "Stopped"]],
"docstatus": 1,
"material_request_type": "Purchase",
"schedule_date": [["fiscal year", "2023-2024"]],
"item_code": args.item_code,
"per_ordered": [["<", 100]],
}
),
) )
+ " - " + " - "
+ frappe.bold(fmt_money(args.requested_amount, currency=currency)) + frappe.bold(fmt_money(params.requested_amount, currency=currency))
+ "</li>" + "</li>"
) )
po_filters = common_filters.copy()
po_filters.update(
{
"status": [["!=", "Closed"]],
"docstatus": 1,
"transaction_date": [["between", [from_date, to_date]]],
"item_code": params.item_code,
"per_billed": [["<", 100]],
}
)
msg += ( msg += (
"<li>" "<li>"
+ frappe.utils.get_link_to_report( + frappe.utils.get_link_to_report(
@@ -360,42 +617,34 @@ def get_expense_breakup(args, currency, budget_against):
label=_("Unbilled Orders"), label=_("Unbilled Orders"),
report_type="Report Builder", report_type="Report Builder",
doctype="Purchase Order", doctype="Purchase Order",
filters=common_filters.copy().update( filters=po_filters,
{
"status": [["!=", "Closed"]],
"docstatus": 1,
"transaction_date": [["fiscal year", "2023-2024"]],
"item_code": args.item_code,
"per_billed": [["<", 100]],
}
),
) )
+ " - " + " - "
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency)) + frappe.bold(fmt_money(params.ordered_amount, currency=currency))
+ "</li></ul>" + "</li></ul>"
) )
return msg return msg
def get_actions(args, budget): def get_actions(params, budget):
yearly_action = budget.action_if_annual_budget_exceeded yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
if args.get("doctype") == "Material Request" and budget.for_material_request: if params.get("doctype") == "Material Request" and budget.for_material_request:
yearly_action = budget.action_if_annual_budget_exceeded_on_mr yearly_action = budget.action_if_annual_budget_exceeded_on_mr
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order: elif params.get("doctype") == "Purchase Order" and budget.for_purchase_order:
yearly_action = budget.action_if_annual_budget_exceeded_on_po yearly_action = budget.action_if_annual_budget_exceeded_on_po
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
return yearly_action, monthly_action return yearly_action, monthly_action
def get_requested_amount(args): def get_requested_amount(params):
item_code = args.get("item_code") item_code = params.get("item_code")
condition = get_other_condition(args, "Material Request") condition = get_other_condition(params, "Material Request")
data = frappe.db.sql( data = frappe.db.sql(
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount """ select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
@@ -409,9 +658,9 @@ def get_requested_amount(args):
return data[0][0] if data else 0 return data[0][0] if data else 0
def get_ordered_amount(args): def get_ordered_amount(params):
item_code = args.get("item_code") item_code = params.get("item_code")
condition = get_other_condition(args, "Purchase Order") condition = get_other_condition(params, "Purchase Order")
data = frappe.db.sql( data = frappe.db.sql(
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
@@ -425,46 +674,51 @@ def get_ordered_amount(args):
return data[0][0] if data else 0 return data[0][0] if data else 0
def get_other_condition(args, for_doc): def get_other_condition(params, for_doc):
condition = "expense_account = '%s'" % (args.expense_account) condition = f"expense_account = '{params.expense_account}'"
budget_against_field = args.get("budget_against_field") budget_against_field = params.get("budget_against_field")
if budget_against_field and args.get(budget_against_field): if budget_against_field and params.get(budget_against_field):
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'" condition += f" and child.{budget_against_field} = '{params.get(budget_against_field)}'"
if args.get("fiscal_year"):
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date" date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
start_date, end_date = frappe.get_cached_value(
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
)
condition += f""" and parent.{date_field} start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
between '{start_date}' and '{end_date}' """ end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
return condition return condition
def get_actual_expense(args): def get_actual_expense(params):
if not args.budget_against_doctype: if not params.budget_against_doctype:
args.budget_against_doctype = frappe.unscrub(args.budget_against_field) params.budget_against_doctype = frappe.unscrub(params.budget_against_field)
budget_against_field = args.get("budget_against_field") budget_against_field = params.get("budget_against_field")
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else "" condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else ""
if args.is_tree: date_condition = (
lft_rgt = frappe.db.get_value( f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
) )
args.update(lft_rgt) if params.is_tree:
lft_rgt = frappe.db.get_value(
params.budget_against_doctype, params.get(budget_against_field), ["lft", "rgt"], as_dict=1
)
params.update(lft_rgt)
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}` condition2 = f"""
where lft>=%(lft)s and rgt<=%(rgt)s and exists(
and name=gle.{budget_against_field})""" select name from `tab{params.budget_against_doctype}`
where lft >= %(lft)s and rgt <= %(rgt)s
and name = gle.{budget_against_field}
)
"""
else: else:
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}` condition2 = f"""
where name=gle.{budget_against_field} and and gle.{budget_against_field} = %({budget_against_field})s
gle.{budget_against_field} = %({budget_against_field})s)""" """
amount = flt( amount = flt(
frappe.db.sql( frappe.db.sql(
@@ -473,63 +727,49 @@ def get_actual_expense(args):
from `tabGL Entry` gle from `tabGL Entry` gle
where where
is_cancelled = 0 is_cancelled = 0
and gle.account=%(account)s and gle.account = %(account)s
{condition1} {condition1}
and gle.fiscal_year=%(fiscal_year)s {date_condition}
and gle.company=%(company)s and gle.company = %(company)s
and gle.docstatus=1 and gle.docstatus = 1
{condition2} {condition2}
""", """,
(args), params,
)[0][0] )[0][0]
) # nosec ) # nosec
return amount return amount
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget): def get_accumulated_monthly_budget(budget_name, posting_date):
distribution = {} posting_date = getdate(posting_date)
if monthly_distribution:
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
md = frappe.qb.DocType("Monthly Distribution")
res = ( bd = frappe.qb.DocType("Budget Distribution")
frappe.qb.from_(mdp) b = frappe.qb.DocType("Budget")
.join(md)
.on(mdp.parent == md.name) result = (
.select(mdp.month, mdp.percentage_allocation) frappe.qb.from_(bd)
.where(md.fiscal_year == fiscal_year) .join(b)
.where(md.name == monthly_distribution) .on(bd.parent == b.name)
.select(Sum(bd.amount).as_("accumulated_amount"))
.where(b.name == budget_name)
.where(bd.start_date <= posting_date)
.run(as_dict=True) .run(as_dict=True)
) )
for d in res: return flt(result[0]["accumulated_amount"]) if result else 0.0
distribution.setdefault(d.month, d.percentage_allocation)
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
accumulated_percentage = 0.0
while dt <= getdate(posting_date):
if monthly_distribution and distribution:
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
else:
accumulated_percentage += 100.0 / 12
dt = add_months(dt, 1)
return annual_budget * accumulated_percentage / 100
def get_item_details(args): def get_item_details(params):
cost_center, expense_account = None, None cost_center, expense_account = None, None
if not args.get("company"): if not params.get("company"):
return cost_center, expense_account return cost_center, expense_account
if args.item_code: if params.item_code:
item_defaults = frappe.db.get_value( item_defaults = frappe.db.get_value(
"Item Default", "Item Default",
{"parent": args.item_code, "company": args.get("company")}, {"parent": params.item_code, "company": params.get("company")},
["buying_cost_center", "expense_account"], ["buying_cost_center", "expense_account"],
) )
if item_defaults: if item_defaults:
@@ -537,7 +777,7 @@ def get_item_details(args):
if not (cost_center and expense_account): if not (cost_center and expense_account):
for doctype in ["Item Group", "Company"]: for doctype in ["Item Group", "Company"]:
data = get_expense_cost_center(doctype, args) data = get_expense_cost_center(doctype, params)
if not cost_center and data: if not cost_center and data:
cost_center = data[0] cost_center = data[0]
@@ -551,14 +791,39 @@ def get_item_details(args):
return cost_center, expense_account return cost_center, expense_account
def get_expense_cost_center(doctype, args): def get_expense_cost_center(doctype, params):
if doctype == "Item Group": if doctype == "Item Group":
return frappe.db.get_value( return frappe.db.get_value(
"Item Default", "Item Default",
{"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")}, {"parent": params.get(frappe.scrub(doctype)), "company": params.get("company")},
["buying_cost_center", "expense_account"], ["buying_cost_center", "expense_account"],
) )
else: else:
return frappe.db.get_value( return frappe.db.get_value(
doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"] doctype, params.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
) )
def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
from_year = frappe.get_cached_value(
"Fiscal Year", from_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
)
to_year = frappe.get_cached_value(
"Fiscal Year", to_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
)
return from_year.year_start_date, to_year.year_end_date
@frappe.whitelist()
def revise_budget(budget_name):
old_budget = frappe.get_doc("Budget", budget_name)
if old_budget.docstatus == 1:
old_budget.cancel()
new_budget = frappe.copy_doc(old_budget)
new_budget.docstatus = 0
new_budget.revision_of = old_budget.name
new_budget.insert()
return new_budget.name

View File

@@ -3,12 +3,14 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import now_datetime, nowdate from frappe.client import submit
from frappe.utils import add_days, flt, get_first_day, get_last_day, getdate, now_datetime, nowdate
from erpnext.accounts.doctype.budget.budget import ( from erpnext.accounts.doctype.budget.budget import (
BudgetError, BudgetError,
get_accumulated_monthly_budget, get_accumulated_monthly_budget,
get_actual_expense, get_actual_expense,
revise_budget,
) )
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
@@ -25,11 +27,15 @@ class TestBudget(ERPNextTestSuite):
def setUp(self): def setUp(self):
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False) frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
self.company = "_Test Company"
self.fiscal_year = frappe.db.get_value("Fiscal Year", {}, "name")
self.account = "_Test Account Cost for Goods Sold - _TC"
self.cost_center = "_Test Cost Center - _TC"
def test_monthly_budget_crossed_ignore(self): def test_monthly_budget_crossed_ignore(self):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
@@ -50,12 +56,13 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_crossed_stop1(self): def test_monthly_budget_crossed_stop1(self):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
accumulated_limit = get_accumulated_monthly_budget( accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount budget.name,
nowdate(),
) )
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
@@ -73,13 +80,11 @@ class TestBudget(ERPNextTestSuite):
def test_exception_approver_role(self): def test_exception_approver_role(self):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
accumulated_limit = get_accumulated_monthly_budget( accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
)
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", "_Test Bank - _TC",
@@ -107,16 +112,16 @@ class TestBudget(ERPNextTestSuite):
applicable_on_purchase_order=1, applicable_on_purchase_order=1,
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop", action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
budget_against="Cost Center", budget_against="Cost Center",
do_not_save=False,
submit_budget=True,
) )
fiscal_year = get_fiscal_year(nowdate())[0]
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
accumulated_limit = get_accumulated_monthly_budget( accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount budget.name,
nowdate(),
) )
mr = frappe.get_doc( mr = frappe.get_doc(
{ {
"doctype": "Material Request", "doctype": "Material Request",
@@ -151,14 +156,15 @@ class TestBudget(ERPNextTestSuite):
applicable_on_purchase_order=1, applicable_on_purchase_order=1,
action_if_accumulated_monthly_budget_exceeded_on_po="Stop", action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
budget_against="Cost Center", budget_against="Cost Center",
do_not_save=False,
submit_budget=True,
) )
fiscal_year = get_fiscal_year(nowdate())[0]
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
accumulated_limit = get_accumulated_monthly_budget( accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount budget.name,
nowdate(),
) )
po = create_purchase_order( po = create_purchase_order(
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
@@ -175,13 +181,14 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_crossed_stop2(self): def test_monthly_budget_crossed_stop2(self):
set_total_expense_zero(nowdate(), "project") set_total_expense_zero(nowdate(), "project")
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
project = frappe.get_value("Project", {"project_name": "_Test Project"}) project = frappe.get_value("Project", {"project_name": "_Test Project"})
accumulated_limit = get_accumulated_monthly_budget( accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount budget.name,
nowdate(),
) )
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
@@ -200,7 +207,7 @@ class TestBudget(ERPNextTestSuite):
def test_yearly_budget_crossed_stop1(self): def test_yearly_budget_crossed_stop1(self):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
@@ -217,7 +224,7 @@ class TestBudget(ERPNextTestSuite):
def test_yearly_budget_crossed_stop2(self): def test_yearly_budget_crossed_stop2(self):
set_total_expense_zero(nowdate(), "project") set_total_expense_zero(nowdate(), "project")
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
project = frappe.get_value("Project", {"project_name": "_Test Project"}) project = frappe.get_value("Project", {"project_name": "_Test Project"})
@@ -237,7 +244,7 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_on_cancellation1(self): def test_monthly_budget_on_cancellation1(self):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
month = now_datetime().month month = now_datetime().month
if month > 9: if month > 9:
month = 9 month = 9
@@ -266,7 +273,7 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_on_cancellation2(self): def test_monthly_budget_on_cancellation2(self):
set_total_expense_zero(nowdate(), "project") set_total_expense_zero(nowdate(), "project")
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
month = now_datetime().month month = now_datetime().month
if month > 9: if month > 9:
month = 9 month = 9
@@ -298,11 +305,17 @@ class TestBudget(ERPNextTestSuite):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC") set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC") budget = make_budget(
budget_against="Cost Center",
cost_center="_Test Company - _TC",
do_not_save=False,
submit_budget=True,
)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
accumulated_limit = get_accumulated_monthly_budget( accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount budget.name,
nowdate(),
) )
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
@@ -331,11 +344,14 @@ class TestBudget(ERPNextTestSuite):
} }
).insert(ignore_permissions=True) ).insert(ignore_permissions=True)
budget = make_budget(budget_against="Cost Center", cost_center=cost_center) budget = make_budget(
budget_against="Cost Center", cost_center=cost_center, do_not_save=False, submit_budget=True
)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
accumulated_limit = get_accumulated_monthly_budget( accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount budget.name,
nowdate(),
) )
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
@@ -372,7 +388,12 @@ class TestBudget(ERPNextTestSuite):
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40}, {"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
) )
make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC") make_budget(
budget_against="Cost Center",
cost_center="Main Budget Cost Center 1 - _TC",
do_not_save=False,
submit_budget=True,
)
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
@@ -387,12 +408,15 @@ class TestBudget(ERPNextTestSuite):
def test_action_for_cumulative_limit(self): def test_action_for_cumulative_limit(self):
set_total_expense_zero(nowdate(), "cost_center") set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center", applicable_on_cumulative_expense=True) budget = make_budget(
budget_against="Cost Center",
accumulated_limit = get_accumulated_monthly_budget( applicable_on_cumulative_expense=True,
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount do_not_save=False,
submit_budget=True,
) )
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
jv = make_journal_entry( jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC", "_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", "_Test Bank - _TC",
@@ -422,6 +446,165 @@ class TestBudget(ERPNextTestSuite):
po.cancel() po.cancel()
jv.cancel() jv.cancel()
def test_fiscal_year_validation(self):
frappe.get_doc(
{
"doctype": "Fiscal Year",
"year": "2100",
"year_start_date": "2100-04-01",
"year_end_date": "2101-03-31",
"companies": [{"company": "_Test Company"}],
}
).insert(ignore_permissions=True)
budget = make_budget(
budget_against="Cost Center",
from_fiscal_year="2100",
to_fiscal_year="2099",
do_not_save=True,
submit_budget=False,
)
with self.assertRaises(frappe.ValidationError):
budget.save()
def test_total_distribution_equals_budget(self):
budget = make_budget(
budget_against="Cost Center",
applicable_on_cumulative_expense=True,
distribute_equally=0,
budget_amount=12000,
do_not_save=False,
submit_budget=False,
)
for row in budget.budget_distribution:
row.amount = 2000
with self.assertRaises(frappe.ValidationError):
budget.save()
def test_evenly_distribute_budget(self):
budget = make_budget(
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
)
total = sum([d.amount for d in budget.budget_distribution])
self.assertEqual(flt(total), 120000)
self.assertTrue(all(d.amount == 10000 for d in budget.budget_distribution))
def test_create_revised_budget(self):
budget = make_budget(
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
)
revised_name = revise_budget(budget.name)
revised_budget = frappe.get_doc("Budget", revised_name)
self.assertNotEqual(budget.name, revised_budget.name)
self.assertEqual(revised_budget.budget_against, budget.budget_against)
self.assertEqual(revised_budget.budget_amount, budget.budget_amount)
old_budget = frappe.get_doc("Budget", budget.name)
self.assertEqual(old_budget.docstatus, 2)
def test_revision_preserves_distribution(self):
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center - _TC")
budget = make_budget(
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
)
revised_name = revise_budget(budget.name)
revised_budget = frappe.get_doc("Budget", revised_name)
self.assertGreater(len(revised_budget.budget_distribution), 0)
total = sum(row.amount for row in revised_budget.budget_distribution)
self.assertEqual(total, revised_budget.budget_amount)
def test_manual_budget_amount_total(self):
budget = make_budget(
budget_against="Cost Center",
distribute_equally=0,
budget_amount=30000,
budget_start_date="2025-04-01",
budget_end_date="2025-06-30",
do_not_save=False,
submit_budget=False,
)
budget.budget_distribution = []
for row in [
{"start_date": "2025-04-01", "end_date": "2025-04-30", "amount": 10000, "percent": 33.33},
{"start_date": "2025-05-01", "end_date": "2025-05-31", "amount": 15000, "percent": 50.00},
{"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000, "percent": 16.67},
]:
budget.append("budget_distribution", row)
budget.save()
total_child_amount = sum(row.amount for row in budget.budget_distribution)
self.assertEqual(total_child_amount, budget.budget_amount)
def test_fiscal_year_company_mismatch(self):
budget = make_budget(budget_against="Cost Center", do_not_save=True, submit_budget=False)
fy = frappe.get_doc(
{
"doctype": "Fiscal Year",
"year": "2099",
"year_start_date": "2099-04-01",
"year_end_date": "2100-03-31",
"companies": [{"company": "_Test Company 2"}],
}
).insert(ignore_permissions=True)
budget.from_fiscal_year = fy.name
budget.to_fiscal_year = fy.name
budget.company = "_Test Company"
with self.assertRaises(frappe.ValidationError):
budget.save()
def test_manual_distribution_total_equals_budget_amount(self):
budget = make_budget(
budget_against="Cost Center",
cost_center="_Test Cost Center - _TC",
distribute_equally=0,
budget_amount=12000,
do_not_save=False,
submit_budget=False,
)
for d in budget.budget_distribution:
d.amount = 2000
with self.assertRaises(frappe.ValidationError):
budget.save()
def test_duplicate_budget_validation(self):
budget = make_budget(
budget_against="Cost Center",
distribute_equally=1,
budget_amount=15000,
do_not_save=False,
submit_budget=True,
)
new_budget = frappe.new_doc("Budget")
new_budget.company = "_Test Company"
new_budget.from_fiscal_year = budget.from_fiscal_year
new_budget.to_fiscal_year = new_budget.from_fiscal_year
new_budget.budget_against = "Cost Center"
new_budget.cost_center = "_Test Cost Center - _TC"
new_budget.account = "_Test Account Cost for Goods Sold - _TC"
new_budget.budget_amount = 10000
with self.assertRaises(frappe.ValidationError):
new_budget.insert()
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project": if budget_against_field == "project":
@@ -430,21 +613,32 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
budget_against = budget_against_CC or "_Test Cost Center - _TC" budget_against = budget_against_CC or "_Test Cost Center - _TC"
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
fiscal_year_start_date, fiscal_year_end_date = get_fiscal_year(nowdate())[1:3]
args = frappe._dict( args = frappe._dict(
{ {
"account": "_Test Account Cost for Goods Sold - _TC", "account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC", "cost_center": "_Test Cost Center - _TC",
"monthly_end_date": posting_date, "month_end_date": posting_date,
"company": "_Test Company", "company": "_Test Company",
"fiscal_year": fiscal_year, "from_fiscal_year": fiscal_year,
"to_fiscal_year": fiscal_year,
"budget_against_field": budget_against_field, "budget_against_field": budget_against_field,
"budget_start_date": fiscal_year_start_date,
"budget_end_date": fiscal_year_end_date,
} }
) )
if not args.get(budget_against_field): if not args.get(budget_against_field):
args[budget_against_field] = budget_against args[budget_against_field] = budget_against
args.budget_against_doctype = frappe.unscrub(budget_against_field)
if frappe.get_cached_value("DocType", args.budget_against_doctype, "is_tree"):
args.is_tree = True
else:
args.is_tree = False
existing_expense = get_actual_expense(args) existing_expense = get_actual_expense(args)
if existing_expense: if existing_expense:
@@ -474,18 +668,33 @@ def make_budget(**args):
budget_against = args.budget_against budget_against = args.budget_against
cost_center = args.cost_center cost_center = args.cost_center
fiscal_year = get_fiscal_year(nowdate())[0] fiscal_year = get_fiscal_year(nowdate())[0]
if budget_against == "Project": if budget_against == "Project":
project_name = "{}%".format("_Test Project/" + fiscal_year) project = frappe.get_value("Project", {"project_name": "_Test Project"})
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)}) budget_list = frappe.get_all(
"Budget",
filters={
"project": project,
"account": "_Test Account Cost for Goods Sold - _TC",
},
pluck="name",
)
else: else:
cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year) budget_list = frappe.get_all(
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)}) "Budget",
for d in budget_list: filters={
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d) "cost_center": cost_center or "_Test Cost Center - _TC",
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d) "account": "_Test Account Cost for Goods Sold - _TC",
},
pluck="name",
)
for name in budget_list:
doc = frappe.get_doc("Budget", name)
if doc.docstatus == 1:
doc.cancel()
frappe.delete_doc("Budget", name, force=True, ignore_missing=True)
budget = frappe.new_doc("Budget") budget = frappe.new_doc("Budget")
@@ -494,18 +703,18 @@ def make_budget(**args):
else: else:
budget.cost_center = cost_center or "_Test Cost Center - _TC" budget.cost_center = cost_center or "_Test Cost Center - _TC"
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution") budget.from_fiscal_year = args.from_fiscal_year or fiscal_year
monthly_distribution.fiscal_year = fiscal_year budget.to_fiscal_year = args.to_fiscal_year or fiscal_year
monthly_distribution.save()
budget.fiscal_year = fiscal_year
budget.monthly_distribution = "_Test Distribution"
budget.company = "_Test Company" budget.company = "_Test Company"
budget.account = "_Test Account Cost for Goods Sold - _TC"
budget.budget_amount = args.budget_amount or 200000
budget.applicable_on_booking_actual_expenses = 1 budget.applicable_on_booking_actual_expenses = 1
budget.action_if_annual_budget_exceeded = "Stop" budget.action_if_annual_budget_exceeded = "Stop"
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore" budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
budget.budget_against = budget_against budget.budget_against = budget_against
budget.append("accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000})
budget.distribution_frequency = "Monthly"
budget.distribute_equally = args.get("distribute_equally", 1)
if args.applicable_on_material_request: if args.applicable_on_material_request:
budget.applicable_on_material_request = 1 budget.applicable_on_material_request = 1
@@ -530,7 +739,13 @@ def make_budget(**args):
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn" args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
) )
budget.insert() if not args.do_not_save:
try:
budget.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
if args.submit_budget:
budget.submit() budget.submit()
return budget return budget

View File

@@ -0,0 +1,58 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-10-12 23:31:03.841996",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"start_date",
"end_date",
"amount",
"percent"
],
"fields": [
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date",
"read_only": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount"
},
{
"fieldname": "percent",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Percent"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-11-03 13:18:28.398198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget Distribution",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,26 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class BudgetDistribution(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Currency
end_date: DF.Date | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
percent: DF.Percent
start_date: DF.Date | None
# end: auto-generated types
pass

View File

@@ -3,6 +3,8 @@
import frappe import frappe
from frappe.query_builder import functions
from frappe.query_builder.utils import DocType
from frappe.tests import IntegrationTestCase from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, flt, today from frappe.utils import add_days, flt, today
@@ -81,10 +83,11 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(je.total_debit, 8500.0) self.assertEqual(je.total_debit, 8500.0)
self.assertEqual(je.total_credit, 8500.0) self.assertEqual(je.total_credit, 8500.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all( acc_balance = frappe.db.get_all(
"GL Entry", "GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0}, filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=["sum(debit)-sum(credit) as balance"], fields=[(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance")],
)[0] )[0]
self.assertEqual(acc_balance.balance, 8500.0) self.assertEqual(acc_balance.balance, 8500.0)
@@ -146,12 +149,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(je.total_debit, 500.0) self.assertEqual(je.total_debit, 500.0)
self.assertEqual(je.total_credit, 500.0) self.assertEqual(je.total_credit, 500.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all( acc_balance = frappe.db.get_all(
"GL Entry", "GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0}, filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[ fields=[
"sum(debit)-sum(credit) as balance", (functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", (
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
], ],
)[0] )[0]
# account shouldn't have balance in base and account currency # account shouldn't have balance in base and account currency
@@ -193,12 +199,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
pe.references = [] pe.references = []
pe.save().submit() pe.save().submit()
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all( acc_balance = frappe.db.get_all(
"GL Entry", "GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0}, filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[ fields=[
"sum(debit)-sum(credit) as balance", (functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", (
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
], ],
)[0] )[0]
# account should have balance only in account currency # account should have balance only in account currency
@@ -235,12 +244,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(flt(je.total_debit, precision), 0.0) self.assertEqual(flt(je.total_debit, precision), 0.0)
self.assertEqual(flt(je.total_credit, precision), 0.0) self.assertEqual(flt(je.total_credit, precision), 0.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all( acc_balance = frappe.db.get_all(
"GL Entry", "GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0}, filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[ fields=[
"sum(debit)-sum(credit) as balance", (functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", (
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
], ],
)[0] )[0]
# account shouldn't have balance in base and account currency post revaluation # account shouldn't have balance in base and account currency post revaluation

View File

@@ -71,8 +71,8 @@ class OpeningInvoiceCreationTool(Document):
max_count = {} max_count = {}
fields = [ fields = [
"company", "company",
"count(name) as total_invoices", {"COUNT": "*", "as": "total_invoices"},
"sum(outstanding_amount) as outstanding_amount", {"SUM": "outstanding_amount", "as": "outstanding_amount"},
] ]
companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"]) companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"])
if not companies: if not companies:

View File

@@ -669,7 +669,7 @@ class PaymentReconciliation(Document):
"party": self.party, "party": self.party,
}, },
fields=[ fields=[
"parent as `name`", "parent as name",
"exchange_rate", "exchange_rate",
], ],
as_list=1, as_list=1,

View File

@@ -975,7 +975,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
total_credit_amount = frappe.db.get_all( total_credit_amount = frappe.db.get_all(
"Journal Entry Account", "Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name}, {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(credit) as amount", [{"SUM": "credit", "as": "amount"}],
group_by="reference_name", group_by="reference_name",
)[0].amount )[0].amount
@@ -1069,7 +1069,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
total_credit_amount = frappe.db.get_all( total_credit_amount = frappe.db.get_all(
"Journal Entry Account", "Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name}, {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(credit) as amount", [{"SUM": "credit", "as": "amount"}],
group_by="reference_name", group_by="reference_name",
)[0].amount )[0].amount

View File

@@ -713,6 +713,7 @@ def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
return frappe.get_all( return frappe.get_all(
"UOM Conversion Detail", "UOM Conversion Detail",
filters={"parent": ("in", items), "uom": ("like", f"{txt}%")}, filters={"parent": ("in", items), "uom": ("like", f"{txt}%")},
fields=["distinct uom"], fields=["uom"],
as_list=1, as_list=1,
distinct=True,
) )

View File

@@ -1374,7 +1374,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
total_debit_amount = frappe.db.get_all( total_debit_amount = frappe.db.get_all(
"Journal Entry Account", "Journal Entry Account",
{"account": creditors_account, "docstatus": 1, "reference_name": pi.name}, {"account": creditors_account, "docstatus": 1, "reference_name": pi.name},
"sum(debit) as amount", [{"SUM": "debit", "as": "amount"}],
group_by="reference_name", group_by="reference_name",
)[0].amount )[0].amount
self.assertEqual(flt(total_debit_amount, 2), 2500) self.assertEqual(flt(total_debit_amount, 2), 2500)
@@ -1456,7 +1456,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
total_debit_amount = frappe.db.get_all( total_debit_amount = frappe.db.get_all(
"Journal Entry Account", "Journal Entry Account",
{"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name}, {"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name},
"sum(debit) as amount", [{"SUM": "debit", "as": "amount"}],
group_by="reference_name", group_by="reference_name",
)[0].amount )[0].amount
self.assertEqual(flt(total_debit_amount, 2), 1500) self.assertEqual(flt(total_debit_amount, 2), 1500)

View File

@@ -213,7 +213,10 @@ def get_allowed_types_from_settings(child_doc: bool = False):
repost_docs = [ repost_docs = [
x.document_type x.document_type
for x in frappe.db.get_all( for x in frappe.db.get_all(
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"] "Repost Allowed Types",
filters={"allowed": True},
fields=["document_type"],
distinct=True,
) )
] ]
result = repost_docs result = repost_docs
@@ -287,7 +290,11 @@ def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters
filters.update({"document_type": ("like", f"%{txt}%")}) filters.update({"document_type": ("like", f"%{txt}%")})
if allowed_types := frappe.db.get_all( if allowed_types := frappe.db.get_all(
"Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1 "Repost Allowed Types",
filters=filters,
fields=["document_type"],
as_list=1,
distinct=True,
): ):
return allowed_types return allowed_types
return [] return []

View File

@@ -3612,7 +3612,7 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.db.get_all( frappe.db.get_all(
"Payment Ledger Entry", "Payment Ledger Entry",
filters={"against_voucher_no": si.name, "delinked": 0}, filters={"against_voucher_no": si.name, "delinked": 0},
fields=["sum(amount), sum(amount_in_account_currency)"], fields=[{"SUM": "amount"}, {"SUM": "amount_in_account_currency"}],
as_list=1, as_list=1,
) )

View File

@@ -121,7 +121,7 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
gl_entries = frappe.db.get_all( gl_entries = frappe.db.get_all(
"GL Entry", "GL Entry",
filters={"voucher_no": pi.name}, filters={"voucher_no": pi.name},
fields=["account", "sum(debit) as debit", "sum(credit) as credit"], fields=["account", {"SUM": "debit", "as": "debit"}, {"SUM": "credit", "as": "credit"}],
group_by="account", group_by="account",
) )
self.assertEqual(len(gl_entries), 3) self.assertEqual(len(gl_entries), 3)

View File

@@ -854,8 +854,8 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
group_by="company", group_by="company",
fields=[ fields=[
"company", "company",
"sum(grand_total) as grand_total", {"SUM": "grand_total", "as": "grand_total"},
"sum(base_grand_total) as base_grand_total", {"SUM": "base_grand_total", "as": "base_grand_total"},
], ],
) )
@@ -870,7 +870,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
"expiry_date": (">=", getdate()), "expiry_date": (">=", getdate()),
}, },
group_by="company", group_by="company",
fields=["company", "sum(loyalty_points) as loyalty_points"], fields=["company", {"SUM": "loyalty_points", "as": "loyalty_points"}],
as_list=1, as_list=1,
) )
) )

View File

@@ -210,7 +210,7 @@ def get_gl_balance(report_date, company):
return frappe._dict( return frappe._dict(
frappe.db.get_all( frappe.db.get_all(
"GL Entry", "GL Entry",
fields=["party", "sum(debit - credit)"], fields=["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}],
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company}, filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party", group_by="party",
as_list=1, as_list=1,

View File

@@ -59,10 +59,8 @@ class BudgetValidation:
_obj.update( _obj.update(
{ {
"accumulated_monthly_budget": get_accumulated_monthly_budget( "accumulated_monthly_budget": get_accumulated_monthly_budget(
self.budget_map[key].monthly_distribution, self.budget_map[key].name,
self.doc_date, self.doc_date,
self.fiscal_year,
self.budget_map[key].budget_amount,
) )
} }
) )
@@ -164,16 +162,19 @@ class BudgetValidation:
def get_budget_records(self) -> list: def get_budget_records(self) -> list:
bud = qb.DocType("Budget") bud = qb.DocType("Budget")
bud_acc = qb.DocType("Budget Account")
query = ( query = (
qb.from_(bud) qb.from_(bud)
.inner_join(bud_acc)
.on(bud.name == bud_acc.parent)
.select( .select(
bud.name, bud.name,
bud.budget_against, bud.budget_against,
bud.company, bud.company,
bud.monthly_distribution, bud.account,
bud.budget_amount,
bud.from_fiscal_year,
bud.to_fiscal_year,
bud.budget_start_date,
bud.budget_end_date,
bud.applicable_on_material_request, bud.applicable_on_material_request,
bud.action_if_annual_budget_exceeded_on_mr, bud.action_if_annual_budget_exceeded_on_mr,
bud.action_if_accumulated_monthly_budget_exceeded_on_mr, bud.action_if_accumulated_monthly_budget_exceeded_on_mr,
@@ -186,13 +187,15 @@ class BudgetValidation:
bud.applicable_on_cumulative_expense, bud.applicable_on_cumulative_expense,
bud.action_if_annual_exceeded_on_cumulative_expense, bud.action_if_annual_exceeded_on_cumulative_expense,
bud.action_if_accumulated_monthly_exceeded_on_cumulative_expense, bud.action_if_accumulated_monthly_exceeded_on_cumulative_expense,
bud_acc.account,
bud_acc.budget_amount,
) )
.where(bud.docstatus.eq(1) & bud.fiscal_year.eq(self.fiscal_year) & bud.company.eq(self.company)) .where(
(bud.docstatus == 1)
& (bud.company == self.company)
& (bud.budget_start_date <= self.doc_date)
& (bud.budget_end_date >= self.doc_date)
)
) )
# add dimension fields
for x in self.dimensions: for x in self.dimensions:
query = query.select(bud[x.get("fieldname")]) query = query.select(bud[x.get("fieldname")])
@@ -314,8 +317,8 @@ class BudgetValidation:
frappe.bold(key[2]), frappe.bold(key[2]),
frappe.bold(frappe.unscrub(key[0])), frappe.bold(frappe.unscrub(key[0])),
frappe.bold(key[1]), frappe.bold(key[1]),
frappe.bold(fmt_money(annual_diff, currency=currency)),
frappe.bold(fmt_money(budget_amt, currency=currency)), frappe.bold(fmt_money(budget_amt, currency=currency)),
frappe.bold(fmt_money(annual_diff, currency=currency)),
) )
self.execute_action(config.action_for_annual, _msg) self.execute_action(config.action_for_annual, _msg)
@@ -425,7 +428,7 @@ class BudgetValidation:
frappe.bold(key[2]), frappe.bold(key[2]),
frappe.bold(frappe.unscrub(key[0])), frappe.bold(frappe.unscrub(key[0])),
frappe.bold(key[1]), frappe.bold(key[1]),
frappe.bold(fmt_money(v_map.accumulated_montly_budget, currency=currency)), frappe.bold(fmt_money(v_map.accumulated_monthly_budget, currency=currency)),
self.budget_applicable_for(v_map, current_amt), self.budget_applicable_for(v_map, current_amt),
frappe.bold(fmt_money(monthly_diff, currency=currency)), frappe.bold(fmt_money(monthly_diff, currency=currency)),
) )

View File

@@ -323,22 +323,24 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
party_type = "customer" party_type = "customer"
fields = [ fields = [
f"sum(abs(`tab{child_doctype}`.qty)) as qty", {"SUM": [{"ABS": f"`tab{child_doctype}`.qty"}], "as": "qty"},
] ]
if doctype != "Subcontracting Receipt": if doctype != "Subcontracting Receipt":
fields += [ fields += [
f"sum(abs(`tab{child_doctype}`.stock_qty)) as stock_qty", {"SUM": [{"ABS": f"`tab{child_doctype}`.stock_qty"}], "as": "stock_qty"},
] ]
if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
fields += [ fields += [
f"sum(abs(`tab{child_doctype}`.rejected_qty)) as rejected_qty", {"SUM": [{"ABS": f"`tab{child_doctype}`.rejected_qty"}], "as": "rejected_qty"},
f"sum(abs(`tab{child_doctype}`.received_qty)) as received_qty", {"SUM": [{"ABS": f"`tab{child_doctype}`.received_qty"}], "as": "received_qty"},
] ]
if doctype == "Purchase Receipt": if doctype == "Purchase Receipt":
fields += [f"sum(abs(`tab{child_doctype}`.received_stock_qty)) as received_stock_qty"] fields += [
{"SUM": [{"ABS": f"`tab{child_doctype}`.received_stock_qty"}], "as": "received_stock_qty"}
]
# Used retrun against and supplier and is_retrun because there is an index added for it # Used retrun against and supplier and is_retrun because there is an index added for it
data = frappe.get_all( data = frappe.get_all(

View File

@@ -563,11 +563,14 @@ class StatusUpdater(Document):
fields=[target_ref_field, target_field], fields=[target_ref_field, target_field],
) )
sum_ref = sum(abs(record[target_ref_field]) for record in child_records) # For operator dicts, the alias is in the "as" key; for strings, use the field name directly
ref_key = target_ref_field.get("as") if isinstance(target_ref_field, dict) else target_ref_field
sum_ref = sum(abs(record[ref_key]) for record in child_records)
if sum_ref > 0: if sum_ref > 0:
percentage = round( percentage = round(
sum(min(abs(record[target_field]), abs(record[target_ref_field])) for record in child_records) sum(min(abs(record[target_field]), abs(record[ref_key])) for record in child_records)
/ sum_ref / sum_ref
* 100, * 100,
6, 6,

View File

@@ -1183,6 +1183,91 @@ class StockController(AccountsController):
self.doctype, self.name, self.docstatus, via_landed_cost_voucher=via_landed_cost_voucher self.doctype, self.name, self.docstatus, via_landed_cost_voucher=via_landed_cost_voucher
) )
self.validate_reserved_batches()
def validate_reserved_batches(self):
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
return
if self.doctype not in ["Delivery Note", "Sales Invoice", "Stock Entry"]:
return
batches = frappe.get_all(
"Serial and Batch Entry",
filters={
"voucher_type": self.doctype,
"voucher_no": self.name,
"docstatus": 1,
"batch_no": ("is", "set"),
"qty": ("<", 0),
},
pluck="batch_no",
)
if not batches:
return
field_mapper = {
"Sales Invoice": [["Sales Order", "sales_order"]],
"Delivery Note": [["Sales Order", "against_sales_order"]],
"Stock Entry": [
["Work Order", "work_order"],
["Subcontracting Inward Order", "subcontracting_inward_order"],
],
}.get(self.doctype)
reserved_batches_data = self.get_reserved_batches(batches)
items = self.items
if self.doctype == "Stock Entry":
items = [self]
for item in items:
for field in field_mapper:
if not item.get(field[1]):
continue
value = item.get(field[1])
for row in reserved_batches_data:
if self.doctype in ["Sales Invoice", "Delivery Note"] and row.item_code != item.get(
"item_code"
):
continue
if row.voucher_no == value:
continue
frappe.throw(
_(
"The batch {0} is already reserved in {1} {2}. So, cannot proceed with the {3} {4}, which is created against the {5} {6}."
).format(
frappe.bold(row.batch_no),
frappe.bold(row.voucher_type),
frappe.bold(row.voucher_no),
frappe.bold(self.doctype),
frappe.bold(self.name),
frappe.bold(field[0]),
frappe.bold(value),
),
title=_("Reserved Batch Conflict"),
)
def get_reserved_batches(self, batches):
doctype = frappe.qb.DocType("Stock Reservation Entry")
child_doc = frappe.qb.DocType("Serial and Batch Entry")
return (
frappe.qb.from_(doctype)
.join(child_doc)
.on(doctype.name == child_doc.parent)
.select(
child_doc.batch_no,
doctype.voucher_type,
doctype.voucher_no,
doctype.item_code,
)
.where((doctype.docstatus == 1) & (child_doc.batch_no.isin(batches)))
).run(as_dict=True)
def make_gl_entries_on_cancel(self, from_repost=False): def make_gl_entries_on_cancel(self, from_repost=False):
if not from_repost: if not from_repost:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
@@ -1235,7 +1320,7 @@ class StockController(AccountsController):
total_returned += flt(item.returned_qty * item.rate) total_returned += flt(item.returned_qty * item.rate)
if total_returned < total_amount: if total_returned < total_amount:
target_ref_field = "(amount - (returned_qty * rate))" target_ref_field = {"SUB": ["amount", {"MUL": ["returned_qty", "rate"]}], "as": "ref_amount"}
self._update_percent_field( self._update_percent_field(
{ {

View File

@@ -292,7 +292,7 @@ class SubcontractingController(StockController):
): ):
for row in frappe.get_all( for row in frappe.get_all(
f"{self.subcontract_data.order_doctype} Item", f"{self.subcontract_data.order_doctype} Item",
fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "name"],
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)}, filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
): ):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
@@ -553,7 +553,9 @@ class SubcontractingController(StockController):
data = [] data = []
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] fields = [
{"DIV": [f"`tab{doctype}`.`stock_qty`", "`tabBOM`.`quantity`"], "as": "qty_consumed_per_unit"}
]
alias_dict = { alias_dict = {
"item_code": "rm_item_code", "item_code": "rm_item_code",

View File

@@ -328,7 +328,7 @@ class TestItemWiseInventoryAccount(IntegrationTestCase):
"voucher_no": pr.name, "voucher_no": pr.name,
"item_code": ("in", items), "item_code": ("in", items),
}, },
fields=["sum(stock_value_difference) as value"], fields=[{"SUM": "stock_value_difference", "as": "value"}],
) )
gl_value = frappe.db.get_value( gl_value = frappe.db.get_value(
@@ -435,7 +435,7 @@ class TestItemWiseInventoryAccount(IntegrationTestCase):
sle_value = frappe.get_all( sle_value = frappe.get_all(
"Stock Ledger Entry", "Stock Ledger Entry",
filters={"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": ("in", items)}, filters={"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": ("in", items)},
fields=["sum(stock_value_difference) as value"], fields=[{"SUM": "stock_value_difference", "as": "value"}],
) )
gl_value = ( gl_value = (

View File

@@ -74,7 +74,7 @@ class OpportunitySummaryBySalesStage:
}[self.filters.get("based_on")] }[self.filters.get("based_on")]
data_based_on = { data_based_on = {
"Number": "count(name) as count", "Number": {"COUNT": "*", "as": "count"},
"Amount": "opportunity_amount as amount", "Amount": "opportunity_amount as amount",
}[self.filters.get("data_based_on")] }[self.filters.get("data_based_on")]

View File

@@ -8,6 +8,7 @@ from itertools import groupby
import frappe import frappe
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from frappe import _ from frappe import _
from frappe.query_builder.custom import Month, MonthName, Quarter
from frappe.utils import cint, flt, getdate from frappe.utils import cint, flt, getdate
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
@@ -74,7 +75,7 @@ class SalesPipelineAnalytics:
] ]
self.data_based_on = { self.data_based_on = {
"Number": "count(name) as count", "Number": {"COUNT": "*", "as": "count"},
"Amount": "opportunity_amount as amount", "Amount": "opportunity_amount as amount",
}[self.filters.get("based_on")] }[self.filters.get("based_on")]
@@ -82,40 +83,52 @@ class SalesPipelineAnalytics:
self.filters.get("pipeline_by") self.filters.get("pipeline_by")
] ]
self.group_by_period = { opp = frappe.qb.DocType("Opportunity")
"Monthly": "month(expected_closing)",
"Quarterly": "QUARTER(expected_closing)", if self.filters.get("range") == "Monthly":
}[self.filters.get("range")] self.group_by_period = Month(opp.expected_closing)
self.duration = MonthName(opp.expected_closing).as_("month")
else:
self.group_by_period = Quarter(opp.expected_closing)
self.duration = Quarter(opp.expected_closing).as_("quarter")
self.pipeline_by = {"Owner": "opportunity_owner", "Sales Stage": "sales_stage"}[ self.pipeline_by = {"Owner": "opportunity_owner", "Sales Stage": "sales_stage"}[
self.filters.get("pipeline_by") self.filters.get("pipeline_by")
] ]
self.duration = {
"Monthly": "monthname(expected_closing) as month",
"Quarterly": "QUARTER(expected_closing) as quarter",
}[self.filters.get("range")]
self.period_by = {"Monthly": "month", "Quarterly": "quarter"}[self.filters.get("range")] self.period_by = {"Monthly": "month", "Quarterly": "quarter"}[self.filters.get("range")]
def get_data(self): def get_data(self):
self.get_fields() self.get_fields()
if self.filters.get("based_on") == "Number": opp = frappe.qb.DocType("Opportunity")
self.query_result = frappe.db.get_list( query = frappe.qb.get_query(
"Opportunity", "Opportunity",
filters=self.get_conditions(), filters=self.get_conditions(),
fields=[self.based_on, self.data_based_on, self.duration], ignore_permissions=True,
group_by=f"{self.group_by_based_on},{self.group_by_period}", )
order_by=self.group_by_period,
pipeline_field = opp._assign if self.group_by_based_on == "_assign" else opp.sales_stage
if self.filters.get("based_on") == "Number":
self.query_result = (
query.select(
pipeline_field.as_(self.pipeline_by),
frappe.query_builder.functions.Count("*").as_("count"),
self.duration,
)
.groupby(pipeline_field, self.group_by_period)
.orderby(self.group_by_period)
.run(as_dict=True)
) )
if self.filters.get("based_on") == "Amount": if self.filters.get("based_on") == "Amount":
self.query_result = frappe.db.get_list( self.query_result = query.select(
"Opportunity", pipeline_field.as_(self.pipeline_by),
filters=self.get_conditions(), opp.opportunity_amount.as_("amount"),
fields=[self.based_on, self.data_based_on, self.duration, "currency"], self.duration,
) opp.currency,
).run(as_dict=True)
self.convert_to_base_currency() self.convert_to_base_currency()

View File

@@ -4,12 +4,13 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "dollar-sign",
"icon_type": "Link", "icon_type": "Link",
"idx": 5, "idx": 5,
"label": "Banking", "label": "Banking",
"link_to": "Bank Reconciliation Tool", "link_to": "Bank Reconciliation Tool",
"link_type": "DocType", "link_type": "DocType",
"modified": "2025-11-17 13:34:23.484506", "modified": "2025-11-19 15:57:20.139306",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Banking", "name": "Banking",
"owner": "Administrator", "owner": "Administrator",

View File

@@ -4,12 +4,13 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "panel-top-open",
"icon_type": "Link", "icon_type": "Link",
"idx": 2, "idx": 2,
"label": "Opening & Closing", "label": "Opening & Closing",
"link_to": "Period Closing Voucher", "link_to": "Period Closing Voucher",
"link_type": "DocType", "link_type": "DocType",
"modified": "2025-11-17 13:33:51.092576", "modified": "2025-11-19 15:59:14.805915",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Opening & Closing", "name": "Opening & Closing",
"owner": "Administrator", "owner": "Administrator",

View File

@@ -4,13 +4,13 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "accounting", "icon": "monitor-check",
"icon_type": "Link", "icon_type": "Link",
"idx": 6, "idx": 6,
"label": "Subscription", "label": "Subscription",
"link_to": "Subscription", "link_to": "Subscription",
"link_type": "DocType", "link_type": "DocType",
"modified": "2025-11-17 13:34:40.653317", "modified": "2025-11-19 16:02:32.686833",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Subscription", "name": "Subscription",
"owner": "Administrator", "owner": "Administrator",

View File

@@ -4,12 +4,13 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "book-text",
"icon_type": "Link", "icon_type": "Link",
"idx": 3, "idx": 3,
"label": "Taxes", "label": "Taxes",
"link_to": "Item Tax Template", "link_to": "Item Tax Template",
"link_type": "DocType", "link_type": "DocType",
"modified": "2025-11-17 13:34:03.502433", "modified": "2025-11-19 15:58:21.226664",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Taxes", "name": "Taxes",
"owner": "Administrator", "owner": "Administrator",

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n" "Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n" "Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-11-16 09:35+0000\n" "POT-Creation-Date: 2025-11-16 09:35+0000\n"
"PO-Revision-Date: 2025-11-18 22:14\n" "PO-Revision-Date: 2025-11-19 22:23\n"
"Last-Translator: hello@frappe.io\n" "Last-Translator: hello@frappe.io\n"
"Language-Team: Bosnian\n" "Language-Team: Bosnian\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -2105,7 +2105,7 @@ msgstr "Računi za Spajanje"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264
msgid "Accrued Expenses" msgid "Accrued Expenses"
msgstr "" msgstr "Nagomilani Troškovi"
#. Option for the 'Account Type' (Select) field in DocType 'Account' #. Option for the 'Account Type' (Select) field in DocType 'Account'
#: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/account.json
@@ -25420,7 +25420,7 @@ msgstr "Kontakt Podizvođača"
#. Order' #. Order'
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
msgid "Job Worker Currency" msgid "Job Worker Currency"
msgstr "" msgstr "Valuta Podizvođača"
#. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting #. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting
#. Receipt' #. Receipt'

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n" "Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n" "Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-11-16 09:35+0000\n" "POT-Creation-Date: 2025-11-16 09:35+0000\n"
"PO-Revision-Date: 2025-11-18 22:14\n" "PO-Revision-Date: 2025-11-19 22:23\n"
"Last-Translator: hello@frappe.io\n" "Last-Translator: hello@frappe.io\n"
"Language-Team: Persian\n" "Language-Team: Persian\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -1178,13 +1178,13 @@ msgstr "تراز حساب"
#: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/account.json
#: erpnext/accounts/doctype/account_category/account_category.json #: erpnext/accounts/doctype/account_category/account_category.json
msgid "Account Category" msgid "Account Category"
msgstr "" msgstr "دسته بندی حساب"
#. Label of the account_category_name (Data) field in DocType 'Account #. Label of the account_category_name (Data) field in DocType 'Account
#. Category' #. Category'
#: erpnext/accounts/doctype/account_category/account_category.json #: erpnext/accounts/doctype/account_category/account_category.json
msgid "Account Category Name" msgid "Account Category Name"
msgstr "" msgstr "نام دسته حساب"
#. Name of a DocType #. Name of a DocType
#: erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json #: erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json
@@ -2024,7 +2024,7 @@ msgstr "حساب‌ها برای ادغام"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264
msgid "Accrued Expenses" msgid "Accrued Expenses"
msgstr "" msgstr "مخارج انباشته"
#. Option for the 'Account Type' (Select) field in DocType 'Account' #. Option for the 'Account Type' (Select) field in DocType 'Account'
#: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/account.json
@@ -3189,7 +3189,7 @@ msgstr "پیش‌پرداخت‌های تخصیص یافته در برابر س
#. Row' #. Row'
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json #: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
msgid "Advanced Filtering" msgid "Advanced Filtering"
msgstr "" msgstr "فیلتر پیشرفته"
#. Label of the advances (Table) field in DocType 'POS Invoice' #. Label of the advances (Table) field in DocType 'POS Invoice'
#. Label of the advances (Table) field in DocType 'Purchase Invoice' #. Label of the advances (Table) field in DocType 'Purchase Invoice'
@@ -5485,16 +5485,16 @@ msgstr "تنظیمات دارایی"
#. Name of a DocType #. Name of a DocType
#: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json #: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json
msgid "Asset Shift Allocation" msgid "Asset Shift Allocation"
msgstr "تخصیص تغییر دارایی" msgstr "تخصیص شیفت دارایی"
#. Name of a DocType #. Name of a DocType
#: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json #: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json
msgid "Asset Shift Factor" msgid "Asset Shift Factor"
msgstr "عامل تغییر دارایی" msgstr "ضریب شیفت دارایی"
#: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py:32 #: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py:32
msgid "Asset Shift Factor {0} is set as default currently. Please change it first." msgid "Asset Shift Factor {0} is set as default currently. Please change it first."
msgstr "عامل تغییر دارایی {0} در حال حاضر به عنوان پیش‌فرض تنظیم شده است. لطفا ابتدا آن را تغییر دهید." msgstr "ضریب شیفت دارایی {0} در حال حاضر به عنوان پیش‌فرض تنظیم شده است. لطفا ابتدا آن را تغییر دهید."
#. Label of the asset_status (Select) field in DocType 'Serial No' #. Label of the asset_status (Select) field in DocType 'Serial No'
#: erpnext/stock/doctype/serial_no/serial_no.json #: erpnext/stock/doctype/serial_no/serial_no.json
@@ -5661,7 +5661,7 @@ msgstr "دارایی {assets_link} برای {item_code} ایجاد شد"
#: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py:223 #: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py:223
msgid "Asset's depreciation schedule updated after Asset Shift Allocation {0}" msgid "Asset's depreciation schedule updated after Asset Shift Allocation {0}"
msgstr "برنامه استهلاک دارایی پس از تخصیص تغییر دارایی {0} به روز شد" msgstr "زمان‌بندی استهلاک دارایی پس از تخصیص شیفت دارایی {0} به روز شد"
#: erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py:81 #: erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py:81
msgid "Asset's value adjusted after cancellation of Asset Value Adjustment {0}" msgid "Asset's value adjusted after cancellation of Asset Value Adjustment {0}"
@@ -5752,7 +5752,7 @@ msgstr "حداقل یکی از موارد فروش یا خرید باید انت
#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:25 #: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:25
msgid "At least one row is required for a financial report template" msgid "At least one row is required for a financial report template"
msgstr "" msgstr "حداقل یک ردیف برای الگوی گزارش مالی لازم است"
#: erpnext/stock/doctype/stock_entry/stock_entry.py:704 #: erpnext/stock/doctype/stock_entry/stock_entry.py:704
msgid "At least one warehouse is mandatory" msgid "At least one warehouse is mandatory"
@@ -7912,7 +7912,7 @@ msgstr ""
#. Row' #. Row'
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json #: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
msgid "Bold text for emphasis (totals, major headings)" msgid "Bold text for emphasis (totals, major headings)"
msgstr "" msgstr "متن پررنگ برای تأکید (مجموع، عناوین اصلی)"
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:282 #: erpnext/accounts/doctype/payment_entry/payment_entry.py:282
msgid "Book Advance Payments as Liability option is chosen. Paid From account changed from {0} to {1}." msgid "Book Advance Payments as Liability option is chosen. Paid From account changed from {0} to {1}."
@@ -7963,7 +7963,7 @@ msgstr "یک قرار ملاقات رزرو کنید"
#: erpnext/stock/doctype/shipment/shipment.json #: erpnext/stock/doctype/shipment/shipment.json
#: erpnext/stock/doctype/shipment/shipment_list.js:5 #: erpnext/stock/doctype/shipment/shipment_list.js:5
msgid "Booked" msgid "Booked"
msgstr "رزرو" msgstr "رزرو شده"
#. Label of the booked_fixed_asset (Check) field in DocType 'Asset' #. Label of the booked_fixed_asset (Check) field in DocType 'Asset'
#: erpnext/assets/doctype/asset/asset.json #: erpnext/assets/doctype/asset/asset.json
@@ -8427,7 +8427,7 @@ msgstr "محاسبه قیمت باندل محصول بر اساس نرخ آیت
#. DocType 'Financial Report Row' #. DocType 'Financial Report Row'
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json #: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
msgid "Calculate but don't show on final report" msgid "Calculate but don't show on final report"
msgstr "" msgstr "محاسبه می‌شود اما در گزارش نهایی نمایش داده نمی‌شود"
#. Label of the calculate_depr_using_total_days (Check) field in DocType #. Label of the calculate_depr_using_total_days (Check) field in DocType
#. 'Accounts Settings' #. 'Accounts Settings'
@@ -8439,7 +8439,7 @@ msgstr "محاسبه استهلاک روزانه با استفاده از کل
#. Row' #. Row'
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json #: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
msgid "Calculated Amount" msgid "Calculated Amount"
msgstr "" msgstr "مبلغ محاسبه شده"
#: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py:53 #: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py:53
msgid "Calculated Bank Statement balance" msgid "Calculated Bank Statement balance"
@@ -10967,7 +10967,7 @@ msgstr ""
#: erpnext/stock/doctype/serial_no/serial_no.json #: erpnext/stock/doctype/serial_no/serial_no.json
#: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:60 #: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:60
msgid "Consumed" msgid "Consumed"
msgstr "مصرف شده است" msgstr "مصرف شده"
#: erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py:62 #: erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py:62
msgid "Consumed Amount" msgid "Consumed Amount"
@@ -12249,12 +12249,14 @@ msgstr "ایجاد <b><a href='/app/{0}'>{1}(ها)</a></b> با موفقیت"
#: erpnext/utilities/bulk_transaction.py:206 #: erpnext/utilities/bulk_transaction.py:206
msgid "Creation of {0} failed.\n" msgid "Creation of {0} failed.\n"
"\t\t\t\tCheck <b><a href=\"/app/bulk-transaction-log\">Bulk Transaction Log</a></b>" "\t\t\t\tCheck <b><a href=\"/app/bulk-transaction-log\">Bulk Transaction Log</a></b>"
msgstr "" msgstr "ایجاد {0} ناموفق بود.\n"
"\t\t\t\tبررسی <b><a href=\"/app/bulk-transaction-log\">لاگ تراکنش‌های انبوه</a></b>"
#: erpnext/utilities/bulk_transaction.py:197 #: erpnext/utilities/bulk_transaction.py:197
msgid "Creation of {0} partially successful.\n" msgid "Creation of {0} partially successful.\n"
"\t\t\t\tCheck <b><a href=\"/app/bulk-transaction-log\">Bulk Transaction Log</a></b>" "\t\t\t\tCheck <b><a href=\"/app/bulk-transaction-log\">Bulk Transaction Log</a></b>"
msgstr "" msgstr "ایجاد {0} تا حدودی موفقیت‌آمیز بود.\n"
"\t\t\t\tبررسی <b><a href=\"/app/bulk-transaction-log\">لاگ تراکنش‌های انبوه</a></b>"
#. Option for the 'Balance must be' (Select) field in DocType 'Account' #. Option for the 'Balance must be' (Select) field in DocType 'Account'
#. Label of the credit_in_account_currency (Currency) field in DocType 'Journal #. Label of the credit_in_account_currency (Currency) field in DocType 'Journal
@@ -12815,7 +12817,7 @@ msgstr "حضانت"
#. Row' #. Row'
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json #: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
msgid "Custom API" msgid "Custom API"
msgstr "" msgstr "API سفارشی"
#. Option for the 'Report Type' (Select) field in DocType 'Financial Report #. Option for the 'Report Type' (Select) field in DocType 'Financial Report
#. Template' #. Template'
@@ -12823,7 +12825,7 @@ msgstr ""
#: erpnext/accounts/doctype/financial_report_template/financial_report_template.json #: erpnext/accounts/doctype/financial_report_template/financial_report_template.json
#: erpnext/accounts/report/custom_financial_statement/custom_financial_statement.json #: erpnext/accounts/report/custom_financial_statement/custom_financial_statement.json
msgid "Custom Financial Statement" msgid "Custom Financial Statement"
msgstr "" msgstr "صورت‌های مالی سفارشی"
#. Label of the custom_remarks (Check) field in DocType 'Payment Entry' #. Label of the custom_remarks (Check) field in DocType 'Payment Entry'
#: erpnext/accounts/doctype/payment_entry/payment_entry.json #: erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -14251,7 +14253,7 @@ msgstr "نوع درخواست مواد پیش‌فرض"
#. 'Company' #. 'Company'
#: erpnext/setup/doctype/company/company.json #: erpnext/setup/doctype/company/company.json
msgid "Default Operating Cost Account" msgid "Default Operating Cost Account"
msgstr "" msgstr "حساب هزینه عملیاتی پیش‌فرض"
#. Label of the default_payable_account (Link) field in DocType 'Company' #. Label of the default_payable_account (Link) field in DocType 'Company'
#. Label of the default_payable_account (Section Break) field in DocType #. Label of the default_payable_account (Section Break) field in DocType
@@ -15517,7 +15519,7 @@ msgstr "پرداخت وام"
#: erpnext/accounts/doctype/invoice_discounting/invoice_discounting.json #: erpnext/accounts/doctype/invoice_discounting/invoice_discounting.json
#: erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js:9 #: erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js:9
msgid "Disbursed" msgid "Disbursed"
msgstr "پرداخت شد" msgstr "پرداخت شده"
#. Option for the 'Action on New Invoice' (Select) field in DocType 'POS #. Option for the 'Action on New Invoice' (Select) field in DocType 'POS
#. Profile' #. Profile'
@@ -18280,11 +18282,11 @@ msgstr ""
#: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:242 #: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:242
msgid "Financial Report Template {0} is disabled" msgid "Financial Report Template {0} is disabled"
msgstr "" msgstr "الگوی گزارش مالی {0} غیرفعال است"
#: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:239 #: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:239
msgid "Financial Report Template {0} not found" msgid "Financial Report Template {0} not found"
msgstr "" msgstr "الگوی گزارش مالی {0} یافت نشد"
#. Name of a Workspace #. Name of a Workspace
#: erpnext/accounts/workspace/financial_reports/financial_reports.json #: erpnext/accounts/workspace/financial_reports/financial_reports.json
@@ -19013,7 +19015,7 @@ msgstr "آدرس انجمن"
#: erpnext/setup/install.py:200 #: erpnext/setup/install.py:200
msgid "Frappe School" msgid "Frappe School"
msgstr "" msgstr "مدرسه Frappe"
#. Title of an incoterm #. Title of an incoterm
#: erpnext/setup/doctype/incoterm/incoterms.csv:4 #: erpnext/setup/doctype/incoterm/incoterms.csv:4
@@ -20780,7 +20782,7 @@ msgstr "ساعت"
#: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/job_card/job_card.json
#: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json #: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
msgid "Hour Rate" msgid "Hour Rate"
msgstr "" msgstr "نرخ ساعتی"
#. Label of the hours (Float) field in DocType 'Workstation Working Hour' #. Label of the hours (Float) field in DocType 'Workstation Working Hour'
#: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json #: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json
@@ -21543,7 +21545,7 @@ msgstr "در درصد"
#: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/doctype/work_order/work_order.json
#: erpnext/stock/doctype/quality_inspection/quality_inspection.json #: erpnext/stock/doctype/quality_inspection/quality_inspection.json
msgid "In Process" msgid "In Process"
msgstr "" msgstr "در حال انجام"
#: erpnext/stock/report/item_variant_details/item_variant_details.py:107 #: erpnext/stock/report/item_variant_details/item_variant_details.py:107
msgid "In Production" msgid "In Production"
@@ -22595,7 +22597,7 @@ msgstr "تخفیف نامعتبر"
#: erpnext/controllers/taxes_and_totals.py:738 #: erpnext/controllers/taxes_and_totals.py:738
msgid "Invalid Discount Amount" msgid "Invalid Discount Amount"
msgstr "" msgstr "مبلغ تخفیف نامعتبر است"
#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:122 #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:122
msgid "Invalid Document" msgid "Invalid Document"
@@ -23643,13 +23645,13 @@ msgstr ""
#. Label of the italic_text (Check) field in DocType 'Financial Report Row' #. Label of the italic_text (Check) field in DocType 'Financial Report Row'
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json #: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
msgid "Italic Text" msgid "Italic Text"
msgstr "" msgstr "متن ایتالیک"
#. Description of the 'Italic Text' (Check) field in DocType 'Financial Report #. Description of the 'Italic Text' (Check) field in DocType 'Financial Report
#. Row' #. Row'
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json #: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
msgid "Italic text for subtotals or notes" msgid "Italic text for subtotals or notes"
msgstr "" msgstr "متن ایتالیک برای جمع‌های جزئی یا یادداشت‌ها"
#. Label of the item_code (Link) field in DocType 'POS Invoice Item' #. Label of the item_code (Link) field in DocType 'POS Invoice Item'
#. Label of the item_code (Link) field in DocType 'Purchase Invoice Item' #. Label of the item_code (Link) field in DocType 'Purchase Invoice Item'
@@ -26598,7 +26600,7 @@ msgstr "نگهداری موجودی"
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
#: erpnext/support/workspace/support/support.json #: erpnext/support/workspace/support/support.json
msgid "Maintenance" msgid "Maintenance"
msgstr "نگهداری" msgstr "تعمیر و نگهداری"
#. Label of the mntc_date (Date) field in DocType 'Maintenance Visit' #. Label of the mntc_date (Date) field in DocType 'Maintenance Visit'
#: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json #: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json
@@ -27331,7 +27333,7 @@ msgstr "هزینه های بازاریابی"
#: erpnext/setup/setup_wizard/data/designation.txt:23 #: erpnext/setup/setup_wizard/data/designation.txt:23
msgid "Marketing Specialist" msgid "Marketing Specialist"
msgstr "کارشناس بازاریابی" msgstr "متخصص بازاریابی"
#. Option for the 'Marital Status' (Select) field in DocType 'Employee' #. Option for the 'Marital Status' (Select) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json #: erpnext/setup/doctype/employee/employee.json
@@ -28337,7 +28339,7 @@ msgstr "نحوه پرداخت‌ها"
#. Label of the model (Data) field in DocType 'Vehicle' #. Label of the model (Data) field in DocType 'Vehicle'
#: erpnext/setup/doctype/vehicle/vehicle.json #: erpnext/setup/doctype/vehicle/vehicle.json
msgid "Model" msgid "Model"
msgstr "" msgstr "مدل"
#. Label of the section_break_11 (Section Break) field in DocType 'POS Closing #. Label of the section_break_11 (Section Break) field in DocType 'POS Closing
#. Entry' #. Entry'
@@ -31455,7 +31457,7 @@ msgstr ""
#: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:667 #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:667
msgid "POS Invoices will be consolidated in a background process" msgid "POS Invoices will be consolidated in a background process"
msgstr "فاکتورهای POS در یک فرآیند پس زمینه تلفیق می‌شوند" msgstr "فاکتورهای POS در یک فرآیند پسزمینه تلفیق می‌شوند"
#: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:669 #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:669
msgid "POS Invoices will be unconsolidated in a background process" msgid "POS Invoices will be unconsolidated in a background process"
@@ -34330,7 +34332,7 @@ msgstr "لطفا اول ذخیره کنید"
#: erpnext/selling/doctype/sales_order/sales_order.js:859 #: erpnext/selling/doctype/sales_order/sales_order.js:859
msgid "Please save the Sales Order before adding a delivery schedule." msgid "Please save the Sales Order before adding a delivery schedule."
msgstr "" msgstr "لطفا قبل از اضافه کردن زمان‌بندی تحویل، سفارش فروش را ذخیره کنید."
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:79 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:79
msgid "Please select <b>Template Type</b> to download template" msgid "Please select <b>Template Type</b> to download template"
@@ -40023,11 +40025,11 @@ msgstr "وضعیت بازنشر"
#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:146 #: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:146
msgid "Repost has started in the background" msgid "Repost has started in the background"
msgstr "ارسال مجدد در پس زمینه شروع شده است" msgstr "ارسال مجدد در پسزمینه شروع شده است"
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js:40 #: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js:40
msgid "Repost in background" msgid "Repost in background"
msgstr "بازنشر در پس زمینه" msgstr "بازنشر در پسزمینه"
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py:118 #: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py:118
msgid "Repost started in the background" msgid "Repost started in the background"
@@ -40064,7 +40066,7 @@ msgstr "ارسال مجدد در پس‌زمینه آغاز شده است."
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js:49 #: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js:49
msgid "Reposting in the background." msgid "Reposting in the background."
msgstr "بازنشر در پس زمینه" msgstr "بازنشر در پسزمینه"
#. Label of the represents_company (Link) field in DocType 'Purchase Invoice' #. Label of the represents_company (Link) field in DocType 'Purchase Invoice'
#. Label of the represents_company (Link) field in DocType 'Sales Invoice' #. Label of the represents_company (Link) field in DocType 'Sales Invoice'
@@ -41045,7 +41047,7 @@ msgstr ""
#: erpnext/quality_management/doctype/quality_procedure/quality_procedure.json #: erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
#: erpnext/quality_management/doctype/quality_review/quality_review.json #: erpnext/quality_management/doctype/quality_review/quality_review.json
msgid "Reviews" msgid "Reviews"
msgstr "" msgstr "بررسی ها"
#. Label of the rgt (Int) field in DocType 'Account' #. Label of the rgt (Int) field in DocType 'Account'
#. Label of the rgt (Int) field in DocType 'Company' #. Label of the rgt (Int) field in DocType 'Company'
@@ -41809,7 +41811,10 @@ msgid "Row #{0}: Selling rate for item {1} is lower than its {2}.\n"
"\t\t\t\t\tSelling {3} should be atleast {4}.<br><br>Alternatively,\n" "\t\t\t\t\tSelling {3} should be atleast {4}.<br><br>Alternatively,\n"
"\t\t\t\t\tyou can disable selling price validation in {5} to bypass\n" "\t\t\t\t\tyou can disable selling price validation in {5} to bypass\n"
"\t\t\t\t\tthis validation." "\t\t\t\t\tthis validation."
msgstr "" msgstr "ردیف #{0}: نرخ فروش برای کالای {1} کمتر از {2} آن است.\n"
"\t\t\t\t\tفروش {3} باید حداقل {4} باشد.<br><br>همچنین،\n"
"\t\t\t\t\tمیتوانید اعتبارسنجی قیمت فروش را در {5} غیرفعال کنید تا\n"
"\t\t\t\t\tاین اعتبارسنجی را دور بزنید."
#: erpnext/manufacturing/doctype/work_order/work_order.py:262 #: erpnext/manufacturing/doctype/work_order/work_order.py:262
msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}."
@@ -45413,7 +45418,7 @@ msgstr ""
#: erpnext/assets/doctype/asset/asset.js:300 #: erpnext/assets/doctype/asset/asset.js:300
#: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json #: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
msgid "Shift" msgid "Shift"
msgstr "تغییر مکان" msgstr "شیفت"
#. Label of the shift_factor (Float) field in DocType 'Asset Shift Factor' #. Label of the shift_factor (Float) field in DocType 'Asset Shift Factor'
#: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json #: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json
@@ -47777,7 +47782,7 @@ msgstr "فاکتور اشتراک"
#. Label of a Card Break in the Accounting Workspace #. Label of a Card Break in the Accounting Workspace
#: erpnext/accounts/workspace/accounting/accounting.json #: erpnext/accounts/workspace/accounting/accounting.json
msgid "Subscription Management" msgid "Subscription Management"
msgstr "" msgstr "مدیریت اشتراک"
#. Label of the subscription_period (Section Break) field in DocType #. Label of the subscription_period (Section Break) field in DocType
#. 'Subscription' #. 'Subscription'
@@ -50060,7 +50065,7 @@ msgstr "موجودی برای اقلام و انبارهای زیر رزرو ش
#: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:37 #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:37
msgid "The sync has started in the background, please check the {0} list for new records." msgid "The sync has started in the background, please check the {0} list for new records."
msgstr "همگام سازی در پس زمینه شروع شده است، لطفاً لیست {0} را برای رکوردهای جدید بررسی کنید." msgstr "همگام سازی در پسزمینه شروع شده است، لطفاً لیست {0} را برای رکوردهای جدید بررسی کنید."
#. Description of the 'Invoice Type Created via POS Screen' (Select) field in #. Description of the 'Invoice Type Created via POS Screen' (Select) field in
#. DocType 'POS Settings' #. DocType 'POS Settings'
@@ -50075,11 +50080,11 @@ msgstr ""
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1007 #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1007
msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage"
msgstr "تسک به عنوان یک کار پس زمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پس‌زمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه می‌کند و به مرحله پیش‌نویس باز می‌گردد." msgstr "تسک به عنوان یک کار پسزمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پس‌زمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه می‌کند و به مرحله پیش‌نویس باز می‌گردد."
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1018 #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1018
msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"
msgstr "تسک به عنوان یک کار پس زمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پس‌زمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه می‌کند و به مرحله ارسال باز می‌گردد." msgstr "تسک به عنوان یک کار پسزمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پس‌زمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه می‌کند و به مرحله ارسال باز می‌گردد."
#: erpnext/stock/doctype/material_request/material_request.py:334 #: erpnext/stock/doctype/material_request/material_request.py:334
msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}" msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}"
@@ -50449,7 +50454,7 @@ msgstr "این برنامه زمانی ایجاد شد که تعدیل ارزش
#: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py:207 #: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py:207
msgid "This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}." msgid "This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}."
msgstr "این برنامه زمانی ایجاد شد که تغییرات دارایی {0} از طریق تخصیص تغییر دارایی {1} تنظیم شد." msgstr "این زمان‌بندی زمانی ایجاد شد که شیفت‌های دارایی {0} از طریق تخصیص شیفت دارایی {1} تنظیم شدند."
#. Description of the 'Dunning Letter' (Section Break) field in DocType #. Description of the 'Dunning Letter' (Section Break) field in DocType
#. 'Dunning Type' #. 'Dunning Type'
@@ -51357,7 +51362,7 @@ msgstr "کل زمان نگهداری"
#. Label of the total_holidays (Int) field in DocType 'Holiday List' #. Label of the total_holidays (Int) field in DocType 'Holiday List'
#: erpnext/setup/doctype/holiday_list/holiday_list.json #: erpnext/setup/doctype/holiday_list/holiday_list.json
msgid "Total Holidays" msgid "Total Holidays"
msgstr "" msgstr "کل تعطیلات"
#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:121 #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:121
msgid "Total Income" msgid "Total Income"
@@ -51698,7 +51703,7 @@ msgstr "کل مالیات"
#: erpnext/stock/doctype/delivery_note/delivery_note.json #: erpnext/stock/doctype/delivery_note/delivery_note.json
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
msgid "Total Taxes and Charges" msgid "Total Taxes and Charges"
msgstr "" msgstr "کل مالیات‌ها و عوارض"
#. Label of the base_total_taxes_and_charges (Currency) field in DocType #. Label of the base_total_taxes_and_charges (Currency) field in DocType
#. 'Payment Entry' #. 'Payment Entry'
@@ -53532,7 +53537,7 @@ msgstr "اعتبار موجودی منفی"
#. 'Pricing Rule' #. 'Pricing Rule'
#: erpnext/accounts/doctype/pricing_rule/pricing_rule.json #: erpnext/accounts/doctype/pricing_rule/pricing_rule.json
msgid "Validate Pricing Rule" msgid "Validate Pricing Rule"
msgstr "" msgstr "اعتبارسنجی قانون قیمت‌گذاری"
#. Label of the validate_selling_price (Check) field in DocType 'Selling #. Label of the validate_selling_price (Check) field in DocType 'Selling
#. Settings' #. Settings'

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n" "Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n" "Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-11-16 09:35+0000\n" "POT-Creation-Date: 2025-11-16 09:35+0000\n"
"PO-Revision-Date: 2025-11-18 22:14\n" "PO-Revision-Date: 2025-11-19 22:23\n"
"Last-Translator: hello@frappe.io\n" "Last-Translator: hello@frappe.io\n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -2105,7 +2105,7 @@ msgstr "Računi za Spajanje"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264
msgid "Accrued Expenses" msgid "Accrued Expenses"
msgstr "Obračunati Troškovi" msgstr "Nagomilani Troškovi"
#. Option for the 'Account Type' (Select) field in DocType 'Account' #. Option for the 'Account Type' (Select) field in DocType 'Account'
#: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/account.json
@@ -25420,7 +25420,7 @@ msgstr "Kontakt Podizvođača"
#. Order' #. Order'
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
msgid "Job Worker Currency" msgid "Job Worker Currency"
msgstr "" msgstr "Valuta Podizvođača"
#. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting #. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting
#. Receipt' #. Receipt'

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n" "Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n" "Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-11-16 09:35+0000\n" "POT-Creation-Date: 2025-11-16 09:35+0000\n"
"PO-Revision-Date: 2025-11-18 22:13\n" "PO-Revision-Date: 2025-11-19 22:23\n"
"Last-Translator: hello@frappe.io\n" "Last-Translator: hello@frappe.io\n"
"Language-Team: Swedish\n" "Language-Team: Swedish\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -1988,7 +1988,7 @@ msgstr "Bokföring"
#. Settings' #. Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Accounts Closing" msgid "Accounts Closing"
msgstr "Bokföring Låsning" msgstr "Bokföring Stängning"
#. Label of the acc_frozen_upto (Date) field in DocType 'Accounts Settings' #. Label of the acc_frozen_upto (Date) field in DocType 'Accounts Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -2111,7 +2111,7 @@ msgstr "Konton att slå ihop"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264
msgid "Accrued Expenses" msgid "Accrued Expenses"
msgstr "Upplupna Kostnader" msgstr "Ackumulerade Kostnader"
#. Option for the 'Account Type' (Select) field in DocType 'Account' #. Option for the 'Account Type' (Select) field in DocType 'Account'
#: erpnext/accounts/doctype/account/account.json #: erpnext/accounts/doctype/account/account.json
@@ -16349,7 +16349,7 @@ msgstr "Förfallodatum kan inte vara före {0}"
#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:129 #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:129
msgid "Due to stock closing entry {0}, you cannot repost item valuation before {1}" msgid "Due to stock closing entry {0}, you cannot repost item valuation before {1}"
msgstr "På grund av lagerlåsning post {0} kan du inte lägga om artikel varuvärdering innan {1}" msgstr "På grund av lagerstängning post {0} kan du inte lägga om artikel varuvärdering innan {1}"
#. Name of a DocType #. Name of a DocType
#. Label of a Card Break in the Receivables Workspace #. Label of a Card Break in the Receivables Workspace
@@ -16443,7 +16443,7 @@ msgstr "Dubbletter av Försäljning Fakturor hittades"
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:78 #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:78
msgid "Duplicate Stock Closing Entry" msgid "Duplicate Stock Closing Entry"
msgstr "Kopiera Lagerlåsning Post" msgstr "Duplicera Lagestängning Post"
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:169 #: erpnext/accounts/doctype/pos_profile/pos_profile.py:169
msgid "Duplicate customer group found in the customer group table" msgid "Duplicate customer group found in the customer group table"
@@ -18405,7 +18405,7 @@ msgstr "Bokföringsår Start Datum"
#. 'Accounts Settings' #. 'Accounts Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) " msgid "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) "
msgstr "Finansiella Rapporter kommer att genereras med hjälp av Bokföring Poster (ska vara aktiverat om Period Låsning Verifikat inte publiceras för alla år i följd eller saknas)" msgstr "Finansiella Rapporter kommer att genereras med hjälp av Bokföring Poster (ska vara aktiverat om Period Stängning Verifikat inte publiceras för alla år i följd eller saknas) "
#: erpnext/manufacturing/doctype/work_order/work_order.js:843 #: erpnext/manufacturing/doctype/work_order/work_order.js:843
#: erpnext/manufacturing/doctype/work_order/work_order.js:858 #: erpnext/manufacturing/doctype/work_order/work_order.js:858
@@ -19797,7 +19797,7 @@ msgstr "Skapa Schema"
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.js:12 #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.js:12
msgid "Generate Stock Closing Entry" msgid "Generate Stock Closing Entry"
msgstr "Skapa Lagerlåsning Post" msgstr "Skapa Lagerstängning Post"
#. Description of a DocType #. Description of a DocType
#: erpnext/stock/doctype/packing_slip/packing_slip.json #: erpnext/stock/doctype/packing_slip/packing_slip.json
@@ -25425,7 +25425,7 @@ msgstr "Jobb Ansvarig Kontakt"
#. Order' #. Order'
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
msgid "Job Worker Currency" msgid "Job Worker Currency"
msgstr "Jobb Arbetare Valuta" msgstr "Jobb Ansvarig Valuta"
#. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting #. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting
#. Receipt' #. Receipt'
@@ -33432,13 +33432,13 @@ msgstr "Period Stängd"
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js:69 #: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js:69
#: erpnext/accounts/report/trial_balance/trial_balance.js:89 #: erpnext/accounts/report/trial_balance/trial_balance.js:89
msgid "Period Closing Entry For Current Period" msgid "Period Closing Entry For Current Period"
msgstr "Period Låsning Post för Aktuell Period" msgstr "Period Stängning Post för Aktuell Period"
#. Label of the period_closing_settings_section (Section Break) field in #. Label of the period_closing_settings_section (Section Break) field in
#. DocType 'Accounts Settings' #. DocType 'Accounts Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Period Closing Settings" msgid "Period Closing Settings"
msgstr "Period Låsning Inställningar" msgstr "Period Stängning Inställningar"
#. Label of the period_closing_voucher (Link) field in DocType 'Account Closing #. Label of the period_closing_voucher (Link) field in DocType 'Account Closing
#. Balance' #. Balance'
@@ -33448,7 +33448,7 @@ msgstr "Period Låsning Inställningar"
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json #: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json
#: erpnext/accounts/workspace/accounting/accounting.json #: erpnext/accounts/workspace/accounting/accounting.json
msgid "Period Closing Voucher" msgid "Period Closing Voucher"
msgstr "Period Låsning Verifikat" msgstr "Period Stängning Verifikat"
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:499 #: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:499
msgid "Period Closing Voucher {0} GL Entry Cancellation Failed" msgid "Period Closing Voucher {0} GL Entry Cancellation Failed"
@@ -39672,7 +39672,7 @@ msgstr "Hälsningar,"
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.js:27 #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.js:27
msgid "Regenerate Stock Closing Entry" msgid "Regenerate Stock Closing Entry"
msgstr "Återskapa Lagerlåsning Post" msgstr "Återskapa Lagerstängning Post"
#. Label of a Card Break in the Buying Workspace #. Label of a Card Break in the Buying Workspace
#: erpnext/buying/workspace/buying/buying.json #: erpnext/buying/workspace/buying/buying.json
@@ -46734,12 +46734,12 @@ msgstr "Lager Kapacitet"
#. Label of the stock_closing_tab (Tab Break) field in DocType 'Stock Settings' #. Label of the stock_closing_tab (Tab Break) field in DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json #: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Stock Closing" msgid "Stock Closing"
msgstr "Lager Låsning" msgstr "Lagerstängning"
#. Name of a DocType #. Name of a DocType
#: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json #: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json
msgid "Stock Closing Balance" msgid "Stock Closing Balance"
msgstr "Lagerlåsning Saldo" msgstr "Lagerstängning Saldo"
#. Label of the stock_closing_entry (Link) field in DocType 'Stock Closing #. Label of the stock_closing_entry (Link) field in DocType 'Stock Closing
#. Balance' #. Balance'
@@ -46747,19 +46747,19 @@ msgstr "Lagerlåsning Saldo"
#: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json #: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.json #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.json
msgid "Stock Closing Entry" msgid "Stock Closing Entry"
msgstr "Lagerlåsning Post" msgstr "Lagerstängning Post"
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:77 #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:77
msgid "Stock Closing Entry {0} already exists for the selected date range" msgid "Stock Closing Entry {0} already exists for the selected date range"
msgstr "Lagerlåsning Post {0} finns redan för vald datumintervall" msgstr "Lagerstängning Post {0} finns redan för vald datumintervall"
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:98 #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:98
msgid "Stock Closing Entry {0} has been queued for processing, system will take sometime to complete it." msgid "Stock Closing Entry {0} has been queued for processing, system will take sometime to complete it."
msgstr "Lagerlåsning Post {0} är i kö för behandling, och kommer att ta lite tid att slutföra." msgstr "Lagerstängning Post {0} är i kö för behandling, och kommer att ta lite tid att slutföra."
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry_dashboard.py:9 #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry_dashboard.py:9
msgid "Stock Closing Log" msgid "Stock Closing Log"
msgstr "Lagerlåsning Logg" msgstr "Lagerstängning Logg"
#. Label of the warehouse_and_reference (Section Break) field in DocType 'POS #. Label of the warehouse_and_reference (Section Break) field in DocType 'POS
#. Invoice Item' #. Invoice Item'

View File

@@ -558,12 +558,14 @@
{ {
"fieldname": "process_loss_percentage", "fieldname": "process_loss_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "% Process Loss" "label": "% Process Loss",
"non_negative": 1
}, },
{ {
"fieldname": "process_loss_qty", "fieldname": "process_loss_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Process Loss Qty", "label": "Process Loss Qty",
"non_negative": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -682,7 +684,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-11-06 15:27:54.806116", "modified": "2025-11-19 16:17:15.925156",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -10,6 +10,8 @@ import frappe
from frappe import _, bold from frappe import _, bold
from frappe.core.doctype.version.version import get_diff from frappe.core.doctype.version.version import get_diff
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Field
from frappe.query_builder.functions import Count, IfNull, Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, parse_json, today from frappe.utils import cint, cstr, flt, get_link_to_form, parse_json, today
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
@@ -1193,7 +1195,6 @@ def get_valuation_rate(data):
2) If no value, get last valuation rate from SLE 2) If no value, get last valuation rate from SLE
3) If no value, get valuation rate from Item 3) If no value, get valuation rate from Item
""" """
from frappe.query_builder.functions import Count, IfNull, Sum
from pypika import Case from pypika import Case
item_code, company = data.get("item_code"), data.get("company") item_code, company = data.get("item_code"), data.get("company")
@@ -1484,7 +1485,10 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
non_stock_items = frappe.get_all( non_stock_items = frappe.get_all(
"Item", "Item",
fields="name", fields="name",
filters={"name": ("in", list(items.keys())), "ifnull(is_stock_item, 0)": 0}, filters=[
["name", "in", list(items.keys())],
[IfNull(Field("is_stock_item"), 0), "=", 0],
],
as_list=1, as_list=1,
) )
@@ -1506,7 +1510,7 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
def add_operating_cost_component_wise( def add_operating_cost_component_wise(
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None, job_card=None stock_entry, work_order=None, consumed_operating_cost=None, op_expense_account=None, job_card=None
): ):
if not work_order: if not work_order:
return False return False
@@ -1530,11 +1534,11 @@ def add_operating_cost_component_wise(
get_component_account(wc.operating_component, stock_entry.company) or op_expense_account get_component_account(wc.operating_component, stock_entry.company) or op_expense_account
) )
actual_cp_operating_cost = flt( actual_cp_operating_cost = flt(
flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0), flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0) - consumed_operating_cost,
row.precision("actual_operating_cost"), row.precision("actual_operating_cost"),
) )
per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty) per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty - work_order.produced_qty)
if per_unit_cost and expense_account: if per_unit_cost and expense_account:
stock_entry.append( stock_entry.append(
@@ -1545,6 +1549,7 @@ def add_operating_cost_component_wise(
wc.operating_component, row.operation wc.operating_component, row.operation
), ),
"amount": per_unit_cost * flt(stock_entry.fg_completed_qty), "amount": per_unit_cost * flt(stock_entry.fg_completed_qty),
"has_operating_cost": 1,
}, },
) )
@@ -1561,13 +1566,20 @@ def get_component_account(parent, company):
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None): def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit from erpnext.stock.doctype.stock_entry.stock_entry import (
get_consumed_operating_cost,
get_operating_cost_per_unit,
)
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no) operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
if operating_cost_per_unit: if operating_cost_per_unit:
cost_added = add_operating_cost_component_wise( cost_added = add_operating_cost_component_wise(
stock_entry, work_order, operating_cost_per_unit, expense_account, job_card=job_card stock_entry,
work_order,
get_consumed_operating_cost(work_order.name, stock_entry.bom_no),
expense_account,
job_card=job_card,
) )
if not cost_added: if not cost_added:
@@ -1577,6 +1589,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
"expense_account": expense_account, "expense_account": expense_account,
"description": _("Operating Cost as per Work Order / BOM"), "description": _("Operating Cost as per Work Order / BOM"),
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty), "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
"has_operating_cost": 1,
}, },
) )
@@ -1594,8 +1607,6 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
) )
def get_max_operation_quantity(): def get_max_operation_quantity():
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Job Card") table = frappe.qb.DocType("Job Card")
query = ( query = (
frappe.qb.from_(table) frappe.qb.from_(table)
@@ -1610,8 +1621,6 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
return min([d.qty for d in query.run(as_dict=True)], default=0) return min([d.qty for d in query.run(as_dict=True)], default=0)
def get_utilised_corrective_cost(): def get_utilised_corrective_cost():
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Stock Entry") table = frappe.qb.DocType("Stock Entry")
subquery = ( subquery = (
frappe.qb.from_(table) frappe.qb.from_(table)
@@ -1721,7 +1730,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not searchfields: if not searchfields:
searchfields = ["name"] searchfields = ["name"]
query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())} query_filters = [
["disabled", "=", 0],
[IfNull(Field("end_of_life"), "3099-12-31"), ">", today()],
]
or_cond_filters = {} or_cond_filters = {}
if txt: if txt:
@@ -1730,8 +1742,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
barcodes = frappe.get_all( barcodes = frappe.get_all(
"Item Barcode", "Item Barcode",
fields=["distinct parent as item_code"], fields=["parent as item_code"],
filters={"barcode": ("like", f"%{txt}%")}, filters={"barcode": ("like", f"%{txt}%")},
distinct=True,
) )
barcodes = [d.item_code for d in barcodes] barcodes = [d.item_code for d in barcodes]
@@ -1741,11 +1754,11 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if filters and filters.get("item_code"): if filters and filters.get("item_code"):
has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants")
if not has_variants: if not has_variants:
query_filters["has_variants"] = 0 query_filters.append(["has_variants", "=", 0])
if filters: if filters:
for fieldname, value in filters.items(): for fieldname, value in filters.items():
query_filters[fieldname] = value query_filters.append([fieldname, "=", value])
return frappe.get_list( return frappe.get_list(
"Item", "Item",

View File

@@ -58,6 +58,15 @@ frappe.ui.form.on("Job Card", {
return doc.status === "Complete" ? "green" : "orange"; return doc.status === "Complete" ? "green" : "orange";
} }
}); });
frm.set_query("employee", () => {
return {
filters: {
company: frm.doc.company,
status: "Active",
},
};
});
}, },
set_company_filters(frm, fieldname) { set_company_filters(frm, fieldname) {

View File

@@ -207,7 +207,7 @@ class JobCard(Document):
job_card_qty = frappe.get_all( job_card_qty = frappe.get_all(
"Job Card", "Job Card",
fields=["sum(for_quantity)"], fields=[{"SUM": "for_quantity"}],
filters={ filters={
"work_order": self.work_order, "work_order": self.work_order,
"operation_id": self.operation_id, "operation_id": self.operation_id,
@@ -933,9 +933,9 @@ class JobCard(Document):
return frappe.get_all( return frappe.get_all(
"Job Card", "Job Card",
fields=[ fields=[
"sum(total_time_in_mins) as time_in_mins", {"SUM": "total_time_in_mins", "as": "time_in_mins"},
"sum(total_completed_qty) as completed_qty", {"SUM": "total_completed_qty", "as": "completed_qty"},
"sum(process_loss_qty) as process_loss_qty", {"SUM": "process_loss_qty", "as": "process_loss_qty"},
], ],
filters={ filters={
"docstatus": 1, "docstatus": 1,
@@ -1423,11 +1423,12 @@ def get_operations(doctype, txt, searchfield, start, page_len, filters):
return frappe.get_all( return frappe.get_all(
"Work Order Operation", "Work Order Operation",
filters=args, filters=args,
fields=["distinct operation as operation"], fields=["operation"],
limit_start=start, limit_start=start,
limit_page_length=page_len, limit_page_length=page_len,
order_by="idx asc", order_by="idx asc",
as_list=1, as_list=1,
distinct=True,
) )

View File

@@ -708,6 +708,119 @@ class TestJobCard(ERPNextTestSuite):
self.assertEqual(wo_doc.process_loss_qty, 2) self.assertEqual(wo_doc.process_loss_qty, 2)
self.assertEqual(wo_doc.status, "Completed") self.assertEqual(wo_doc.status, "Completed")
def test_op_cost_calculation(self):
from erpnext.manufacturing.doctype.routing.test_routing import (
create_routing,
setup_bom,
setup_operations,
)
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as make_stock_entry_for_wo,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
make_workstation(workstation_name="Test Workstation Z", hour_rate_rent=240)
operations = [
{"operation": "Test Operation A1", "workstation": "Test Workstation Z", "time_in_mins": 30},
]
warehouse = create_warehouse("Test Warehouse 123 for Job Card")
setup_operations(operations)
item_code = "Test Job Card Process Qty Item"
for item in [item_code, item_code + "RM 1", item_code + "RM 2"]:
if not frappe.db.exists("Item", item):
make_item(
item,
{
"item_name": item,
"stock_uom": "Nos",
"is_stock_item": 1,
},
)
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
bom_doc = setup_bom(
item_code=item_code,
routing=routing_doc.name,
raw_materials=[item_code + "RM 1", item_code + "RM 2"],
source_warehouse=warehouse,
)
for row in bom_doc.items:
make_stock_entry(
item_code=row.item_code,
target=row.source_warehouse,
qty=10,
basic_rate=100,
)
wo_doc = make_wo_order_test_record(
production_item=item_code,
bom_no=bom_doc.name,
qty=10,
skip_transfer=1,
wip_warehouse=warehouse,
source_warehouse=warehouse,
)
first_job_card = frappe.get_all(
"Job Card",
filters={"work_order": wo_doc.name, "sequence_id": 1},
fields=["name"],
order_by="sequence_id",
limit=1,
)[0].name
jc = frappe.get_doc("Job Card", first_job_card)
for _ in jc.scheduled_time_logs:
jc.append(
"time_logs",
{
"from_time": now(),
"to_time": add_to_date(now(), minutes=1),
"completed_qty": 4,
},
)
jc.for_quantity = 4
jc.save()
jc.submit()
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 4))
s.submit()
self.assertEqual(s.additional_costs[0].amount, 4)
make_job_card(
wo_doc.name,
[
{
"name": wo_doc.operations[0].name,
"operation": "Test Operation A1",
"qty": 6,
"pending_qty": 6,
}
],
)
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
job_card.append(
"time_logs",
{
"from_time": add_to_date(now(), hours=1),
"to_time": add_to_date(now(), hours=1, minutes=2),
"completed_qty": 6,
},
)
job_card.for_quantity = 6
job_card.save()
job_card.submit()
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6))
self.assertEqual(s.additional_costs[0].amount, 8)
def create_bom_with_multiple_operations(): def create_bom_with_multiple_operations():
"Create a BOM with multiple operations and Material Transfer against Job Card" "Create a BOM with multiple operations and Material Transfer against Job Card"

View File

@@ -624,7 +624,7 @@ class ProductionPlan(Document):
so_wise_planned_qty = frappe._dict() so_wise_planned_qty = frappe._dict()
data = frappe.get_all( data = frappe.get_all(
"Production Plan Item", "Production Plan Item",
fields=["sales_order", "sales_order_item", "SUM(planned_qty) as qty"], fields=["sales_order", "sales_order_item", {"SUM": "planned_qty", "as": "qty"}],
filters={ filters={
"sales_order": ("in", sales_orders), "sales_order": ("in", sales_orders),
"docstatus": 1, "docstatus": 1,

View File

@@ -73,9 +73,10 @@ class TestProductionPlan(IntegrationTestCase):
material_requests = frappe.get_all( material_requests = frappe.get_all(
"Material Request Item", "Material Request Item",
fields=["distinct parent"], fields=["parent"],
filters={"production_plan": pln.name}, filters={"production_plan": pln.name},
as_list=1, as_list=1,
distinct=True,
) )
self.assertTrue(len(material_requests), 2) self.assertTrue(len(material_requests), 2)

View File

@@ -976,8 +976,9 @@ class TestWorkOrder(IntegrationTestCase):
job_cards = frappe.get_all( job_cards = frappe.get_all(
"Job Card Time Log", "Job Card Time Log",
fields=["distinct parent as name", "docstatus"], fields=["parent as name", "docstatus"],
order_by="creation asc", order_by="creation asc",
distinct=True,
) )
for job_card in job_cards: for job_card in job_cards:

View File

@@ -166,9 +166,10 @@ class WorkOrder(Document):
operation_details = frappe._dict( operation_details = frappe._dict(
frappe.get_all( frappe.get_all(
"Job Card", "Job Card",
fields=["operation", "for_quantity"], fields=["operation", {"SUM": "for_quantity"}],
filters={"docstatus": ("<", 2), "work_order": self.name}, filters={"docstatus": ("<", 2), "work_order": self.name},
as_list=1, as_list=1,
group_by="operation_id",
) )
) )
@@ -717,7 +718,7 @@ class WorkOrder(Document):
if self.production_plan_item: if self.production_plan_item:
total_qty = frappe.get_all( total_qty = frappe.get_all(
"Work Order", "Work Order",
fields="sum(produced_qty) as produced_qty", fields=[{"SUM": "produced_qty", "as": "produced_qty"}],
filters={ filters={
"docstatus": 1, "docstatus": 1,
"production_plan": self.production_plan, "production_plan": self.production_plan,
@@ -1346,7 +1347,7 @@ class WorkOrder(Document):
else: else:
data = frappe.get_all( data = frappe.get_all(
"Stock Entry", "Stock Entry",
fields=["timestamp(posting_date, posting_time) as posting_datetime"], fields=[{"TIMESTAMP": ["posting_date", "posting_time"], "as": "posting_datetime"}],
filters={ filters={
"work_order": self.name, "work_order": self.name,
"purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]), "purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]),

View File

@@ -80,7 +80,7 @@ def get_filtered_data(filters):
def get_bom_count(bom_data): def get_bom_count(bom_data):
data = frappe.get_all( data = frappe.get_all(
"BOM Item", "BOM Item",
fields=["count(name) as count", "bom_no"], fields=[{"COUNT": "*", "as": "count"}, "bom_no"],
filters={"bom_no": ("in", bom_data)}, filters={"bom_no": ("in", bom_data)},
group_by="bom_no", group_by="bom_no",
) )

View File

@@ -59,7 +59,7 @@ def get_data(filters):
job_card_time_details = {} job_card_time_details = {}
for job_card_data in frappe.get_all( for job_card_data in frappe.get_all(
"Job Card Time Log", "Job Card Time Log",
fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"], fields=[{"MIN": "from_time", "as": "from_time"}, {"MAX": "to_time", "as": "to_time"}, "parent"],
filters=job_card_time_filter, filters=job_card_time_filter,
group_by="parent", group_by="parent",
): ):

View File

@@ -230,7 +230,12 @@ class ProductionPlanReport:
purchased_items = frappe.get_all( purchased_items = frappe.get_all(
"Purchase Order Item", "Purchase Order Item",
fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"], fields=[
"item_code",
{"MIN": "schedule_date", "as": "arrival_date"},
"qty as arrival_qty",
"warehouse",
],
filters={ filters={
"item_code": ("in", self.item_codes), "item_code": ("in", self.item_codes),
"warehouse": ("in", self.warehouses), "warehouse": ("in", self.warehouses),

View File

@@ -448,4 +448,4 @@ erpnext.patches.v16_0.update_serial_batch_entries
erpnext.patches.v16_0.set_company_wise_warehouses erpnext.patches.v16_0.set_company_wise_warehouses
erpnext.patches.v16_0.set_valuation_method_on_companies erpnext.patches.v16_0.set_valuation_method_on_companies
erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table
erpnext.patches.v16_0.migrate_budget_records_to_new_structure

View File

@@ -10,7 +10,7 @@ def execute():
frappe.reload_doc("stock", "doctype", "item") frappe.reload_doc("stock", "doctype", "item")
for data in frappe.get_all( for data in frappe.get_all(
"Item Quality Inspection Parameter", fields=["distinct parent"], filters={"parenttype": "Item"} "Item Quality Inspection Parameter", fields=["parent"], filters={"parenttype": "Item"}, distinct=True
): ):
qc_doc = frappe.new_doc("Quality Inspection Template") qc_doc = frappe.new_doc("Quality Inspection Template")
qc_doc.quality_inspection_template_name = "QIT/%s" % data.parent qc_doc.quality_inspection_template_name = "QIT/%s" % data.parent

View File

@@ -8,7 +8,7 @@ import frappe
def execute(): def execute():
warehouse_perm = frappe.get_all( warehouse_perm = frappe.get_all(
"User Permission", "User Permission",
fields=["count(*) as p_count", "is_default", "user"], fields=[{"COUNT": "*", "as": "p_count"}, "is_default", "user"],
filters={"allow": "Warehouse"}, filters={"allow": "Warehouse"},
group_by="user", group_by="user",
) )

View File

@@ -0,0 +1,128 @@
import frappe
from frappe.utils import add_months, flt, get_first_day, get_last_day
def execute():
remove_old_property_setter()
budget_names = frappe.db.get_list(
"Budget",
filters={"docstatus": ["in", [0, 1]]},
pluck="name",
)
for budget in budget_names:
migrate_single_budget(budget)
def remove_old_property_setter():
old_property_setter = frappe.db.get_value(
"Property Setter",
{
"doc_type": "Budget",
"field_name": "naming_series",
"property": "options",
"value": "Budget-.YYYY.-",
},
"name",
)
if old_property_setter:
frappe.delete_doc("Property Setter", old_property_setter, force=1)
def migrate_single_budget(budget_name):
budget_doc = frappe.get_doc("Budget", budget_name)
account_rows = frappe.get_all(
"Budget Account",
filters={"parent": budget_name},
fields=["account", "budget_amount"],
order_by="idx asc",
)
if not account_rows:
return
frappe.db.delete("Budget Account", filters={"parent": budget_doc.name})
percentage_allocations = get_percentage_allocations(budget_doc)
fiscal_year = frappe.get_cached_value(
"Fiscal Year",
budget_doc.fiscal_year,
["name", "year_start_date", "year_end_date"],
as_dict=True,
)
for row in account_rows:
create_new_budget_from_row(budget_doc, fiscal_year, row, percentage_allocations)
if budget_doc.docstatus == 1:
budget_doc.cancel()
else:
frappe.delete_doc("Budget", budget_name)
def get_percentage_allocations(budget_doc):
if budget_doc.monthly_distribution:
distribution_doc = frappe.get_cached_doc("Monthly Distribution", budget_doc.monthly_distribution)
return [flt(row.percentage_allocation) for row in distribution_doc.percentages]
return [100 / 12] * 12
def create_new_budget_from_row(budget_doc, fiscal_year, account_row, percentage_allocations):
new_budget = frappe.new_doc("Budget")
core_fields = ["budget_against", "company", "cost_center", "project"]
for field in core_fields:
new_budget.set(field, budget_doc.get(field))
new_budget.from_fiscal_year = fiscal_year.name
new_budget.to_fiscal_year = fiscal_year.name
new_budget.budget_start_date = fiscal_year.year_start_date
new_budget.budget_end_date = fiscal_year.year_end_date
new_budget.account = account_row.account
new_budget.budget_amount = flt(account_row.budget_amount)
new_budget.distribution_frequency = "Monthly"
new_budget.distribute_equally = 1 if len(set(percentage_allocations)) == 1 else 0
copy_fields = [
"applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr",
"action_if_accumulated_monthly_budget_exceeded_on_mr",
"applicable_on_purchase_order",
"action_if_annual_budget_exceeded_on_po",
"action_if_accumulated_monthly_budget_exceeded_on_po",
"applicable_on_booking_actual_expenses",
"action_if_annual_budget_exceeded",
"action_if_accumulated_monthly_budget_exceeded",
"applicable_on_cumulative_expense",
"action_if_annual_exceeded_on_cumulative_expense",
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
]
for field in copy_fields:
new_budget.set(field, budget_doc.get(field))
current_start = fiscal_year.year_start_date
for percentage in percentage_allocations:
new_budget.append(
"budget_distribution",
{
"start_date": get_first_day(current_start),
"end_date": get_last_day(current_start),
"percent": percentage,
"amount": new_budget.budget_amount * percentage / 100,
},
)
current_start = add_months(current_start, 1)
new_budget.flags.ignore_validate = True
new_budget.flags.ignore_links = True
new_budget.insert(ignore_permissions=True, ignore_mandatory=True)
if budget_doc.docstatus == 1:
new_budget.submit()

View File

@@ -401,8 +401,6 @@ def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, o
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
fields = "distinct *"
or_filters = [] or_filters = []
if txt: if txt:
@@ -424,13 +422,14 @@ def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, o
return frappe.get_list( return frappe.get_list(
doctype, doctype,
fields=fields, fields="*",
filters=filters, filters=filters,
or_filters=or_filters, or_filters=or_filters,
limit_start=limit_start, limit_start=limit_start,
limit_page_length=limit_page_length, limit_page_length=limit_page_length,
order_by=order_by, order_by=order_by,
ignore_permissions=ignore_permissions, ignore_permissions=ignore_permissions,
distinct=True,
) )

View File

@@ -4,13 +4,17 @@ erpnext.financial_statements = {
filters: get_filters(), filters: get_filters(),
baseData: null, baseData: null,
formatter: function (value, row, column, data, default_formatter, filter) { formatter: function (value, row, column, data, default_formatter, filter) {
const report_params = [value, row, column, data, default_formatter, filter];
// Growth/Margin // Growth/Margin
if (this._is_special_view(column, data)) if (erpnext.financial_statements._is_special_view(column, data))
return this._format_special_view(value, row, column, data, default_formatter); return erpnext.financial_statements._format_special_view(...report_params);
if (frappe.query_report.get_filter_value("report_template")) if (frappe.query_report.get_filter_value("report_template"))
return this._format_custom_report(value, row, column, data, default_formatter, filter); return erpnext.financial_statements._format_custom_report(...report_params);
else return this._format_standard_report(value, row, column, data, default_formatter, filter);
if (frappe.query_report.get_filter_value("report_template"))
return erpnext.financial_statements._format_custom_report(...report_params);
else return erpnext.financial_statements._format_standard_report(...report_params);
}, },
_is_special_view: function (column, data) { _is_special_view: function (column, data) {
@@ -20,11 +24,11 @@ erpnext.financial_statements = {
}, },
_format_custom_report: function (value, row, column, data, default_formatter, filter) { _format_custom_report: function (value, row, column, data, default_formatter, filter) {
const columnInfo = this._parse_column_info(column.fieldname, data); const columnInfo = erpnext.financial_statements._parse_column_info(column.fieldname, data);
const formatting = this._get_formatting_for_column(data, columnInfo); const formatting = erpnext.financial_statements._get_formatting_for_column(data, columnInfo);
if (columnInfo.isAccount) { if (columnInfo.isAccount) {
return this._format_custom_account_column( return erpnext.financial_statements._format_custom_account_column(
value, value,
data, data,
formatting, formatting,
@@ -33,7 +37,14 @@ erpnext.financial_statements = {
row row
); );
} else { } else {
return this._format_custom_value_column(value, data, formatting, column, default_formatter, row); return erpnext.financial_statements._format_custom_value_column(
value,
data,
formatting,
column,
default_formatter,
row
);
} }
}, },
@@ -99,7 +110,7 @@ erpnext.financial_statements = {
} }
// Style // Style
return this._style_custom_value(formattedValue, formatting, null); return erpnext.financial_statements._style_custom_value(formattedValue, formatting, null);
}, },
_format_custom_value_column: function (value, data, formatting, column, default_formatter, row) { _format_custom_value_column: function (value, data, formatting, column, default_formatter, row) {
@@ -111,7 +122,7 @@ erpnext.financial_statements = {
if (col.fieldtype === "Float") col.options = null; if (col.fieldtype === "Float") col.options = null;
let formattedValue = default_formatter(value, row, col, data); let formattedValue = default_formatter(value, row, col, data);
return this._style_custom_value(formattedValue, formatting, value); return erpnext.financial_statements._style_custom_value(formattedValue, formatting, value);
}, },
_style_custom_value(formattedValue, formatting, value) { _style_custom_value(formattedValue, formatting, value) {
@@ -157,7 +168,7 @@ erpnext.financial_statements = {
}, },
_format_standard_report: function (value, row, column, data, default_formatter, filter) { _format_standard_report: function (value, row, column, data, default_formatter, filter) {
if (data && column.fieldname == this.name_field) { if (data && column.fieldname == erpnext.financial_statements.name_field) {
value = data.section_name || data.account_name || value; value = data.section_name || data.account_name || value;
if (filter && filter?.text && filter?.type == "contains") { if (filter && filter?.text && filter?.type == "contains") {

View File

@@ -179,7 +179,11 @@ def get_reverse_charge_total(filters):
try: try:
return ( return (
frappe.db.get_all( frappe.db.get_all(
"Purchase Invoice", filters=query_filters, fields=["sum(base_total)"], as_list=True, limit=1 "Purchase Invoice",
filters=query_filters,
fields=[{"SUM": "base_total"}],
as_list=True,
limit=1,
)[0][0] )[0][0]
or 0 or 0
) )
@@ -219,7 +223,11 @@ def get_reverse_charge_recoverable_total(filters):
try: try:
return ( return (
frappe.db.get_all( frappe.db.get_all(
"Purchase Invoice", filters=query_filters, fields=["sum(base_total)"], as_list=True, limit=1 "Purchase Invoice",
filters=query_filters,
fields=[{"SUM": "base_total"}],
as_list=True,
limit=1,
)[0][0] )[0][0]
or 0 or 0
) )
@@ -274,7 +282,11 @@ def get_standard_rated_expenses_total(filters):
try: try:
return ( return (
frappe.db.get_all( frappe.db.get_all(
"Purchase Invoice", filters=query_filters, fields=["sum(base_total)"], as_list=True, limit=1 "Purchase Invoice",
filters=query_filters,
fields=[{"SUM": "base_total"}],
as_list=True,
limit=1,
)[0][0] )[0][0]
or 0 or 0
) )
@@ -292,7 +304,7 @@ def get_standard_rated_expenses_tax(filters):
frappe.db.get_all( frappe.db.get_all(
"Purchase Invoice", "Purchase Invoice",
filters=query_filters, filters=query_filters,
fields=["sum(recoverable_standard_rated_expenses)"], fields=[{"SUM": "recoverable_standard_rated_expenses"}],
as_list=True, as_list=True,
limit=1, limit=1,
)[0][0] )[0][0]
@@ -310,7 +322,7 @@ def get_tourist_tax_return_total(filters):
try: try:
return ( return (
frappe.db.get_all( frappe.db.get_all(
"Sales Invoice", filters=query_filters, fields=["sum(base_total)"], as_list=True, limit=1 "Sales Invoice", filters=query_filters, fields=[{"SUM": "base_total"}], as_list=True, limit=1
)[0][0] )[0][0]
or 0 or 0
) )
@@ -328,7 +340,7 @@ def get_tourist_tax_return_tax(filters):
frappe.db.get_all( frappe.db.get_all(
"Sales Invoice", "Sales Invoice",
filters=query_filters, filters=query_filters,
fields=["sum(tourist_tax_return)"], fields=[{"SUM": "tourist_tax_return"}],
as_list=True, as_list=True,
limit=1, limit=1,
)[0][0] )[0][0]

View File

@@ -14,6 +14,7 @@ from frappe.contacts.address_and_contact import (
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.utils.rename_doc import update_linked_doctypes from frappe.model.utils.rename_doc import update_linked_doctypes
from frappe.query_builder import Field, functions
from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils import cint, cstr, flt, get_formatted_email, today
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
@@ -503,11 +504,11 @@ def get_loyalty_programs(doc):
loyalty_programs = frappe.get_all( loyalty_programs = frappe.get_all(
"Loyalty Program", "Loyalty Program",
fields=["name", "customer_group", "customer_territory"], fields=["name", "customer_group", "customer_territory"],
filters={ filters=[
"auto_opt_in": 1, ["auto_opt_in", "=", 1],
"from_date": ["<=", today()], ["from_date", "<=", today()],
"ifnull(to_date, '2500-01-01')": [">=", today()], [functions.IfNull(Field("to_date"), "2500-01-01"), ">=", today()],
}, ],
) )
for loyalty_program in loyalty_programs: for loyalty_program in loyalty_programs:

View File

@@ -630,7 +630,7 @@ def get_ordered_items(quotation: str):
frappe.get_all( frappe.get_all(
"Sales Order Item", "Sales Order Item",
filters={"prevdoc_docname": quotation, "docstatus": 1}, filters={"prevdoc_docname": quotation, "docstatus": 1},
fields=["quotation_item", "sum(qty)"], fields=["quotation_item", {"SUM": "qty"}],
group_by="quotation_item", group_by="quotation_item",
as_list=1, as_list=1,
) )

View File

@@ -992,7 +992,11 @@ def get_requested_item_qty(sales_order):
for d in frappe.db.get_all( for d in frappe.db.get_all(
"Material Request Item", "Material Request Item",
filters={"docstatus": 1, "sales_order": sales_order}, filters={"docstatus": 1, "sales_order": sales_order},
fields=["sales_order_item", "sum(qty) as qty", "sum(received_qty) as received_qty"], fields=[
"sales_order_item",
{"SUM": "qty", "as": "qty"},
{"SUM": "received_qty", "as": "received_qty"},
],
group_by="sales_order_item", group_by="sales_order_item",
): ):
result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty}) result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty})

View File

@@ -95,7 +95,7 @@ def get_data(filters=None):
items = get_selling_items(filters) items = get_selling_items(filters)
item_stock_map = frappe.get_all( item_stock_map = frappe.get_all(
"Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code" "Bin", fields=["item_code", {"SUM": "actual_qty", "as": "available"}], group_by="item_code"
) )
item_stock_map = {item.item_code: item.available for item in item_stock_map} item_stock_map = {item.item_code: item.available for item in item_stock_map}
price_list_map = fetch_item_prices( price_list_map = fetch_item_prices(

View File

@@ -799,7 +799,7 @@ class EmailDigest(Document):
"status": ["not in", ("Cancelled")], "status": ["not in", ("Cancelled")],
"company": self.company, "company": self.company,
}, },
fields=["count(*) as count", "sum(grand_total) as grand_total"], fields=[{"COUNT": "*", "as": "count"}, {"SUM": "grand_total", "as": "grand_total"}],
) )
def get_from_to_date(self): def get_from_to_date(self):

View File

@@ -63,7 +63,7 @@ def get_all_customers(date_range, company, field, limit=None):
return frappe.get_list( return frappe.get_list(
"Sales Invoice", "Sales Invoice",
fields=["customer as name", "sum(outstanding_amount) as value"], fields=["customer as name", {"SUM": "outstanding_amount", "as": "value"}],
filters=filters, filters=filters,
group_by="customer", group_by="customer",
order_by="value desc", order_by="value desc",
@@ -80,7 +80,7 @@ def get_all_customers(date_range, company, field, limit=None):
return frappe.get_list( return frappe.get_list(
"Sales Order", "Sales Order",
fields=["customer as name", f"sum({select_field}) as value"], fields=["customer as name", {"SUM": select_field, "as": "value"}],
filters=filters, filters=filters,
group_by="customer", group_by="customer",
order_by="value desc", order_by="value desc",
@@ -91,10 +91,10 @@ def get_all_customers(date_range, company, field, limit=None):
@frappe.whitelist() @frappe.whitelist()
def get_all_items(date_range, company, field, limit=None): def get_all_items(date_range, company, field, limit=None):
if field in ("available_stock_qty", "available_stock_value"): if field in ("available_stock_qty", "available_stock_value"):
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)" sum_field = "actual_qty" if field == "available_stock_qty" else "stock_value"
results = frappe.db.get_all( results = frappe.db.get_all(
"Bin", "Bin",
fields=["item_code as name", f"{select_field} as value"], fields=["item_code as name", {"SUM": sum_field, "as": "value"}],
group_by="item_code", group_by="item_code",
order_by="value desc", order_by="value desc",
limit=limit, limit=limit,
@@ -125,7 +125,7 @@ def get_all_items(date_range, company, field, limit=None):
select_doctype, select_doctype,
fields=[ fields=[
f"`tab{child_doctype}`.item_code as name", f"`tab{child_doctype}`.item_code as name",
f"sum(`tab{child_doctype}`.{select_field}) as value", {"SUM": f"`tab{child_doctype}`.{select_field}", "as": "value"},
], ],
filters=filters, filters=filters,
order_by="value desc", order_by="value desc",
@@ -145,7 +145,7 @@ def get_all_suppliers(date_range, company, field, limit=None):
return frappe.get_list( return frappe.get_list(
"Purchase Invoice", "Purchase Invoice",
fields=["supplier as name", "sum(outstanding_amount) as value"], fields=["supplier as name", {"SUM": "outstanding_amount", "as": "value"}],
filters=filters, filters=filters,
group_by="supplier", group_by="supplier",
order_by="value desc", order_by="value desc",
@@ -162,7 +162,7 @@ def get_all_suppliers(date_range, company, field, limit=None):
return frappe.get_list( return frappe.get_list(
"Purchase Order", "Purchase Order",
fields=["supplier as name", f"sum({select_field}) as value"], fields=["supplier as name", {"SUM": select_field, "as": "value"}],
filters=filters, filters=filters,
group_by="supplier", group_by="supplier",
order_by="value desc", order_by="value desc",
@@ -186,7 +186,7 @@ def get_all_sales_partner(date_range, company, field, limit=None):
"Sales Order", "Sales Order",
fields=[ fields=[
"sales_partner as name", "sales_partner as name",
f"sum({select_field}) as value", {"SUM": select_field, "as": "value"},
], ],
filters=filters, filters=filters,
group_by="sales_partner", group_by="sales_partner",
@@ -210,7 +210,7 @@ def get_all_sales_person(date_range, company, field=None, limit=0):
"Sales Order", "Sales Order",
fields=[ fields=[
"`tabSales Team`.sales_person as name", "`tabSales Team`.sales_person as name",
"sum(`tabSales Team`.allocated_amount) as value", {"SUM": "`tabSales Team`.allocated_amount", "as": "value"},
], ],
filters=filters, filters=filters,
group_by="`tabSales Team`.sales_person", group_by="`tabSales Team`.sales_person",

View File

@@ -31,7 +31,7 @@ def get(
warehouses = frappe.get_list( warehouses = frappe.get_list(
"Bin", "Bin",
fields=["warehouse", "sum(stock_value) stock_value"], fields=["warehouse", {"SUM": "stock_value", "as": "stock_value"}],
filters={"warehouse": ["IN", warehouses], "stock_value": [">", 0]}, filters={"warehouse": ["IN", warehouses], "stock_value": [">", 0]},
group_by="warehouse", group_by="warehouse",
order_by="stock_value DESC", order_by="stock_value DESC",

View File

@@ -405,8 +405,9 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
serial_nos = get_serial_nos(serial_no) serial_nos = get_serial_nos(serial_no)
batches = frappe.get_all( batches = frappe.get_all(
"Serial No", "Serial No",
fields=["distinct batch_no"], fields=["batch_no"],
filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)}, filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)},
distinct=True,
) )
if not batches: if not batches:

View File

@@ -320,12 +320,13 @@ def get_inventory_documents(
return frappe.get_all( return frappe.get_all(
"DocField", "DocField",
fields=["distinct parent"], fields=["parent"],
filters=and_filters, filters=and_filters,
or_filters=or_filters, or_filters=or_filters,
start=start, start=start,
page_length=page_len, page_length=page_len,
as_list=1, as_list=1,
distinct=True,
) )
@@ -382,7 +383,7 @@ def get_inventory_dimensions():
return frappe.get_all( return frappe.get_all(
"Inventory Dimension", "Inventory Dimension",
fields=[ fields=[
"distinct target_fieldname as fieldname", "target_fieldname as fieldname",
"source_fieldname", "source_fieldname",
"reference_document as doctype", "reference_document as doctype",
"validate_negative_stock", "validate_negative_stock",
@@ -390,6 +391,7 @@ def get_inventory_dimensions():
], ],
filters={"disabled": 0}, filters={"disabled": 0},
order_by="creation", order_by="creation",
distinct=True,
) )

View File

@@ -12,7 +12,8 @@
"col_break3", "col_break3",
"amount", "amount",
"base_amount", "base_amount",
"has_corrective_cost" "has_corrective_cost",
"has_operating_cost"
], ],
"fields": [ "fields": [
{ {
@@ -70,13 +71,20 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Has Corrective Cost", "label": "Has Corrective Cost",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "has_operating_cost",
"fieldtype": "Check",
"label": "Has Operating Cost",
"read_only": 1
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-06-09 10:22:20.286641", "modified": "2025-07-16 15:27:59.175530",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Landed Cost Taxes and Charges", "name": "Landed Cost Taxes and Charges",

View File

@@ -21,6 +21,7 @@ class LandedCostTaxesandCharges(Document):
exchange_rate: DF.Float exchange_rate: DF.Float
expense_account: DF.Link | None expense_account: DF.Link | None
has_corrective_cost: DF.Check has_corrective_cost: DF.Check
has_operating_cost: DF.Check
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data

View File

@@ -346,6 +346,9 @@ frappe.ui.form.on("Material Request", {
label: __("For Warehouse"), label: __("For Warehouse"),
options: "Warehouse", options: "Warehouse",
reqd: 1, reqd: 1,
get_query: function () {
return { filters: { company: frm.doc.company } };
},
}, },
{ fieldname: "qty", fieldtype: "Float", label: __("Quantity"), reqd: 1, default: 1 }, { fieldname: "qty", fieldtype: "Float", label: __("Quantity"), reqd: 1, default: 1 },
{ {

View File

@@ -121,7 +121,12 @@ def get_indexed_packed_items_table(doc):
""" """
indexed_table = {} indexed_table = {}
for packed_item in doc.get("packed_items"): for packed_item in doc.get("packed_items"):
key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname) key = (
packed_item.parent_item,
packed_item.item_code,
packed_item.idx if doc.is_new() else packed_item.parent_detail_docname,
)
indexed_table[key] = packed_item indexed_table[key] = packed_item
return indexed_table return indexed_table
@@ -182,7 +187,11 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re
exists, pi_row = False, {} exists, pi_row = False, {}
# check if row already exists in packed items table # check if row already exists in packed items table
key = (main_item_row.item_code, packing_item.item_code, main_item_row.name) key = (
main_item_row.item_code,
packing_item.item_code,
main_item_row.idx if doc.is_new() else main_item_row.name,
)
if packed_items_table.get(key): if packed_items_table.get(key):
pi_row, exists = packed_items_table.get(key), True pi_row, exists = packed_items_table.get(key), True

View File

@@ -593,7 +593,7 @@ class TestPickList(IntegrationTestCase):
for dn in frappe.get_all( for dn in frappe.get_all(
"Delivery Note", "Delivery Note",
filters={"against_pick_list": pick_list.name, "customer": "_Test Customer"}, filters={"against_pick_list": pick_list.name, "customer": "_Test Customer"},
fields={"name"}, fields=["name"],
): ):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
self.assertEqual(dn_item.item_code, "_Test Item") self.assertEqual(dn_item.item_code, "_Test Item")
@@ -604,7 +604,7 @@ class TestPickList(IntegrationTestCase):
for dn in frappe.get_all( for dn in frappe.get_all(
"Delivery Note", "Delivery Note",
filters={"against_pick_list": pick_list.name, "customer": "_Test Customer 1"}, filters={"against_pick_list": pick_list.name, "customer": "_Test Customer 1"},
fields={"name"}, fields=["name"],
): ):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
self.assertEqual(dn_item.item_code, "_Test Item 2") self.assertEqual(dn_item.item_code, "_Test Item 2")
@@ -637,7 +637,7 @@ class TestPickList(IntegrationTestCase):
pick_list_1.submit() pick_list_1.submit()
create_delivery_note(pick_list_1.name) create_delivery_note(pick_list_1.name)
for dn in frappe.get_all( for dn in frappe.get_all(
"Delivery Note", filters={"against_pick_list": pick_list_1.name}, fields={"name"} "Delivery Note", filters={"against_pick_list": pick_list_1.name}, fields=["name"]
): ):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
if dn_item.item_code == "_Test Item": if dn_item.item_code == "_Test Item":

View File

@@ -1333,7 +1333,7 @@ def get_item_wise_returned_qty(pr_doc):
"Purchase Receipt", "Purchase Receipt",
fields=[ fields=[
"`tabPurchase Receipt Item`.purchase_receipt_item", "`tabPurchase Receipt Item`.purchase_receipt_item",
"sum(abs(`tabPurchase Receipt Item`.qty)) as qty", {"SUM": [{"ABS": "`tabPurchase Receipt Item`.qty"}], "as": "qty"},
], ],
filters=[ filters=[
["Purchase Receipt", "docstatus", "=", 1], ["Purchase Receipt", "docstatus", "=", 1],

View File

@@ -18,6 +18,9 @@ def get_data():
"Purchase Order": ["items", "purchase_order"], "Purchase Order": ["items", "purchase_order"],
"Project": ["items", "project"], "Project": ["items", "project"],
}, },
"internal_and_external_links": {
"Purchase Invoice": ["items", "purchase_invoice"],
},
"transactions": [ "transactions": [
{ {
"label": _("Related"), "label": _("Related"),

View File

@@ -1900,7 +1900,7 @@ class TestPurchaseReceipt(IntegrationTestCase):
data = frappe.get_all( data = frappe.get_all(
"Stock Ledger Entry", "Stock Ledger Entry",
filters={"voucher_no": pr_return.name, "docstatus": 1}, filters={"voucher_no": pr_return.name, "docstatus": 1},
fields=["SUM(stock_value_difference) as stock_value_difference"], fields=[{"SUM": "stock_value_difference", "as": "stock_value_difference"}],
)[0] )[0]
self.assertEqual(abs(data["stock_value_difference"]), 400.00) self.assertEqual(abs(data["stock_value_difference"]), 400.00)

View File

@@ -1393,7 +1393,36 @@ class SerialandBatchBundle(Document):
if self.voucher_type == "POS Invoice": if self.voucher_type == "POS Invoice":
return return
if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1: child_doctype = self.voucher_type + " Item"
mapper = {
"Asset Capitalization": "Asset Capitalization Stock Item",
"Asset Repair": "Asset Repair Consumed Item",
"Stock Entry": "Stock Entry Detail",
}.get(self.voucher_type)
if mapper:
child_doctype = mapper
if self.voucher_type == "Delivery Note" and not frappe.db.exists(
"Delivery Note Item", self.voucher_detail_no
):
child_doctype = "Packed Item"
elif self.voucher_type == "Sales Invoice" and not frappe.db.exists(
"Sales Invoice Item", self.voucher_detail_no
):
child_doctype = "Packed Item"
elif self.voucher_type == "Subcontracting Receipt" and not frappe.db.exists(
"Subcontracting Receipt Item", self.voucher_detail_no
):
child_doctype = "Subcontracting Receipt Supplied Item"
if (
frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1
and self.voucher_detail_no
and frappe.db.exists(child_doctype, self.voucher_detail_no)
):
msg = f"""The {self.voucher_type} {bold(self.voucher_no)} msg = f"""The {self.voucher_type} {bold(self.voucher_no)}
is in submitted state, please cancel it first""" is in submitted state, please cancel it first"""
frappe.throw(_(msg)) frappe.throw(_(msg))

View File

@@ -976,12 +976,10 @@ frappe.ui.form.on("Stock Entry Detail", {
no_batch_serial_number_value = true; no_batch_serial_number_value = true;
} }
if ( if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) {
no_batch_serial_number_value && if (!frappe.flags.dialog_set) {
!frappe.flags.hide_serial_batch_dialog &&
!frappe.flags.dialog_set
) {
frappe.flags.dialog_set = true; frappe.flags.dialog_set = true;
}
erpnext.stock.select_batch_and_serial_no(frm, d); erpnext.stock.select_batch_and_serial_no(frm, d);
} else { } else {
frappe.flags.dialog_set = false; frappe.flags.dialog_set = false;

View File

@@ -2420,7 +2420,7 @@ class StockEntry(StockController, SubcontractingInwardController):
data = frappe.get_all( data = frappe.get_all(
"Work Order Operation", "Work Order Operation",
filters={"parent": self.work_order}, filters={"parent": self.work_order},
fields=["max(process_loss_qty) as process_loss_qty"], fields=[{"MAX": "process_loss_qty", "as": "process_loss_qty"}],
) )
if data and data[0].process_loss_qty is not None: if data and data[0].process_loss_qty is not None:
@@ -3145,7 +3145,7 @@ class StockEntry(StockController, SubcontractingInwardController):
stock_entries_child_list.append(d.ste_detail) stock_entries_child_list.append(d.ste_detail)
transferred_qty = frappe.get_all( transferred_qty = frappe.get_all(
"Stock Entry Detail", "Stock Entry Detail",
fields=["sum(qty) as qty"], fields=[{"SUM": "qty", "as": "qty"}],
filters={ filters={
"against_stock_entry": d.against_stock_entry, "against_stock_entry": d.against_stock_entry,
"ste_detail": d.ste_detail, "ste_detail": d.ste_detail,
@@ -3417,6 +3417,26 @@ def get_work_order_details(work_order, company):
} }
def get_consumed_operating_cost(wo_name, bom_no):
table = frappe.qb.DocType("Stock Entry")
child_table = frappe.qb.DocType("Landed Cost Taxes and Charges")
query = (
frappe.qb.from_(child_table)
.join(table)
.on(child_table.parent == table.name)
.select(Sum(child_table.amount).as_("consumed_cost"))
.where(
(table.docstatus == 1)
& (table.work_order == wo_name)
& (table.purpose == "Manufacture")
& (table.bom_no == bom_no)
& (child_table.has_operating_cost == 1)
)
)
cost = query.run(pluck="consumed_cost")
return cost[0] if cost and cost[0] else 0
def get_operating_cost_per_unit(work_order=None, bom_no=None): def get_operating_cost_per_unit(work_order=None, bom_no=None):
operating_cost_per_unit = 0 operating_cost_per_unit = 0
if work_order: if work_order:
@@ -3434,7 +3454,9 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
for d in work_order.get("operations"): for d in work_order.get("operations"):
if flt(d.completed_qty): if flt(d.completed_qty):
operating_cost_per_unit += flt(d.actual_operating_cost) / flt(d.completed_qty) operating_cost_per_unit += flt(
d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no)
) / flt(d.completed_qty - work_order.produced_qty)
elif work_order.qty: elif work_order.qty:
operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty) operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty)

View File

@@ -8,6 +8,7 @@ from uuid import uuid4
import frappe import frappe
from frappe.core.page.permission_manager.permission_manager import reset from frappe.core.page.permission_manager.permission_manager import reset
from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.query_builder.functions import Timestamp
from frappe.tests import IntegrationTestCase from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, add_to_date, flt, today from frappe.utils import add_days, add_to_date, flt, today
@@ -1281,12 +1282,16 @@ class TestStockLedgerEntry(IntegrationTestCase, StockTestMixin):
item=item, from_warehouse=source_warehouse, to_warehouse=target_warehouse, qty=1_728.0 item=item, from_warehouse=source_warehouse, to_warehouse=target_warehouse, qty=1_728.0
) )
filters = {"voucher_no": transfer.name, "voucher_type": transfer.doctype, "is_cancelled": 0} sle = frappe.qb.DocType("Stock Ledger Entry")
sles = frappe.get_all( sles = (
"Stock Ledger Entry", frappe.qb.from_(sle)
fields=["*"], .select("*")
filters=filters, .where(sle.voucher_no == transfer.name)
order_by="timestamp(posting_date, posting_time), creation", .where(sle.voucher_type == transfer.doctype)
.where(sle.is_cancelled == 0)
.orderby(Timestamp(sle.posting_date, sle.posting_time))
.orderby(sle.creation)
.run(as_dict=True)
) )
self.assertEqual(abs(sles[0].stock_value_difference), sles[1].stock_value_difference) self.assertEqual(abs(sles[0].stock_value_difference), sles[1].stock_value_difference)

View File

@@ -977,6 +977,7 @@ class StockReconciliation(StockController):
is_customer_item = frappe.get_cached_value("Item", d.item_code, "is_customer_provided_item") is_customer_item = frappe.get_cached_value("Item", d.item_code, "is_customer_provided_item")
if is_customer_item and d.valuation_rate: if is_customer_item and d.valuation_rate:
d.valuation_rate = 0.0 d.valuation_rate = 0.0
d.allow_zero_valuation_rate = 1
changed_any_values = True changed_any_values = True
if changed_any_values: if changed_any_values:

View File

@@ -196,12 +196,10 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "allow_zero_valuation_rate",
"fieldname": "allow_zero_valuation_rate", "fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Zero Valuation Rate", "label": "Allow Zero Valuation Rate",
"print_hide": 1, "print_hide": 1
"read_only": 1
}, },
{ {
"depends_on": "barcode", "depends_on": "barcode",
@@ -268,7 +266,7 @@
"grid_page_length": 50, "grid_page_length": 50,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-04-28 22:40:30.086415", "modified": "2025-11-20 15:27:13.868179",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Reconciliation Item", "name": "Stock Reconciliation Item",

View File

@@ -24,6 +24,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Conversion Factor", "label": "Conversion Factor",
"non_negative": 1,
"oldfieldname": "conversion_factor", "oldfieldname": "conversion_factor",
"oldfieldtype": "Float" "oldfieldtype": "Float"
} }
@@ -31,12 +32,13 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:10:57.645955", "modified": "2025-11-19 21:27:13.968771",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "UOM Conversion Detail", "name": "UOM Conversion Detail",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []

View File

@@ -7,6 +7,8 @@ import json
import frappe import frappe
from frappe import _, throw from frappe import _, throw
from frappe.contacts.address_and_contact import load_address_and_contact from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.query_builder import Field
from frappe.query_builder.functions import IfNull
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.caching import request_cache from frappe.utils.caching import request_cache
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
@@ -197,10 +199,12 @@ def get_children(doctype, parent=None, company=None, is_root=False, include_disa
include_disabled = json.loads(include_disabled) include_disabled = json.loads(include_disabled)
fields = ["name as value", "is_group as expandable"] fields = ["name as value", "is_group as expandable"]
filters = [ filters = [
["ifnull(`parent_warehouse`, '')", "=", parent], [IfNull(Field("parent_warehouse"), ""), "=", parent],
["company", "in", (company, None, "")], ["company", "in", (company, None, "")],
] ]
if frappe.db.has_column(doctype, "disabled") and not include_disabled: if frappe.db.has_column(doctype, "disabled") and not include_disabled:
filters.append(["disabled", "=", False]) filters.append(["disabled", "=", False])
@@ -236,7 +240,9 @@ def get_child_warehouses(warehouse):
def get_warehouses_based_on_account(account, company=None): def get_warehouses_based_on_account(account, company=None):
warehouses = [] warehouses = []
for d in frappe.get_all("Warehouse", fields=["name", "is_group"], filters={"account": account}): for d in frappe.get_all(
"Warehouse", fields=["name", "is_group"], filters={"account": account, "disabled": 0}
):
if d.is_group: if d.is_group:
warehouses.extend(get_child_warehouses(d.name)) warehouses.extend(get_child_warehouses(d.name))
else: else:

View File

@@ -115,7 +115,7 @@ def get_stock_ledger_entries(report_filters):
"posting_time", "posting_time",
"company", "company",
"warehouse", "warehouse",
"(stock_value_difference / actual_qty) as valuation_rate", {"DIV": ["stock_value_difference", "actual_qty"], "as": "valuation_rate"},
] ]
filters = {"is_cancelled": 0} filters = {"is_cancelled": 0}

View File

@@ -143,9 +143,9 @@ def get_stock_details_map(variant_list):
stock_details = frappe.db.get_all( stock_details = frappe.db.get_all(
"Bin", "Bin",
fields=[ fields=[
"sum(planned_qty) as planned_qty", {"SUM": "planned_qty", "as": "planned_qty"},
"sum(actual_qty) as actual_qty", {"SUM": "actual_qty", "as": "actual_qty"},
"sum(projected_qty) as projected_qty", {"SUM": "projected_qty", "as": "projected_qty"},
"item_code", "item_code",
], ],
filters={"item_code": ["in", variant_list]}, filters={"item_code": ["in", variant_list]},
@@ -167,7 +167,7 @@ def get_buying_price_map(variant_list):
buying = frappe.db.get_all( buying = frappe.db.get_all(
"Item Price", "Item Price",
fields=[ fields=[
"avg(price_list_rate) as avg_rate", {"AVG": "price_list_rate", "as": "avg_rate"},
"item_code", "item_code",
], ],
filters={"item_code": ["in", variant_list], "buying": 1}, filters={"item_code": ["in", variant_list], "buying": 1},
@@ -185,7 +185,7 @@ def get_selling_price_map(variant_list):
selling = frappe.db.get_all( selling = frappe.db.get_all(
"Item Price", "Item Price",
fields=[ fields=[
"avg(price_list_rate) as avg_rate", {"AVG": "price_list_rate", "as": "avg_rate"},
"item_code", "item_code",
], ],
filters={"item_code": ["in", variant_list], "selling": 1}, filters={"item_code": ["in", variant_list], "selling": 1},

View File

@@ -183,14 +183,15 @@ def get_voucher_type(doctype, txt, searchfield, start, page_len, filters):
child_doctypes = frappe.get_all( child_doctypes = frappe.get_all(
"DocField", "DocField",
filters={"fieldname": "serial_and_batch_bundle"}, filters={"fieldname": "serial_and_batch_bundle"},
fields=["distinct parent as parent"], fields=["parent"],
distinct=True,
) )
query_filters = {"options": ["in", [d.parent for d in child_doctypes]]} query_filters = {"options": ["in", [d.parent for d in child_doctypes]]}
if txt: if txt:
query_filters["parent"] = ["like", f"%{txt}%"] query_filters["parent"] = ["like", f"%{txt}%"]
return frappe.get_all("DocField", filters=query_filters, fields=["distinct parent"], as_list=True) return frappe.get_all("DocField", filters=query_filters, fields=["parent"], as_list=True, distinct=True)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -61,7 +61,7 @@ def get_stock_ledger_data(report_filters, filters):
"name", "name",
"voucher_type", "voucher_type",
"voucher_no", "voucher_no",
"sum(stock_value_difference) as stock_value", {"SUM": "stock_value_difference", "as": "stock_value"},
"posting_date", "posting_date",
"posting_time", "posting_time",
], ],
@@ -88,7 +88,10 @@ def get_gl_data(report_filters, filters):
"name", "name",
"voucher_type", "voucher_type",
"voucher_no", "voucher_no",
"sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value", {
"SUB": [{"SUM": "debit_in_account_currency"}, {"SUM": "credit_in_account_currency"}],
"as": "account_value",
},
], ],
group_by="voucher_type, voucher_no", group_by="voucher_type, voucher_no",
) )

View File

@@ -546,7 +546,10 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
opening_data = frappe.get_all( opening_data = frappe.get_all(
"Stock Ledger Entry", "Stock Ledger Entry",
fields=["sum(actual_qty) as qty_after_transaction", "sum(stock_value_difference) as stock_value"], fields=[
{"SUM": "actual_qty", "as": "qty_after_transaction"},
{"SUM": "stock_value_difference", "as": "stock_value"},
],
filters=query_filters, filters=query_filters,
)[0] )[0]

View File

@@ -1511,7 +1511,7 @@ def get_batchwise_qty(voucher_type, voucher_no):
batches = frappe.get_all( batches = frappe.get_all(
"Serial and Batch Entry", "Serial and Batch Entry",
filters={"parent": ("in", bundles), "batch_no": ("is", "set")}, filters={"parent": ("in", bundles), "batch_no": ("is", "set")},
fields=["batch_no", "SUM(qty) as qty"], fields=["batch_no", {"SUM": "qty", "as": "qty"}],
group_by="batch_no", group_by="batch_no",
as_list=1, as_list=1,
) )

View File

@@ -1,6 +1,7 @@
import json import json
import frappe import frappe
from frappe.query_builder.functions import Timestamp
from frappe.tests import IntegrationTestCase from frappe.tests import IntegrationTestCase
from erpnext.stock.utils import scan_barcode from erpnext.stock.utils import scan_barcode
@@ -20,11 +21,23 @@ class StockTestMixin:
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
if sle_filters: if sle_filters:
filters.update(sle_filters) filters.update(sle_filters)
sles = frappe.get_all(
"Stock Ledger Entry", sle = frappe.qb.DocType("Stock Ledger Entry")
fields=["*"], query = (
filters=filters, frappe.qb.from_(sle)
order_by="timestamp(posting_date, posting_time), creation", .select("*")
.where(sle.voucher_no == doc.name)
.where(sle.voucher_type == doc.doctype)
.where(sle.is_cancelled == 0)
)
if sle_filters:
for key, value in sle_filters.items():
query = query.where(sle[key] == value)
sles = (
query.orderby(Timestamp(sle.posting_date, sle.posting_time))
.orderby(sle.creation)
.run(as_dict=True)
) )
self.assertGreaterEqual(len(sles), len(expected_sles)) self.assertGreaterEqual(len(sles), len(expected_sles))

View File

@@ -0,0 +1,269 @@
{
"app": "erpnext",
"creation": "2025-11-17 13:19:05.050624",
"docstatus": 0,
"doctype": "Workspace Sidebar",
"header_icon": "setting",
"idx": 0,
"items": [
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.051033",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "home",
"idx": 1,
"indent": 0,
"keep_closed": 0,
"label": "Home",
"link_to": "Settings",
"link_type": "Workspace",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "gj96mk2v27",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.050624",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "crm",
"idx": 2,
"indent": 0,
"keep_closed": 0,
"label": "CRM Settings",
"link_to": "CRM Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "evs5nlr1q0",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.053190",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "sell",
"idx": 3,
"indent": 0,
"keep_closed": 0,
"label": "Selling Settings",
"link_to": "Selling Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "gj9u1k3i56",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.053492",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "buying",
"idx": 4,
"indent": 0,
"keep_closed": 0,
"label": "Buying Settings",
"link_to": "Buying Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "gj9ct0hfhj",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.052094",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "accounting",
"idx": 5,
"indent": 0,
"keep_closed": 0,
"label": "Accounts Settings",
"link_to": "Accounts Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "gj996hcboc",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.052884",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "stock",
"idx": 6,
"indent": 0,
"keep_closed": 0,
"label": "Stock Settings",
"link_to": "Stock Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "gj9hup4r96",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.050624",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "building-2",
"idx": 7,
"indent": 0,
"keep_closed": 0,
"label": "Manufacturing Settings",
"link_to": "Manufacturing Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "e0c9du5q9a",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.051420",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "printer",
"idx": 8,
"indent": 0,
"keep_closed": 0,
"label": "Print Settings",
"link_to": "Print Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "gj9j587oit",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.051734",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "computer",
"idx": 9,
"indent": 0,
"keep_closed": 0,
"label": "System Settings",
"link_to": "System Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "gj9uqukv96",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.052565",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "earth",
"idx": 10,
"indent": 0,
"keep_closed": 0,
"label": "Global Defaults",
"link_to": "Global Defaults",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "gj9koe7aa7",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"creation": "2025-11-17 13:19:05.050624",
"docstatus": 0,
"doctype": "Workspace Sidebar Item",
"icon": "projects",
"idx": 11,
"indent": 0,
"keep_closed": 0,
"label": "Projects Settings",
"link_to": "Projects Settings",
"link_type": "DocType",
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"name": "e0ccorpl3c",
"owner": "Administrator",
"parent": "Settings",
"parentfield": "items",
"parenttype": "Workspace Sidebar",
"show_arrow": 0,
"type": "Link"
}
],
"modified": "2025-11-19 16:15:06.422138",
"modified_by": "Administrator",
"module": "Setup",
"name": "Settings",
"owner": "Administrator",
"title": "Settings"
}