Merge branch 'develop' into version-16-beta

This commit is contained in:
Rohit Waghchaure
2025-11-21 22:08:39 +05:30
135 changed files with 107631 additions and 34332 deletions

View File

@@ -50,6 +50,15 @@ pull_request_rules:
- version-15-hotfix
assignees:
- "{{ author }}"
- name: backport to version-16-beta
conditions:
- label="backport version-16-beta"
actions:
backport:
branches:
- version-16-beta
assignees:
- "{{ author }}"
- name: Automatic merge on CI success and review
conditions:
- status-success=linters

View File

@@ -14,6 +14,7 @@ import openpyxl
from frappe import _
from frappe.core.doctype.data_import.data_import import DataImport
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.file_manager import get_file, save_file
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
@@ -371,7 +372,7 @@ def get_import_status(docname):
logs = frappe.get_all(
"Data Import Log",
fields=["count(*) as count", "success"],
fields=[{"COUNT": "*", "as": "count"}, "success"],
filters={"data_import": docname},
group_by="success",
)

View File

@@ -4,16 +4,6 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Budget", {
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 () {
return {
filters: {
@@ -30,8 +20,28 @@ frappe.ui.form.on("Budget", {
});
},
refresh: function (frm) {
refresh: async function (frm) {
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) {
@@ -39,6 +49,15 @@ frappe.ui.form.on("Budget", {
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) {
if (frm.doc.budget_against == "Cost Center") {
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("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",
"cost_center",
"project",
"fiscal_year",
"account",
"column_break_3",
"monthly_distribution",
"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",
"applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr",
@@ -32,8 +41,8 @@
"applicable_on_cumulative_expense",
"action_if_annual_exceeded_on_cumulative_expense",
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
"section_break_21",
"accounts"
"section_break_kkan",
"revision_of"
],
"fields": [
{
@@ -44,6 +53,7 @@
"in_standard_filter": 1,
"label": "Budget Against",
"options": "\nCost Center\nProject",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
@@ -53,6 +63,7 @@
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
@@ -62,7 +73,8 @@
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Cost Center",
"options": "Cost Center"
"options": "Cost Center",
"read_only_depends_on": "eval: doc.revision_of"
},
{
"depends_on": "eval:doc.budget_against == 'Project'",
@@ -70,28 +82,13 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Project",
"options": "Project"
},
{
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Fiscal Year",
"options": "Fiscal Year",
"reqd": 1
"options": "Project",
"read_only_depends_on": "eval: doc.revision_of"
},
{
"fieldname": "column_break_3",
"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",
"fieldtype": "Link",
@@ -187,22 +184,12 @@
"options": "\nStop\nWarn\nIgnore"
},
{
"fieldname": "section_break_21",
"fieldtype": "Section Break"
},
{
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Budget Accounts",
"options": "Budget Account",
"reqd": 1
},
{
"default": "BUDGET-.########",
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"no_copy": 1,
"options": "BUDGET-.YYYY.-",
"options": "BUDGET-.########",
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
@@ -232,13 +219,97 @@
"fieldtype": "Select",
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
"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,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-06-16 15:57:13.114981",
"modified": "2025-11-19 17:00:00.648224",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",

View File

@@ -2,10 +2,14 @@
# For license information, please see license.txt
from datetime import date
import frappe
from frappe import _
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 (
get_accounting_dimensions,
@@ -30,9 +34,9 @@ class Budget(Document):
if TYPE_CHECKING:
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_on_mr: 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_purchase_order: DF.Check
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
cost_center: DF.Link | None
fiscal_year: DF.Link
monthly_distribution: DF.Link | None
naming_series: DF.Literal["BUDGET-.YYYY.-"]
distribute_equally: DF.Check
distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
from_fiscal_year: DF.Link
naming_series: DF.Literal["BUDGET-.########"]
project: DF.Link | None
revision_of: DF.Data | None
to_fiscal_year: DF.Link
# end: auto-generated types
def validate(self):
if not self.get(frappe.scrub(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_accounts()
self.validate_account()
self.set_null_value()
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):
budget_against_field = frappe.scrub(self.budget_against)
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(
"""
select
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
where
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format(
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))
),
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
as_dict=1,
f"""
SELECT name, account
FROM `tabBudget`
WHERE
docstatus < 2
AND company = %s
AND {budget_against_field} = %s
AND account = %s
AND name != %s
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(
_(
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
"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),
DuplicateBudgetError,
)
def validate_accounts(self):
account_list = []
for d in self.get("accounts"):
if d.account:
account_details = frappe.get_cached_value(
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1
def validate_account(self):
if not self.account:
frappe.throw(_("Account is mandatory"))
account_details = frappe.get_cached_value(
"Account", self.account, ["is_group", "company", "report_type"], as_dict=1
)
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(self.account))
elif account_details.company != self.company:
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
elif account_details.report_type != "Profit and Loss":
frappe.throw(
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
self.account
)
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
elif account_details.company != self.company:
frappe.throw(
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
)
elif account_details.report_type != "Profit and Loss":
frappe.throw(
_(
"Budget cannot be assigned against {0}, as it's not an Income or Expense 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):
if self.budget_against == "Cost Center":
@@ -139,30 +187,201 @@ class Budget(Document):
):
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):
args = frappe._dict(args)
params = frappe._dict(
{
"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):
return
if not args.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
if not params.fiscal_year:
params.fiscal_year = get_fiscal_year(params.get("posting_date"), company=params.get("company"))[0]
if args.get("company"):
frappe.flags.exception_approver_role = frappe.get_cached_value(
"Company", args.get("company"), "exception_budget_approver_role"
)
posting_date = getdate(params.get("posting_date"))
posting_fiscal_year = get_fiscal_year(posting_date, company=params.get("company"))[0]
year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year)
if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}):
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 budget_exists:
return
if not args.account:
args.account = args.get("expense_account")
if params.get("company"):
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:
args.cost_center, args.account = get_item_details(args)
if not params.account:
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
default_dimensions = [
@@ -180,59 +399,78 @@ def validate_expense_against_budget(args, expense_amount=0):
budget_against = dimension.get("fieldname")
if (
args.get(budget_against)
and args.account
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
params.get(budget_against)
and params.account
and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense")
):
doctype = dimension.get("document_type")
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}`
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
args.is_tree = True
params.is_tree = True
else:
condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
args.is_tree = False
condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}"
params.is_tree = False
args.budget_against_field = budget_against
args.budget_against_doctype = doctype
params.budget_against_field = budget_against
params.budget_against_doctype = doctype
budget_records = frappe.db.sql(
f"""
select
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
ifnull(b.applicable_on_material_request, 0) as for_material_request,
ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
b.action_if_annual_budget_exceeded, 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, `tabBudget Account` ba
where
b.name=ba.parent and b.fiscal_year=%s
and ba.account=%s and b.docstatus=1
SELECT
b.name,
b.{budget_against} AS budget_against,
b.budget_amount,
b.from_fiscal_year,
b.to_fiscal_year,
b.budget_start_date,
b.budget_end_date,
IFNULL(b.applicable_on_material_request, 0) AS for_material_request,
IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order,
IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses,
b.action_if_annual_budget_exceeded,
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}
""",
(args.fiscal_year, args.account),
""",
(params.company, params.posting_date, params.account),
as_dict=True,
) # nosec
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:
if flt(budget.budget_amount):
yearly_action, monthly_action = get_actions(args, budget)
args["for_material_request"] = budget.for_material_request
args["for_purchase_order"] = budget.for_purchase_order
yearly_action, monthly_action = get_actions(params, budget)
params["for_material_request"] = budget.for_material_request
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"):
compare_expense_with_budget(
args,
params,
flt(budget.budget_amount),
_("Annual"),
yearly_action,
@@ -241,14 +479,12 @@ def validate_budget_records(args, budget_records, expense_amount):
)
if monthly_action in ["Stop", "Warn"]:
budget_amount = get_accumulated_monthly_budget(
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
)
budget_amount = get_accumulated_monthly_budget(budget.name, params.posting_date)
args["month_end_date"] = get_last_day(args.posting_date)
params["month_end_date"] = get_last_day(params.posting_date)
compare_expense_with_budget(
args,
params,
budget_amount,
_("Accumulated Monthly"),
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):
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
def compare_expense_with_budget(params, budget_amount, action_for, action, budget_against, amount=0):
params.actual_expense, params.requested_amount, params.ordered_amount = get_actual_expense(params), 0, 0
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:
amount = args.requested_amount + args.ordered_amount
if params.get("doctype") == "Material Request" and params.for_material_request:
amount = params.requested_amount + params.ordered_amount
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
amount = args.ordered_amount
elif params.get("doctype") == "Purchase Order" and params.for_purchase_order:
amount = params.ordered_amount
total_expense = args.actual_expense + amount
total_expense = params.actual_expense + amount
if total_expense > budget_amount:
if args.actual_expense > budget_amount:
diff = args.actual_expense - budget_amount
if params.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}.")
else:
diff = total_expense - budget_amount
_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(
_(action_for),
frappe.bold(args.account),
frappe.unscrub(args.budget_against_field),
frappe.bold(params.account),
frappe.unscrub(params.budget_against_field),
frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, 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(
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"))
def get_expense_breakup(args, currency, budget_against):
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
def get_expense_breakup(params, currency, budget_against):
msg = "<hr> {} - <ul>".format(_("Total Expenses booked through"))
common_filters = frappe._dict(
{
args.budget_against_field: budget_against,
"account": args.account,
"company": args.company,
params.budget_against_field: budget_against,
"account": params.account,
"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(
"General Ledger",
label=_("Actual Expenses"),
filters=common_filters.copy().update(
{
"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,
}
),
filters=gl_filters,
)
+ " - "
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
+ frappe.bold(fmt_money(params.actual_expense, currency=currency))
+ "</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 += (
"<li>"
@@ -337,22 +592,24 @@ def get_expense_breakup(args, currency, budget_against):
label=_("Material Requests"),
report_type="Report Builder",
doctype="Material Request",
filters=common_filters.copy().update(
{
"status": [["!=", "Stopped"]],
"docstatus": 1,
"material_request_type": "Purchase",
"schedule_date": [["fiscal year", "2023-2024"]],
"item_code": args.item_code,
"per_ordered": [["<", 100]],
}
),
filters=mr_filters,
)
+ " - "
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
+ frappe.bold(fmt_money(params.requested_amount, currency=currency))
+ "</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 += (
"<li>"
+ frappe.utils.get_link_to_report(
@@ -360,42 +617,34 @@ def get_expense_breakup(args, currency, budget_against):
label=_("Unbilled Orders"),
report_type="Report Builder",
doctype="Purchase Order",
filters=common_filters.copy().update(
{
"status": [["!=", "Closed"]],
"docstatus": 1,
"transaction_date": [["fiscal year", "2023-2024"]],
"item_code": args.item_code,
"per_billed": [["<", 100]],
}
),
filters=po_filters,
)
+ " - "
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
+ frappe.bold(fmt_money(params.ordered_amount, currency=currency))
+ "</li></ul>"
)
return msg
def get_actions(args, budget):
def get_actions(params, budget):
yearly_action = budget.action_if_annual_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
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
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
return yearly_action, monthly_action
def get_requested_amount(args):
item_code = args.get("item_code")
condition = get_other_condition(args, "Material Request")
def get_requested_amount(params):
item_code = params.get("item_code")
condition = get_other_condition(params, "Material Request")
data = frappe.db.sql(
""" 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
def get_ordered_amount(args):
item_code = args.get("item_code")
condition = get_other_condition(args, "Purchase Order")
def get_ordered_amount(params):
item_code = params.get("item_code")
condition = get_other_condition(params, "Purchase Order")
data = frappe.db.sql(
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
@@ -425,111 +674,102 @@ def get_ordered_amount(args):
return data[0][0] if data else 0
def get_other_condition(args, for_doc):
condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field")
def get_other_condition(params, for_doc):
condition = f"expense_account = '{params.expense_account}'"
budget_against_field = params.get("budget_against_field")
if budget_against_field and args.get(budget_against_field):
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
if budget_against_field and params.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"
start_date, end_date = frappe.get_cached_value(
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
)
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
condition += f""" and parent.{date_field}
between '{start_date}' and '{end_date}' """
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_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
def get_actual_expense(args):
if not args.budget_against_doctype:
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
def get_actual_expense(params):
if not params.budget_against_doctype:
params.budget_against_doctype = frappe.unscrub(params.budget_against_field)
budget_against_field = args.get("budget_against_field")
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
budget_against_field = params.get("budget_against_field")
condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else ""
if args.is_tree:
date_condition = (
f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
)
if params.is_tree:
lft_rgt = frappe.db.get_value(
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
params.budget_against_doctype, params.get(budget_against_field), ["lft", "rgt"], as_dict=1
)
params.update(lft_rgt)
args.update(lft_rgt)
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
where lft>=%(lft)s and rgt<=%(rgt)s
and name=gle.{budget_against_field})"""
condition2 = f"""
and exists(
select name from `tab{params.budget_against_doctype}`
where lft >= %(lft)s and rgt <= %(rgt)s
and name = gle.{budget_against_field}
)
"""
else:
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
where name=gle.{budget_against_field} and
gle.{budget_against_field} = %({budget_against_field})s)"""
condition2 = f"""
and gle.{budget_against_field} = %({budget_against_field})s
"""
amount = flt(
frappe.db.sql(
f"""
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where
is_cancelled = 0
and gle.account=%(account)s
{condition1}
and gle.fiscal_year=%(fiscal_year)s
and gle.company=%(company)s
and gle.docstatus=1
{condition2}
""",
(args),
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where
is_cancelled = 0
and gle.account = %(account)s
{condition1}
{date_condition}
and gle.company = %(company)s
and gle.docstatus = 1
{condition2}
""",
params,
)[0][0]
) # nosec
return amount
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {}
if monthly_distribution:
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
md = frappe.qb.DocType("Monthly Distribution")
def get_accumulated_monthly_budget(budget_name, posting_date):
posting_date = getdate(posting_date)
res = (
frappe.qb.from_(mdp)
.join(md)
.on(mdp.parent == md.name)
.select(mdp.month, mdp.percentage_allocation)
.where(md.fiscal_year == fiscal_year)
.where(md.name == monthly_distribution)
.run(as_dict=True)
)
bd = frappe.qb.DocType("Budget Distribution")
b = frappe.qb.DocType("Budget")
for d in res:
distribution.setdefault(d.month, d.percentage_allocation)
result = (
frappe.qb.from_(bd)
.join(b)
.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)
)
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
return flt(result[0]["accumulated_amount"]) if result else 0.0
def get_item_details(args):
def get_item_details(params):
cost_center, expense_account = None, None
if not args.get("company"):
if not params.get("company"):
return cost_center, expense_account
if args.item_code:
if params.item_code:
item_defaults = frappe.db.get_value(
"Item Default",
{"parent": args.item_code, "company": args.get("company")},
{"parent": params.item_code, "company": params.get("company")},
["buying_cost_center", "expense_account"],
)
if item_defaults:
@@ -537,7 +777,7 @@ def get_item_details(args):
if not (cost_center and expense_account):
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:
cost_center = data[0]
@@ -551,14 +791,39 @@ def get_item_details(args):
return cost_center, expense_account
def get_expense_cost_center(doctype, args):
def get_expense_cost_center(doctype, params):
if doctype == "Item Group":
return frappe.db.get_value(
"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"],
)
else:
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 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 (
BudgetError,
get_accumulated_monthly_budget,
get_actual_expense,
revise_budget,
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_fiscal_year
@@ -25,11 +27,15 @@ class TestBudget(ERPNextTestSuite):
def setUp(self):
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):
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(
"_Test Account Cost for Goods Sold - _TC",
@@ -50,12 +56,13 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_crossed_stop1(self):
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")
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(
"_Test Account Cost for Goods Sold - _TC",
@@ -73,13 +80,11 @@ class TestBudget(ERPNextTestSuite):
def test_exception_approver_role(self):
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")
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
)
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
@@ -107,16 +112,16 @@ class TestBudget(ERPNextTestSuite):
applicable_on_purchase_order=1,
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
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, "fiscal_year", fiscal_year)
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(
{
"doctype": "Material Request",
@@ -151,14 +156,15 @@ class TestBudget(ERPNextTestSuite):
applicable_on_purchase_order=1,
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
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, "fiscal_year", fiscal_year)
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(
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):
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")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
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(
"_Test Account Cost for Goods Sold - _TC",
@@ -200,7 +207,7 @@ class TestBudget(ERPNextTestSuite):
def test_yearly_budget_crossed_stop1(self):
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(
"_Test Account Cost for Goods Sold - _TC",
@@ -217,7 +224,7 @@ class TestBudget(ERPNextTestSuite):
def test_yearly_budget_crossed_stop2(self):
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"})
@@ -237,7 +244,7 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_on_cancellation1(self):
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
if month > 9:
month = 9
@@ -266,7 +273,7 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_on_cancellation2(self):
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
if 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", "_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")
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(
"_Test Account Cost for Goods Sold - _TC",
@@ -331,11 +344,14 @@ class TestBudget(ERPNextTestSuite):
}
).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")
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(
"_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},
)
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(
"_Test Account Cost for Goods Sold - _TC",
@@ -387,12 +408,15 @@ class TestBudget(ERPNextTestSuite):
def test_action_for_cumulative_limit(self):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center", applicable_on_cumulative_expense=True)
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
budget = make_budget(
budget_against="Cost Center",
applicable_on_cumulative_expense=True,
do_not_save=False,
submit_budget=True,
)
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
@@ -422,6 +446,165 @@ class TestBudget(ERPNextTestSuite):
po.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):
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"
fiscal_year = get_fiscal_year(nowdate())[0]
fiscal_year_start_date, fiscal_year_end_date = get_fiscal_year(nowdate())[1:3]
args = frappe._dict(
{
"account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
"monthly_end_date": posting_date,
"month_end_date": posting_date,
"company": "_Test Company",
"fiscal_year": fiscal_year,
"from_fiscal_year": fiscal_year,
"to_fiscal_year": fiscal_year,
"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):
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)
if existing_expense:
@@ -474,18 +668,33 @@ def make_budget(**args):
budget_against = args.budget_against
cost_center = args.cost_center
fiscal_year = get_fiscal_year(nowdate())[0]
if budget_against == "Project":
project_name = "{}%".format("_Test Project/" + fiscal_year)
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
project = frappe.get_value("Project", {"project_name": "_Test Project"})
budget_list = frappe.get_all(
"Budget",
filters={
"project": project,
"account": "_Test Account Cost for Goods Sold - _TC",
},
pluck="name",
)
else:
cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)})
for d in budget_list:
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
budget_list = frappe.get_all(
"Budget",
filters={
"cost_center": cost_center or "_Test Cost Center - _TC",
"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")
@@ -494,18 +703,18 @@ def make_budget(**args):
else:
budget.cost_center = cost_center or "_Test Cost Center - _TC"
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
monthly_distribution.fiscal_year = fiscal_year
monthly_distribution.save()
budget.fiscal_year = fiscal_year
budget.monthly_distribution = "_Test Distribution"
budget.from_fiscal_year = args.from_fiscal_year or fiscal_year
budget.to_fiscal_year = args.to_fiscal_year or fiscal_year
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.action_if_annual_budget_exceeded = "Stop"
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
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:
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"
)
budget.insert()
budget.submit()
if not args.do_not_save:
try:
budget.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
if args.submit_budget:
budget.submit()
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
from frappe.query_builder import functions
from frappe.query_builder.utils import DocType
from frappe.tests import IntegrationTestCase
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_credit, 8500.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all(
"GL Entry",
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]
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_credit, 500.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
(
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
],
)[0]
# account shouldn't have balance in base and account currency
@@ -193,12 +199,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
pe.references = []
pe.save().submit()
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
(
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
],
)[0]
# 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_credit, precision), 0.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
(
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
],
)[0]
# account shouldn't have balance in base and account currency post revaluation

View File

@@ -453,7 +453,7 @@ class JournalEntry(AccountsController):
if (
d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
and d.debit
):
asset = frappe.get_cached_doc("Asset", d.reference_name)

View File

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

View File

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

View File

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

View File

@@ -191,6 +191,9 @@ class POSInvoice(SalesInvoice):
super().__init__(*args, **kwargs)
def validate(self):
if not self.customer:
frappe.throw(_("Please select Customer first"))
if not cint(self.is_pos):
frappe.throw(
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
@@ -390,14 +393,14 @@ class POSInvoice(SalesInvoice):
):
return
from erpnext.stock.stock_ledger import is_negative_stock_allowed
for d in self.get("items"):
if not d.serial_and_batch_bundle:
if is_negative_stock_allowed(item_code=d.item_code):
return
available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
if is_negative_stock_allowed:
continue
item_code, warehouse, _qty = (
frappe.bold(d.item_code),
@@ -855,20 +858,22 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
return bin_qty - pos_sales_qty, is_stock_item, is_negative_stock_allowed(item_code=item_code)
else:
is_stock_item = True
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
return get_bundle_availability(item_code, warehouse), is_stock_item
return get_bundle_availability(item_code, warehouse), is_stock_item, False
else:
is_stock_item = False
# Is a service item or non_stock item
return 0, is_stock_item
return 0, is_stock_item, False
def get_bundle_availability(bundle_item_code, warehouse):

View File

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

View File

@@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args):
if group_condition:
conditions += " and " + group_condition
if args.get("transaction_date"):
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date"
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date")
values["transaction_date"] = date
if args.get("doctype") in [
"Quotation",

View File

@@ -1374,7 +1374,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": creditors_account, "docstatus": 1, "reference_name": pi.name},
"sum(debit) as amount",
[{"SUM": "debit", "as": "amount"}],
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), 2500)
@@ -1456,7 +1456,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name},
"sum(debit) as amount",
[{"SUM": "debit", "as": "amount"}],
group_by="reference_name",
)[0].amount
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 = [
x.document_type
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
@@ -287,7 +290,11 @@ def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters
filters.update({"document_type": ("like", f"%{txt}%")})
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 []

View File

@@ -3612,7 +3612,7 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.db.get_all(
"Payment Ledger Entry",
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,
)

View File

@@ -121,7 +121,7 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
gl_entries = frappe.db.get_all(
"GL Entry",
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",
)
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",
fields=[
"company",
"sum(grand_total) as grand_total",
"sum(base_grand_total) as base_grand_total",
{"SUM": "grand_total", "as": "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()),
},
group_by="company",
fields=["company", "sum(loyalty_points) as loyalty_points"],
fields=["company", {"SUM": "loyalty_points", "as": "loyalty_points"}],
as_list=1,
)
)

View File

@@ -210,7 +210,7 @@ def get_gl_balance(report_date, company):
return frappe._dict(
frappe.db.get_all(
"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},
group_by="party",
as_list=1,

View File

@@ -11,8 +11,8 @@ import frappe.defaults
from frappe import _, qb, throw
from frappe.desk.reportview import build_match_conditions
from frappe.model.meta import get_field_precision
from frappe.query_builder import AliasedQuery, Case, Criterion, Table
from frappe.query_builder.functions import Count, Max, Round, Sum
from frappe.query_builder import AliasedQuery, Case, Criterion, Field, Table
from frappe.query_builder.functions import Count, IfNull, Max, Round, Sum
from frappe.query_builder.utils import DocType
from frappe.utils import (
add_days,
@@ -1311,7 +1311,10 @@ def get_children(doctype, parent, company, is_root=False, include_disabled=False
if frappe.db.has_column(doctype, "disabled") and not include_disabled:
filters.append(["disabled", "=", False])
filters.append([f'ifnull(`{parent_fieldname}`,"")', "=", "" if is_root else parent])
if is_root:
filters.append(IfNull(Field(parent_fieldname), "") == "")
else:
filters.append([parent_fieldname, "=", parent])
if is_root:
fields += ["root_type", "report_type", "account_currency"] if doctype == "Account" else []

View File

@@ -59,10 +59,8 @@ class BudgetValidation:
_obj.update(
{
"accumulated_monthly_budget": get_accumulated_monthly_budget(
self.budget_map[key].monthly_distribution,
self.budget_map[key].name,
self.doc_date,
self.fiscal_year,
self.budget_map[key].budget_amount,
)
}
)
@@ -164,16 +162,19 @@ class BudgetValidation:
def get_budget_records(self) -> list:
bud = qb.DocType("Budget")
bud_acc = qb.DocType("Budget Account")
query = (
qb.from_(bud)
.inner_join(bud_acc)
.on(bud.name == bud_acc.parent)
.select(
bud.name,
bud.budget_against,
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.action_if_annual_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.action_if_annual_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:
query = query.select(bud[x.get("fieldname")])
@@ -314,8 +317,8 @@ class BudgetValidation:
frappe.bold(key[2]),
frappe.bold(frappe.unscrub(key[0])),
frappe.bold(key[1]),
frappe.bold(fmt_money(annual_diff, 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)
@@ -425,7 +428,7 @@ class BudgetValidation:
frappe.bold(key[2]),
frappe.bold(frappe.unscrub(key[0])),
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),
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"
fields = [
f"sum(abs(`tab{child_doctype}`.qty)) as qty",
{"SUM": [{"ABS": f"`tab{child_doctype}`.qty"}], "as": "qty"},
]
if doctype != "Subcontracting Receipt":
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"):
fields += [
f"sum(abs(`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}`.rejected_qty"}], "as": "rejected_qty"},
{"SUM": [{"ABS": f"`tab{child_doctype}`.received_qty"}], "as": "received_qty"},
]
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
data = frappe.get_all(

View File

@@ -99,6 +99,7 @@ class SellingController(StockController):
# set contact and address details for customer, if they are not mentioned
self.set_missing_lead_customer_details(for_validate=for_validate)
self.set_price_list_and_item_details(for_validate=for_validate)
self.set_company_contact_person()
def set_missing_lead_customer_details(self, for_validate=False):
customer, lead = None, None
@@ -154,6 +155,13 @@ class SellingController(StockController):
self.set_price_list_currency("Selling")
self.set_missing_item_details(for_validate=for_validate)
def set_company_contact_person(self):
"""Set the Company's Default Sales Contact as Company Contact Person."""
if self.company and self.meta.has_field("company_contact_person") and not self.company_contact_person:
self.company_contact_person = frappe.get_cached_value(
"Company", self.company, "default_sales_contact"
)
def remove_shipping_charge(self):
if self.shipping_rule:
shipping_rule = frappe.get_last_doc("Shipping Rule", self.shipping_rule)

View File

@@ -101,6 +101,7 @@ status_map = {
["Draft", None],
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
[
"Completed",
@@ -562,11 +563,14 @@ class StatusUpdater(Document):
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:
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
* 100,
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.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):
if not from_repost:
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)
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(
{

View File

@@ -292,7 +292,7 @@ class SubcontractingController(StockController):
):
for row in frappe.get_all(
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)},
):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
@@ -553,7 +553,9 @@ class SubcontractingController(StockController):
data = []
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 = {
"item_code": "rm_item_code",

View File

@@ -328,7 +328,7 @@ class TestItemWiseInventoryAccount(IntegrationTestCase):
"voucher_no": pr.name,
"item_code": ("in", items),
},
fields=["sum(stock_value_difference) as value"],
fields=[{"SUM": "stock_value_difference", "as": "value"}],
)
gl_value = frappe.db.get_value(
@@ -435,7 +435,7 @@ class TestItemWiseInventoryAccount(IntegrationTestCase):
sle_value = frappe.get_all(
"Stock Ledger Entry",
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 = (

View File

@@ -239,6 +239,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
crm_activities.refresh();
}
};
if (this.frm) {
extend_cscript(this.frm.cscript, new erpnext.LeadController({ frm: this.frm }));
if (cur_frm) {
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
}

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
"label": "Home",
"link_to": "Home",
"link_type": "Workspace",
"modified": "2025-11-18 12:06:56.506311",
"modified": "2025-11-20 16:09:28.269913",
"modified_by": "Administrator",
"name": "Home",
"owner": "Administrator",

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

56509
erpnext/locale/sl.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -10,6 +10,8 @@ import frappe
from frappe import _, bold
from frappe.core.doctype.version.version import get_diff
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.website.website_generator import WebsiteGenerator
@@ -1191,7 +1193,6 @@ def get_valuation_rate(data):
2) If no value, get last valuation rate from SLE
3) If no value, get valuation rate from Item
"""
from frappe.query_builder.functions import Count, IfNull, Sum
from pypika import Case
item_code, company = data.get("item_code"), data.get("company")
@@ -1482,7 +1483,10 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
non_stock_items = frappe.get_all(
"Item",
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,
)
@@ -1504,7 +1508,7 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
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:
return False
@@ -1528,11 +1532,11 @@ def add_operating_cost_component_wise(
get_component_account(wc.operating_component, stock_entry.company) or op_expense_account
)
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"),
)
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:
stock_entry.append(
@@ -1543,6 +1547,7 @@ def add_operating_cost_component_wise(
wc.operating_component, row.operation
),
"amount": per_unit_cost * flt(stock_entry.fg_completed_qty),
"has_operating_cost": 1,
},
)
@@ -1559,13 +1564,20 @@ def get_component_account(parent, company):
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)
if operating_cost_per_unit:
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:
@@ -1575,6 +1587,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
"expense_account": expense_account,
"description": _("Operating Cost as per Work Order / BOM"),
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
"has_operating_cost": 1,
},
)
@@ -1592,8 +1605,6 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
)
def get_max_operation_quantity():
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Job Card")
query = (
frappe.qb.from_(table)
@@ -1608,8 +1619,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)
def get_utilised_corrective_cost():
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Stock Entry")
subquery = (
frappe.qb.from_(table)
@@ -1719,7 +1728,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not searchfields:
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 = {}
if txt:
@@ -1728,8 +1740,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
barcodes = frappe.get_all(
"Item Barcode",
fields=["distinct parent as item_code"],
fields=["parent as item_code"],
filters={"barcode": ("like", f"%{txt}%")},
distinct=True,
)
barcodes = [d.item_code for d in barcodes]
@@ -1739,11 +1752,11 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if filters and filters.get("item_code"):
has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants")
if not has_variants:
query_filters["has_variants"] = 0
query_filters.append(["has_variants", "=", 0])
if filters:
for fieldname, value in filters.items():
query_filters[fieldname] = value
query_filters.append([fieldname, "=", value])
return frappe.get_list(
"Item",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ def get_data(filters):
job_card_time_details = {}
for job_card_data in frappe.get_all(
"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,
group_by="parent",
):

View File

@@ -230,7 +230,12 @@ class ProductionPlanReport:
purchased_items = frappe.get_all(
"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={
"item_code": ("in", self.item_codes),
"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_valuation_method_on_companies
erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table
erpnext.patches.v16_0.migrate_budget_records_to_new_structure

View File

@@ -10,7 +10,7 @@ def execute():
frappe.reload_doc("stock", "doctype", "item")
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.quality_inspection_template_name = "QIT/%s" % data.parent

View File

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

View File

@@ -258,10 +258,10 @@ class ItemTax:
item_tax_rates = frappe.parse_json(item.item_tax_rate or {})
if tax_row.account_head in item_tax_rates:
if item_tax_rates and tax_row.account_head in item_tax_rates:
return item_tax_rates[tax_row.account_head]
return tax_row.rate
return flt(tax_row.rate)
def get_item_tax_doc(item, tax, rate, tax_value, idx, precision=2):

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)
fields = "distinct *"
or_filters = []
if txt:
@@ -424,13 +422,14 @@ def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, o
return frappe.get_list(
doctype,
fields=fields,
fields="*",
filters=filters,
or_filters=or_filters,
limit_start=limit_start,
limit_page_length=limit_page_length,
order_by=order_by,
ignore_permissions=ignore_permissions,
distinct=True,
)

View File

@@ -409,7 +409,9 @@ def get_children(doctype, parent, task=None, project=None, is_root=False):
# via expand child
filters.append(["parent_task", "=", parent])
else:
filters.append(['ifnull(`parent_task`, "")', "=", ""])
from frappe.query_builder import Field, functions
filters.append(functions.IfNull(Field("parent_task"), "") == "")
if project:
filters.append(["project", "=", project])

View File

@@ -119,6 +119,10 @@ erpnext.sales_common = {
company() {
super.company();
this.set_default_company_address();
if (!this.is_onload) {
// we don't want to override the mapped contact from prevdoc
this.set_default_company_contact_person();
}
}
set_default_company_address() {
@@ -143,6 +147,24 @@ erpnext.sales_common = {
}
}
set_default_company_contact_person() {
if (!frappe.meta.has_field(this.frm.doc.doctype, "company_contact_person")) {
return;
}
if (this.frm.doc.company) {
frappe.db
.get_value("Company", this.frm.doc.company, "default_sales_contact")
.then((r) => {
if (r.message?.default_sales_contact) {
this.frm.set_value("company_contact_person", r.message.default_sales_contact);
} else {
this.frm.set_value("company_contact_person", "");
}
});
}
}
customer() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function () {

View File

@@ -179,7 +179,11 @@ def get_reverse_charge_total(filters):
try:
return (
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]
or 0
)
@@ -219,7 +223,11 @@ def get_reverse_charge_recoverable_total(filters):
try:
return (
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]
or 0
)
@@ -274,7 +282,11 @@ def get_standard_rated_expenses_total(filters):
try:
return (
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]
or 0
)
@@ -292,7 +304,7 @@ def get_standard_rated_expenses_tax(filters):
frappe.db.get_all(
"Purchase Invoice",
filters=query_filters,
fields=["sum(recoverable_standard_rated_expenses)"],
fields=[{"SUM": "recoverable_standard_rated_expenses"}],
as_list=True,
limit=1,
)[0][0]
@@ -310,7 +322,7 @@ def get_tourist_tax_return_total(filters):
try:
return (
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]
or 0
)
@@ -328,7 +340,7 @@ def get_tourist_tax_return_tax(filters):
frappe.db.get_all(
"Sales Invoice",
filters=query_filters,
fields=["sum(tourist_tax_return)"],
fields=[{"SUM": "tourist_tax_return"}],
as_list=True,
limit=1,
)[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.naming import set_name_by_naming_series, set_name_from_naming_options
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.user import get_users_with_role
@@ -234,7 +235,7 @@ class Customer(TransactionBase):
self.update_lead_status()
if self.flags.is_new_doc:
self.link_lead_address_and_contact()
self.link_address_and_contact()
self.copy_communication()
self.update_customer_groups()
@@ -278,15 +279,23 @@ class Customer(TransactionBase):
if self.lead_name:
frappe.db.set_value("Lead", self.lead_name, "status", "Converted")
def link_lead_address_and_contact(self):
if self.lead_name:
# assign lead address and contact to customer (if already not set)
def link_address_and_contact(self):
linked_documents = {
"Lead": self.lead_name,
"Opportunity": self.opportunity_name,
"Prospect": self.prospect_name,
}
for doctype, docname in linked_documents.items():
# assign lead, opportunity and prospect address and contact to customer (if already not set)
if not docname:
continue
linked_contacts_and_addresses = frappe.get_all(
"Dynamic Link",
filters=[
["parenttype", "in", ["Contact", "Address"]],
["link_doctype", "=", "Lead"],
["link_name", "=", self.lead_name],
["link_doctype", "=", doctype],
["link_name", "=", docname],
],
fields=["parent as name", "parenttype as doctype"],
)
@@ -503,11 +512,11 @@ def get_loyalty_programs(doc):
loyalty_programs = frappe.get_all(
"Loyalty Program",
fields=["name", "customer_group", "customer_territory"],
filters={
"auto_opt_in": 1,
"from_date": ["<=", today()],
"ifnull(to_date, '2500-01-01')": [">=", today()],
},
filters=[
["auto_opt_in", "=", 1],
["from_date", "<=", today()],
[functions.IfNull(Field("to_date"), "2500-01-01"), ">=", today()],
],
)
for loyalty_program in loyalty_programs:

View File

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

View File

@@ -992,7 +992,11 @@ def get_requested_item_qty(sales_order):
for d in frappe.db.get_all(
"Material Request Item",
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",
):
result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty})

View File

@@ -55,7 +55,7 @@ def search_by_term(search_term, warehouse, price_list):
}
)
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
item_stock_qty, is_stock_item, is_negative_stock_allowed = get_stock_availability(item_code, warehouse)
item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
item.update({"actual_qty": item_stock_qty})
@@ -198,7 +198,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
current_date = frappe.utils.today()
for item in items_data:
item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
item.actual_qty, _, is_negative_stock_allowed = get_stock_availability(item.item_code, warehouse)
item_prices = frappe.get_all(
"Item Price",

View File

@@ -831,12 +831,16 @@ erpnext.PointOfSale.Controller = class {
const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message;
const available_qty = resp[0];
const is_stock_item = resp[1];
const is_negative_stock_allowed = resp[2];
frappe.dom.unfreeze();
const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold();
if (is_negative_stock_allowed) return;
if (!(available_qty > 0)) {
if (is_stock_item) {
frappe.model.clear_doc(item_row.doctype, item_row.name);

View File

@@ -95,7 +95,7 @@ def get_data(filters=None):
items = get_selling_items(filters)
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}
price_list_map = fetch_item_prices(

View File

@@ -40,6 +40,13 @@ frappe.ui.form.on("Company", {
return { filters: { selling: 1 } };
});
frm.set_query("default_sales_contact", function (doc) {
return {
query: "frappe.contacts.doctype.contact.contact.contact_query",
filters: { link_doctype: "Company", link_name: doc.name },
};
});
frm.set_query("default_buying_terms", function () {
return { filters: { buying: 1 } };
});

View File

@@ -103,6 +103,7 @@
"total_monthly_sales",
"column_break_goals",
"default_selling_terms",
"default_sales_contact",
"default_warehouse_for_sales_return",
"credit_limit",
"transactions_annual_history",
@@ -923,6 +924,12 @@
{
"fieldname": "column_break_9prc",
"fieldtype": "Column Break"
},
{
"fieldname": "default_sales_contact",
"fieldtype": "Link",
"label": "Default Sales Contact",
"options": "Contact"
}
],
"icon": "fa fa-building",
@@ -930,7 +937,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-11-16 16:50:27.624096",
"modified": "2025-11-16 16:51:27.624096",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -70,6 +70,7 @@ class Company(NestedSet):
default_payable_account: DF.Link | None
default_provisional_account: DF.Link | None
default_receivable_account: DF.Link | None
default_sales_contact: DF.Link | None
default_scrap_warehouse: DF.Link | None
default_selling_terms: DF.Link | None
default_warehouse_for_sales_return: DF.Link | None

View File

@@ -799,7 +799,7 @@ class EmailDigest(Document):
"status": ["not in", ("Cancelled")],
"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):

Some files were not shown because too many files have changed in this diff Show More