mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-04 22:18:27 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into support-53364
This commit is contained in:
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -669,7 +669,7 @@ class PaymentReconciliation(Document):
|
||||
"party": self.party,
|
||||
},
|
||||
fields=[
|
||||
"parent as `name`",
|
||||
"parent as name",
|
||||
"exchange_rate",
|
||||
],
|
||||
as_list=1,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -563,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,
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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")]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-11-16 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-11-18 22:14\n"
|
||||
"PO-Revision-Date: 2025-11-19 22:23\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Bosnian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -2105,7 +2105,7 @@ msgstr "Računi za Spajanje"
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264
|
||||
msgid "Accrued Expenses"
|
||||
msgstr ""
|
||||
msgstr "Nagomilani Troškovi"
|
||||
|
||||
#. Option for the 'Account Type' (Select) field in DocType 'Account'
|
||||
#: erpnext/accounts/doctype/account/account.json
|
||||
@@ -25420,7 +25420,7 @@ msgstr "Kontakt Podizvođača"
|
||||
#. Order'
|
||||
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
|
||||
msgid "Job Worker Currency"
|
||||
msgstr ""
|
||||
msgstr "Valuta Podizvođača"
|
||||
|
||||
#. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting
|
||||
#. Receipt'
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-11-16 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-11-18 22:14\n"
|
||||
"PO-Revision-Date: 2025-11-19 22:23\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Persian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -1178,13 +1178,13 @@ msgstr "تراز حساب"
|
||||
#: erpnext/accounts/doctype/account/account.json
|
||||
#: erpnext/accounts/doctype/account_category/account_category.json
|
||||
msgid "Account Category"
|
||||
msgstr ""
|
||||
msgstr "دسته بندی حساب"
|
||||
|
||||
#. Label of the account_category_name (Data) field in DocType 'Account
|
||||
#. Category'
|
||||
#: erpnext/accounts/doctype/account_category/account_category.json
|
||||
msgid "Account Category Name"
|
||||
msgstr ""
|
||||
msgstr "نام دسته حساب"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json
|
||||
@@ -2024,7 +2024,7 @@ msgstr "حسابها برای ادغام"
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264
|
||||
msgid "Accrued Expenses"
|
||||
msgstr ""
|
||||
msgstr "مخارج انباشته"
|
||||
|
||||
#. Option for the 'Account Type' (Select) field in DocType 'Account'
|
||||
#: erpnext/accounts/doctype/account/account.json
|
||||
@@ -3189,7 +3189,7 @@ msgstr "پیشپرداختهای تخصیص یافته در برابر س
|
||||
#. Row'
|
||||
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
|
||||
msgid "Advanced Filtering"
|
||||
msgstr ""
|
||||
msgstr "فیلتر پیشرفته"
|
||||
|
||||
#. Label of the advances (Table) field in DocType 'POS Invoice'
|
||||
#. Label of the advances (Table) field in DocType 'Purchase Invoice'
|
||||
@@ -5485,16 +5485,16 @@ msgstr "تنظیمات دارایی"
|
||||
#. Name of a DocType
|
||||
#: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json
|
||||
msgid "Asset Shift Allocation"
|
||||
msgstr "تخصیص تغییر دارایی"
|
||||
msgstr "تخصیص شیفت دارایی"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json
|
||||
msgid "Asset Shift Factor"
|
||||
msgstr "عامل تغییر دارایی"
|
||||
msgstr "ضریب شیفت دارایی"
|
||||
|
||||
#: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py:32
|
||||
msgid "Asset Shift Factor {0} is set as default currently. Please change it first."
|
||||
msgstr "عامل تغییر دارایی {0} در حال حاضر به عنوان پیشفرض تنظیم شده است. لطفا ابتدا آن را تغییر دهید."
|
||||
msgstr "ضریب شیفت دارایی {0} در حال حاضر به عنوان پیشفرض تنظیم شده است. لطفا ابتدا آن را تغییر دهید."
|
||||
|
||||
#. Label of the asset_status (Select) field in DocType 'Serial No'
|
||||
#: erpnext/stock/doctype/serial_no/serial_no.json
|
||||
@@ -5661,7 +5661,7 @@ msgstr "دارایی {assets_link} برای {item_code} ایجاد شد"
|
||||
|
||||
#: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py:223
|
||||
msgid "Asset's depreciation schedule updated after Asset Shift Allocation {0}"
|
||||
msgstr "برنامه استهلاک دارایی پس از تخصیص تغییر دارایی {0} به روز شد"
|
||||
msgstr "زمانبندی استهلاک دارایی پس از تخصیص شیفت دارایی {0} به روز شد"
|
||||
|
||||
#: erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py:81
|
||||
msgid "Asset's value adjusted after cancellation of Asset Value Adjustment {0}"
|
||||
@@ -5752,7 +5752,7 @@ msgstr "حداقل یکی از موارد فروش یا خرید باید انت
|
||||
|
||||
#: erpnext/accounts/doctype/financial_report_template/financial_report_template.js:25
|
||||
msgid "At least one row is required for a financial report template"
|
||||
msgstr ""
|
||||
msgstr "حداقل یک ردیف برای الگوی گزارش مالی لازم است"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:704
|
||||
msgid "At least one warehouse is mandatory"
|
||||
@@ -7912,7 +7912,7 @@ msgstr ""
|
||||
#. Row'
|
||||
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
|
||||
msgid "Bold text for emphasis (totals, major headings)"
|
||||
msgstr ""
|
||||
msgstr "متن پررنگ برای تأکید (مجموع، عناوین اصلی)"
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:282
|
||||
msgid "Book Advance Payments as Liability option is chosen. Paid From account changed from {0} to {1}."
|
||||
@@ -7963,7 +7963,7 @@ msgstr "یک قرار ملاقات رزرو کنید"
|
||||
#: erpnext/stock/doctype/shipment/shipment.json
|
||||
#: erpnext/stock/doctype/shipment/shipment_list.js:5
|
||||
msgid "Booked"
|
||||
msgstr "رزرو"
|
||||
msgstr "رزرو شده"
|
||||
|
||||
#. Label of the booked_fixed_asset (Check) field in DocType 'Asset'
|
||||
#: erpnext/assets/doctype/asset/asset.json
|
||||
@@ -8427,7 +8427,7 @@ msgstr "محاسبه قیمت باندل محصول بر اساس نرخ آیت
|
||||
#. DocType 'Financial Report Row'
|
||||
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
|
||||
msgid "Calculate but don't show on final report"
|
||||
msgstr ""
|
||||
msgstr "محاسبه میشود اما در گزارش نهایی نمایش داده نمیشود"
|
||||
|
||||
#. Label of the calculate_depr_using_total_days (Check) field in DocType
|
||||
#. 'Accounts Settings'
|
||||
@@ -8439,7 +8439,7 @@ msgstr "محاسبه استهلاک روزانه با استفاده از کل
|
||||
#. Row'
|
||||
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
|
||||
msgid "Calculated Amount"
|
||||
msgstr ""
|
||||
msgstr "مبلغ محاسبه شده"
|
||||
|
||||
#: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py:53
|
||||
msgid "Calculated Bank Statement balance"
|
||||
@@ -10967,7 +10967,7 @@ msgstr ""
|
||||
#: erpnext/stock/doctype/serial_no/serial_no.json
|
||||
#: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:60
|
||||
msgid "Consumed"
|
||||
msgstr "مصرف شده است"
|
||||
msgstr "مصرف شده"
|
||||
|
||||
#: erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py:62
|
||||
msgid "Consumed Amount"
|
||||
@@ -12249,12 +12249,14 @@ msgstr "ایجاد <b><a href='/app/{0}'>{1}(ها)</a></b> با موفقیت"
|
||||
#: erpnext/utilities/bulk_transaction.py:206
|
||||
msgid "Creation of {0} failed.\n"
|
||||
"\t\t\t\tCheck <b><a href=\"/app/bulk-transaction-log\">Bulk Transaction Log</a></b>"
|
||||
msgstr ""
|
||||
msgstr "ایجاد {0} ناموفق بود.\n"
|
||||
"\t\t\t\tبررسی <b><a href=\"/app/bulk-transaction-log\">لاگ تراکنشهای انبوه</a></b>"
|
||||
|
||||
#: erpnext/utilities/bulk_transaction.py:197
|
||||
msgid "Creation of {0} partially successful.\n"
|
||||
"\t\t\t\tCheck <b><a href=\"/app/bulk-transaction-log\">Bulk Transaction Log</a></b>"
|
||||
msgstr ""
|
||||
msgstr "ایجاد {0} تا حدودی موفقیتآمیز بود.\n"
|
||||
"\t\t\t\tبررسی <b><a href=\"/app/bulk-transaction-log\">لاگ تراکنشهای انبوه</a></b>"
|
||||
|
||||
#. Option for the 'Balance must be' (Select) field in DocType 'Account'
|
||||
#. Label of the credit_in_account_currency (Currency) field in DocType 'Journal
|
||||
@@ -12815,7 +12817,7 @@ msgstr "حضانت"
|
||||
#. Row'
|
||||
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
|
||||
msgid "Custom API"
|
||||
msgstr ""
|
||||
msgstr "API سفارشی"
|
||||
|
||||
#. Option for the 'Report Type' (Select) field in DocType 'Financial Report
|
||||
#. Template'
|
||||
@@ -12823,7 +12825,7 @@ msgstr ""
|
||||
#: erpnext/accounts/doctype/financial_report_template/financial_report_template.json
|
||||
#: erpnext/accounts/report/custom_financial_statement/custom_financial_statement.json
|
||||
msgid "Custom Financial Statement"
|
||||
msgstr ""
|
||||
msgstr "صورتهای مالی سفارشی"
|
||||
|
||||
#. Label of the custom_remarks (Check) field in DocType 'Payment Entry'
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.json
|
||||
@@ -14251,7 +14253,7 @@ msgstr "نوع درخواست مواد پیشفرض"
|
||||
#. 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Default Operating Cost Account"
|
||||
msgstr ""
|
||||
msgstr "حساب هزینه عملیاتی پیشفرض"
|
||||
|
||||
#. Label of the default_payable_account (Link) field in DocType 'Company'
|
||||
#. Label of the default_payable_account (Section Break) field in DocType
|
||||
@@ -15517,7 +15519,7 @@ msgstr "پرداخت وام"
|
||||
#: erpnext/accounts/doctype/invoice_discounting/invoice_discounting.json
|
||||
#: erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js:9
|
||||
msgid "Disbursed"
|
||||
msgstr "پرداخت شد"
|
||||
msgstr "پرداخت شده"
|
||||
|
||||
#. Option for the 'Action on New Invoice' (Select) field in DocType 'POS
|
||||
#. Profile'
|
||||
@@ -18280,11 +18282,11 @@ msgstr ""
|
||||
|
||||
#: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:242
|
||||
msgid "Financial Report Template {0} is disabled"
|
||||
msgstr ""
|
||||
msgstr "الگوی گزارش مالی {0} غیرفعال است"
|
||||
|
||||
#: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:239
|
||||
msgid "Financial Report Template {0} not found"
|
||||
msgstr ""
|
||||
msgstr "الگوی گزارش مالی {0} یافت نشد"
|
||||
|
||||
#. Name of a Workspace
|
||||
#: erpnext/accounts/workspace/financial_reports/financial_reports.json
|
||||
@@ -19013,7 +19015,7 @@ msgstr "آدرس انجمن"
|
||||
|
||||
#: erpnext/setup/install.py:200
|
||||
msgid "Frappe School"
|
||||
msgstr ""
|
||||
msgstr "مدرسه Frappe"
|
||||
|
||||
#. Title of an incoterm
|
||||
#: erpnext/setup/doctype/incoterm/incoterms.csv:4
|
||||
@@ -20780,7 +20782,7 @@ msgstr "ساعت"
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.json
|
||||
#: erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
|
||||
msgid "Hour Rate"
|
||||
msgstr ""
|
||||
msgstr "نرخ ساعتی"
|
||||
|
||||
#. Label of the hours (Float) field in DocType 'Workstation Working Hour'
|
||||
#: erpnext/manufacturing/doctype/workstation_working_hour/workstation_working_hour.json
|
||||
@@ -21543,7 +21545,7 @@ msgstr "در درصد"
|
||||
#: erpnext/manufacturing/doctype/work_order/work_order.json
|
||||
#: erpnext/stock/doctype/quality_inspection/quality_inspection.json
|
||||
msgid "In Process"
|
||||
msgstr ""
|
||||
msgstr "در حال انجام"
|
||||
|
||||
#: erpnext/stock/report/item_variant_details/item_variant_details.py:107
|
||||
msgid "In Production"
|
||||
@@ -22595,7 +22597,7 @@ msgstr "تخفیف نامعتبر"
|
||||
|
||||
#: erpnext/controllers/taxes_and_totals.py:738
|
||||
msgid "Invalid Discount Amount"
|
||||
msgstr ""
|
||||
msgstr "مبلغ تخفیف نامعتبر است"
|
||||
|
||||
#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:122
|
||||
msgid "Invalid Document"
|
||||
@@ -23643,13 +23645,13 @@ msgstr ""
|
||||
#. Label of the italic_text (Check) field in DocType 'Financial Report Row'
|
||||
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
|
||||
msgid "Italic Text"
|
||||
msgstr ""
|
||||
msgstr "متن ایتالیک"
|
||||
|
||||
#. Description of the 'Italic Text' (Check) field in DocType 'Financial Report
|
||||
#. Row'
|
||||
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
|
||||
msgid "Italic text for subtotals or notes"
|
||||
msgstr ""
|
||||
msgstr "متن ایتالیک برای جمعهای جزئی یا یادداشتها"
|
||||
|
||||
#. Label of the item_code (Link) field in DocType 'POS Invoice Item'
|
||||
#. Label of the item_code (Link) field in DocType 'Purchase Invoice Item'
|
||||
@@ -26598,7 +26600,7 @@ msgstr "نگهداری موجودی"
|
||||
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
|
||||
#: erpnext/support/workspace/support/support.json
|
||||
msgid "Maintenance"
|
||||
msgstr "نگهداری"
|
||||
msgstr "تعمیر و نگهداری"
|
||||
|
||||
#. Label of the mntc_date (Date) field in DocType 'Maintenance Visit'
|
||||
#: erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json
|
||||
@@ -27331,7 +27333,7 @@ msgstr "هزینه های بازاریابی"
|
||||
|
||||
#: erpnext/setup/setup_wizard/data/designation.txt:23
|
||||
msgid "Marketing Specialist"
|
||||
msgstr "کارشناس بازاریابی"
|
||||
msgstr "متخصص بازاریابی"
|
||||
|
||||
#. Option for the 'Marital Status' (Select) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
@@ -28337,7 +28339,7 @@ msgstr "نحوه پرداختها"
|
||||
#. Label of the model (Data) field in DocType 'Vehicle'
|
||||
#: erpnext/setup/doctype/vehicle/vehicle.json
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
msgstr "مدل"
|
||||
|
||||
#. Label of the section_break_11 (Section Break) field in DocType 'POS Closing
|
||||
#. Entry'
|
||||
@@ -31455,7 +31457,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:667
|
||||
msgid "POS Invoices will be consolidated in a background process"
|
||||
msgstr "فاکتورهای POS در یک فرآیند پس زمینه تلفیق میشوند"
|
||||
msgstr "فاکتورهای POS در یک فرآیند پسزمینه تلفیق میشوند"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:669
|
||||
msgid "POS Invoices will be unconsolidated in a background process"
|
||||
@@ -34330,7 +34332,7 @@ msgstr "لطفا اول ذخیره کنید"
|
||||
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.js:859
|
||||
msgid "Please save the Sales Order before adding a delivery schedule."
|
||||
msgstr ""
|
||||
msgstr "لطفا قبل از اضافه کردن زمانبندی تحویل، سفارش فروش را ذخیره کنید."
|
||||
|
||||
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:79
|
||||
msgid "Please select <b>Template Type</b> to download template"
|
||||
@@ -40023,11 +40025,11 @@ msgstr "وضعیت بازنشر"
|
||||
|
||||
#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py:146
|
||||
msgid "Repost has started in the background"
|
||||
msgstr "ارسال مجدد در پس زمینه شروع شده است"
|
||||
msgstr "ارسال مجدد در پسزمینه شروع شده است"
|
||||
|
||||
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js:40
|
||||
msgid "Repost in background"
|
||||
msgstr "بازنشر در پس زمینه"
|
||||
msgstr "بازنشر در پسزمینه"
|
||||
|
||||
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py:118
|
||||
msgid "Repost started in the background"
|
||||
@@ -40064,7 +40066,7 @@ msgstr "ارسال مجدد در پسزمینه آغاز شده است."
|
||||
|
||||
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js:49
|
||||
msgid "Reposting in the background."
|
||||
msgstr "بازنشر در پس زمینه"
|
||||
msgstr "بازنشر در پسزمینه"
|
||||
|
||||
#. Label of the represents_company (Link) field in DocType 'Purchase Invoice'
|
||||
#. Label of the represents_company (Link) field in DocType 'Sales Invoice'
|
||||
@@ -41045,7 +41047,7 @@ msgstr ""
|
||||
#: erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
|
||||
#: erpnext/quality_management/doctype/quality_review/quality_review.json
|
||||
msgid "Reviews"
|
||||
msgstr ""
|
||||
msgstr "بررسی ها"
|
||||
|
||||
#. Label of the rgt (Int) field in DocType 'Account'
|
||||
#. Label of the rgt (Int) field in DocType 'Company'
|
||||
@@ -41809,7 +41811,10 @@ msgid "Row #{0}: Selling rate for item {1} is lower than its {2}.\n"
|
||||
"\t\t\t\t\tSelling {3} should be atleast {4}.<br><br>Alternatively,\n"
|
||||
"\t\t\t\t\tyou can disable selling price validation in {5} to bypass\n"
|
||||
"\t\t\t\t\tthis validation."
|
||||
msgstr ""
|
||||
msgstr "ردیف #{0}: نرخ فروش برای کالای {1} کمتر از {2} آن است.\n"
|
||||
"\t\t\t\t\tفروش {3} باید حداقل {4} باشد.<br><br>همچنین،\n"
|
||||
"\t\t\t\t\tمیتوانید اعتبارسنجی قیمت فروش را در {5} غیرفعال کنید تا\n"
|
||||
"\t\t\t\t\tاین اعتبارسنجی را دور بزنید."
|
||||
|
||||
#: erpnext/manufacturing/doctype/work_order/work_order.py:262
|
||||
msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}."
|
||||
@@ -45413,7 +45418,7 @@ msgstr ""
|
||||
#: erpnext/assets/doctype/asset/asset.js:300
|
||||
#: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
|
||||
msgid "Shift"
|
||||
msgstr "تغییر مکان"
|
||||
msgstr "شیفت"
|
||||
|
||||
#. Label of the shift_factor (Float) field in DocType 'Asset Shift Factor'
|
||||
#: erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json
|
||||
@@ -47777,7 +47782,7 @@ msgstr "فاکتور اشتراک"
|
||||
#. Label of a Card Break in the Accounting Workspace
|
||||
#: erpnext/accounts/workspace/accounting/accounting.json
|
||||
msgid "Subscription Management"
|
||||
msgstr ""
|
||||
msgstr "مدیریت اشتراک"
|
||||
|
||||
#. Label of the subscription_period (Section Break) field in DocType
|
||||
#. 'Subscription'
|
||||
@@ -50060,7 +50065,7 @@ msgstr "موجودی برای اقلام و انبارهای زیر رزرو ش
|
||||
|
||||
#: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:37
|
||||
msgid "The sync has started in the background, please check the {0} list for new records."
|
||||
msgstr "همگام سازی در پس زمینه شروع شده است، لطفاً لیست {0} را برای رکوردهای جدید بررسی کنید."
|
||||
msgstr "همگام سازی در پسزمینه شروع شده است، لطفاً لیست {0} را برای رکوردهای جدید بررسی کنید."
|
||||
|
||||
#. Description of the 'Invoice Type Created via POS Screen' (Select) field in
|
||||
#. DocType 'POS Settings'
|
||||
@@ -50075,11 +50080,11 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1007
|
||||
msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage"
|
||||
msgstr "تسک به عنوان یک کار پس زمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پسزمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه میکند و به مرحله پیشنویس باز میگردد."
|
||||
msgstr "تسک به عنوان یک کار پسزمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پسزمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه میکند و به مرحله پیشنویس باز میگردد."
|
||||
|
||||
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:1018
|
||||
msgid "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"
|
||||
msgstr "تسک به عنوان یک کار پس زمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پسزمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه میکند و به مرحله ارسال باز میگردد."
|
||||
msgstr "تسک به عنوان یک کار پسزمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پسزمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه میکند و به مرحله ارسال باز میگردد."
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.py:334
|
||||
msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}"
|
||||
@@ -50449,7 +50454,7 @@ msgstr "این برنامه زمانی ایجاد شد که تعدیل ارزش
|
||||
|
||||
#: erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py:207
|
||||
msgid "This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}."
|
||||
msgstr "این برنامه زمانی ایجاد شد که تغییرات دارایی {0} از طریق تخصیص تغییر دارایی {1} تنظیم شد."
|
||||
msgstr "این زمانبندی زمانی ایجاد شد که شیفتهای دارایی {0} از طریق تخصیص شیفت دارایی {1} تنظیم شدند."
|
||||
|
||||
#. Description of the 'Dunning Letter' (Section Break) field in DocType
|
||||
#. 'Dunning Type'
|
||||
@@ -51357,7 +51362,7 @@ msgstr "کل زمان نگهداری"
|
||||
#. Label of the total_holidays (Int) field in DocType 'Holiday List'
|
||||
#: erpnext/setup/doctype/holiday_list/holiday_list.json
|
||||
msgid "Total Holidays"
|
||||
msgstr ""
|
||||
msgstr "کل تعطیلات"
|
||||
|
||||
#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:121
|
||||
msgid "Total Income"
|
||||
@@ -51698,7 +51703,7 @@ msgstr "کل مالیات"
|
||||
#: erpnext/stock/doctype/delivery_note/delivery_note.json
|
||||
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
|
||||
msgid "Total Taxes and Charges"
|
||||
msgstr ""
|
||||
msgstr "کل مالیاتها و عوارض"
|
||||
|
||||
#. Label of the base_total_taxes_and_charges (Currency) field in DocType
|
||||
#. 'Payment Entry'
|
||||
@@ -53532,7 +53537,7 @@ msgstr "اعتبار موجودی منفی"
|
||||
#. 'Pricing Rule'
|
||||
#: erpnext/accounts/doctype/pricing_rule/pricing_rule.json
|
||||
msgid "Validate Pricing Rule"
|
||||
msgstr ""
|
||||
msgstr "اعتبارسنجی قانون قیمتگذاری"
|
||||
|
||||
#. Label of the validate_selling_price (Check) field in DocType 'Selling
|
||||
#. Settings'
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-11-16 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-11-18 22:14\n"
|
||||
"PO-Revision-Date: 2025-11-19 22:23\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Croatian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -2105,7 +2105,7 @@ msgstr "Računi za Spajanje"
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264
|
||||
msgid "Accrued Expenses"
|
||||
msgstr "Obračunati Troškovi"
|
||||
msgstr "Nagomilani Troškovi"
|
||||
|
||||
#. Option for the 'Account Type' (Select) field in DocType 'Account'
|
||||
#: erpnext/accounts/doctype/account/account.json
|
||||
@@ -25420,7 +25420,7 @@ msgstr "Kontakt Podizvođača"
|
||||
#. Order'
|
||||
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
|
||||
msgid "Job Worker Currency"
|
||||
msgstr ""
|
||||
msgstr "Valuta Podizvođača"
|
||||
|
||||
#. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting
|
||||
#. Receipt'
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-11-16 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-11-18 22:13\n"
|
||||
"PO-Revision-Date: 2025-11-19 22:23\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Swedish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -1988,7 +1988,7 @@ msgstr "Bokföring"
|
||||
#. Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Accounts Closing"
|
||||
msgstr "Bokföring Låsning"
|
||||
msgstr "Bokföring Stängning"
|
||||
|
||||
#. Label of the acc_frozen_upto (Date) field in DocType 'Accounts Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
@@ -2111,7 +2111,7 @@ msgstr "Konton att slå ihop"
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:158
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:264
|
||||
msgid "Accrued Expenses"
|
||||
msgstr "Upplupna Kostnader"
|
||||
msgstr "Ackumulerade Kostnader"
|
||||
|
||||
#. Option for the 'Account Type' (Select) field in DocType 'Account'
|
||||
#: erpnext/accounts/doctype/account/account.json
|
||||
@@ -16349,7 +16349,7 @@ msgstr "Förfallodatum kan inte vara före {0}"
|
||||
|
||||
#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:129
|
||||
msgid "Due to stock closing entry {0}, you cannot repost item valuation before {1}"
|
||||
msgstr "På grund av lagerlåsning post {0} kan du inte lägga om artikel varuvärdering innan {1}"
|
||||
msgstr "På grund av lagerstängning post {0} kan du inte lägga om artikel varuvärdering innan {1}"
|
||||
|
||||
#. Name of a DocType
|
||||
#. Label of a Card Break in the Receivables Workspace
|
||||
@@ -16443,7 +16443,7 @@ msgstr "Dubbletter av Försäljning Fakturor hittades"
|
||||
|
||||
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:78
|
||||
msgid "Duplicate Stock Closing Entry"
|
||||
msgstr "Kopiera Lagerlåsning Post"
|
||||
msgstr "Duplicera Lagestängning Post"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:169
|
||||
msgid "Duplicate customer group found in the customer group table"
|
||||
@@ -18405,7 +18405,7 @@ msgstr "Bokföringsår Start Datum"
|
||||
#. 'Accounts Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) "
|
||||
msgstr "Finansiella Rapporter kommer att genereras med hjälp av Bokföring Poster (ska vara aktiverat om Period Låsning Verifikat inte publiceras för alla år i följd eller saknas)"
|
||||
msgstr "Finansiella Rapporter kommer att genereras med hjälp av Bokföring Poster (ska vara aktiverat om Period Stängning Verifikat inte publiceras för alla år i följd eller saknas) "
|
||||
|
||||
#: erpnext/manufacturing/doctype/work_order/work_order.js:843
|
||||
#: erpnext/manufacturing/doctype/work_order/work_order.js:858
|
||||
@@ -19797,7 +19797,7 @@ msgstr "Skapa Schema"
|
||||
|
||||
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.js:12
|
||||
msgid "Generate Stock Closing Entry"
|
||||
msgstr "Skapa Lagerlåsning Post"
|
||||
msgstr "Skapa Lagerstängning Post"
|
||||
|
||||
#. Description of a DocType
|
||||
#: erpnext/stock/doctype/packing_slip/packing_slip.json
|
||||
@@ -25425,7 +25425,7 @@ msgstr "Jobb Ansvarig Kontakt"
|
||||
#. Order'
|
||||
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
|
||||
msgid "Job Worker Currency"
|
||||
msgstr "Jobb Arbetare Valuta"
|
||||
msgstr "Jobb Ansvarig Valuta"
|
||||
|
||||
#. Label of the supplier_delivery_note (Data) field in DocType 'Subcontracting
|
||||
#. Receipt'
|
||||
@@ -33432,13 +33432,13 @@ msgstr "Period Stängd"
|
||||
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js:69
|
||||
#: erpnext/accounts/report/trial_balance/trial_balance.js:89
|
||||
msgid "Period Closing Entry For Current Period"
|
||||
msgstr "Period Låsning Post för Aktuell Period"
|
||||
msgstr "Period Stängning Post för Aktuell Period"
|
||||
|
||||
#. Label of the period_closing_settings_section (Section Break) field in
|
||||
#. DocType 'Accounts Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Period Closing Settings"
|
||||
msgstr "Period Låsning Inställningar"
|
||||
msgstr "Period Stängning Inställningar"
|
||||
|
||||
#. Label of the period_closing_voucher (Link) field in DocType 'Account Closing
|
||||
#. Balance'
|
||||
@@ -33448,7 +33448,7 @@ msgstr "Period Låsning Inställningar"
|
||||
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json
|
||||
#: erpnext/accounts/workspace/accounting/accounting.json
|
||||
msgid "Period Closing Voucher"
|
||||
msgstr "Period Låsning Verifikat"
|
||||
msgstr "Period Stängning Verifikat"
|
||||
|
||||
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:499
|
||||
msgid "Period Closing Voucher {0} GL Entry Cancellation Failed"
|
||||
@@ -39672,7 +39672,7 @@ msgstr "Hälsningar,"
|
||||
|
||||
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.js:27
|
||||
msgid "Regenerate Stock Closing Entry"
|
||||
msgstr "Återskapa Lagerlåsning Post"
|
||||
msgstr "Återskapa Lagerstängning Post"
|
||||
|
||||
#. Label of a Card Break in the Buying Workspace
|
||||
#: erpnext/buying/workspace/buying/buying.json
|
||||
@@ -46734,12 +46734,12 @@ msgstr "Lager Kapacitet"
|
||||
#. Label of the stock_closing_tab (Tab Break) field in DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Stock Closing"
|
||||
msgstr "Lager Låsning"
|
||||
msgstr "Lagerstängning"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json
|
||||
msgid "Stock Closing Balance"
|
||||
msgstr "Lagerlåsning Saldo"
|
||||
msgstr "Lagerstängning Saldo"
|
||||
|
||||
#. Label of the stock_closing_entry (Link) field in DocType 'Stock Closing
|
||||
#. Balance'
|
||||
@@ -46747,19 +46747,19 @@ msgstr "Lagerlåsning Saldo"
|
||||
#: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json
|
||||
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.json
|
||||
msgid "Stock Closing Entry"
|
||||
msgstr "Lagerlåsning Post"
|
||||
msgstr "Lagerstängning Post"
|
||||
|
||||
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:77
|
||||
msgid "Stock Closing Entry {0} already exists for the selected date range"
|
||||
msgstr "Lagerlåsning Post {0} finns redan för vald datumintervall"
|
||||
msgstr "Lagerstängning Post {0} finns redan för vald datumintervall"
|
||||
|
||||
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:98
|
||||
msgid "Stock Closing Entry {0} has been queued for processing, system will take sometime to complete it."
|
||||
msgstr "Lagerlåsning Post {0} är i kö för behandling, och kommer att ta lite tid att slutföra."
|
||||
msgstr "Lagerstängning Post {0} är i kö för behandling, och kommer att ta lite tid att slutföra."
|
||||
|
||||
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry_dashboard.py:9
|
||||
msgid "Stock Closing Log"
|
||||
msgstr "Lagerlåsning Logg"
|
||||
msgstr "Lagerstängning Logg"
|
||||
|
||||
#. Label of the warehouse_and_reference (Section Break) field in DocType 'POS
|
||||
#. Invoice Item'
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1193,7 +1195,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")
|
||||
@@ -1484,7 +1485,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,
|
||||
)
|
||||
|
||||
@@ -1506,7 +1510,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
|
||||
@@ -1530,11 +1534,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(
|
||||
@@ -1545,6 +1549,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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1561,13 +1566,20 @@ def get_component_account(parent, company):
|
||||
|
||||
|
||||
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
|
||||
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:
|
||||
@@ -1577,6 +1589,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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1594,8 +1607,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)
|
||||
@@ -1610,8 +1621,6 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
|
||||
return min([d.qty for d in query.run(as_dict=True)], default=0)
|
||||
|
||||
def get_utilised_corrective_cost():
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
subquery = (
|
||||
frappe.qb.from_(table)
|
||||
@@ -1721,7 +1730,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:
|
||||
@@ -1730,8 +1742,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]
|
||||
@@ -1741,11 +1754,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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
):
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
128
erpnext/patches/v16_0/migrate_budget_records_to_new_structure.py
Normal file
128
erpnext/patches/v16_0/migrate_budget_records_to_new_structure.py
Normal 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()
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,13 +4,17 @@ erpnext.financial_statements = {
|
||||
filters: get_filters(),
|
||||
baseData: null,
|
||||
formatter: function (value, row, column, data, default_formatter, filter) {
|
||||
const report_params = [value, row, column, data, default_formatter, filter];
|
||||
// Growth/Margin
|
||||
if (this._is_special_view(column, data))
|
||||
return this._format_special_view(value, row, column, data, default_formatter);
|
||||
if (erpnext.financial_statements._is_special_view(column, data))
|
||||
return erpnext.financial_statements._format_special_view(...report_params);
|
||||
|
||||
if (frappe.query_report.get_filter_value("report_template"))
|
||||
return this._format_custom_report(value, row, column, data, default_formatter, filter);
|
||||
else return this._format_standard_report(value, row, column, data, default_formatter, filter);
|
||||
return erpnext.financial_statements._format_custom_report(...report_params);
|
||||
|
||||
if (frappe.query_report.get_filter_value("report_template"))
|
||||
return erpnext.financial_statements._format_custom_report(...report_params);
|
||||
else return erpnext.financial_statements._format_standard_report(...report_params);
|
||||
},
|
||||
|
||||
_is_special_view: function (column, data) {
|
||||
@@ -20,11 +24,11 @@ erpnext.financial_statements = {
|
||||
},
|
||||
|
||||
_format_custom_report: function (value, row, column, data, default_formatter, filter) {
|
||||
const columnInfo = this._parse_column_info(column.fieldname, data);
|
||||
const formatting = this._get_formatting_for_column(data, columnInfo);
|
||||
const columnInfo = erpnext.financial_statements._parse_column_info(column.fieldname, data);
|
||||
const formatting = erpnext.financial_statements._get_formatting_for_column(data, columnInfo);
|
||||
|
||||
if (columnInfo.isAccount) {
|
||||
return this._format_custom_account_column(
|
||||
return erpnext.financial_statements._format_custom_account_column(
|
||||
value,
|
||||
data,
|
||||
formatting,
|
||||
@@ -33,7 +37,14 @@ erpnext.financial_statements = {
|
||||
row
|
||||
);
|
||||
} else {
|
||||
return this._format_custom_value_column(value, data, formatting, column, default_formatter, row);
|
||||
return erpnext.financial_statements._format_custom_value_column(
|
||||
value,
|
||||
data,
|
||||
formatting,
|
||||
column,
|
||||
default_formatter,
|
||||
row
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -99,7 +110,7 @@ erpnext.financial_statements = {
|
||||
}
|
||||
|
||||
// Style
|
||||
return this._style_custom_value(formattedValue, formatting, null);
|
||||
return erpnext.financial_statements._style_custom_value(formattedValue, formatting, null);
|
||||
},
|
||||
|
||||
_format_custom_value_column: function (value, data, formatting, column, default_formatter, row) {
|
||||
@@ -111,7 +122,7 @@ erpnext.financial_statements = {
|
||||
if (col.fieldtype === "Float") col.options = null;
|
||||
|
||||
let formattedValue = default_formatter(value, row, col, data);
|
||||
return this._style_custom_value(formattedValue, formatting, value);
|
||||
return erpnext.financial_statements._style_custom_value(formattedValue, formatting, value);
|
||||
},
|
||||
|
||||
_style_custom_value(formattedValue, formatting, value) {
|
||||
@@ -157,7 +168,7 @@ erpnext.financial_statements = {
|
||||
},
|
||||
|
||||
_format_standard_report: function (value, row, column, data, default_formatter, filter) {
|
||||
if (data && column.fieldname == this.name_field) {
|
||||
if (data && column.fieldname == erpnext.financial_statements.name_field) {
|
||||
value = data.section_name || data.account_name || value;
|
||||
|
||||
if (filter && filter?.text && filter?.type == "contains") {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -503,11 +504,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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -63,7 +63,7 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Invoice",
|
||||
fields=["customer as name", "sum(outstanding_amount) as value"],
|
||||
fields=["customer as name", {"SUM": "outstanding_amount", "as": "value"}],
|
||||
filters=filters,
|
||||
group_by="customer",
|
||||
order_by="value desc",
|
||||
@@ -80,7 +80,7 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=["customer as name", f"sum({select_field}) as value"],
|
||||
fields=["customer as name", {"SUM": select_field, "as": "value"}],
|
||||
filters=filters,
|
||||
group_by="customer",
|
||||
order_by="value desc",
|
||||
@@ -91,10 +91,10 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
@frappe.whitelist()
|
||||
def get_all_items(date_range, company, field, limit=None):
|
||||
if field in ("available_stock_qty", "available_stock_value"):
|
||||
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
|
||||
sum_field = "actual_qty" if field == "available_stock_qty" else "stock_value"
|
||||
results = frappe.db.get_all(
|
||||
"Bin",
|
||||
fields=["item_code as name", f"{select_field} as value"],
|
||||
fields=["item_code as name", {"SUM": sum_field, "as": "value"}],
|
||||
group_by="item_code",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
@@ -125,7 +125,7 @@ def get_all_items(date_range, company, field, limit=None):
|
||||
select_doctype,
|
||||
fields=[
|
||||
f"`tab{child_doctype}`.item_code as name",
|
||||
f"sum(`tab{child_doctype}`.{select_field}) as value",
|
||||
{"SUM": f"`tab{child_doctype}`.{select_field}", "as": "value"},
|
||||
],
|
||||
filters=filters,
|
||||
order_by="value desc",
|
||||
@@ -145,7 +145,7 @@ def get_all_suppliers(date_range, company, field, limit=None):
|
||||
|
||||
return frappe.get_list(
|
||||
"Purchase Invoice",
|
||||
fields=["supplier as name", "sum(outstanding_amount) as value"],
|
||||
fields=["supplier as name", {"SUM": "outstanding_amount", "as": "value"}],
|
||||
filters=filters,
|
||||
group_by="supplier",
|
||||
order_by="value desc",
|
||||
@@ -162,7 +162,7 @@ def get_all_suppliers(date_range, company, field, limit=None):
|
||||
|
||||
return frappe.get_list(
|
||||
"Purchase Order",
|
||||
fields=["supplier as name", f"sum({select_field}) as value"],
|
||||
fields=["supplier as name", {"SUM": select_field, "as": "value"}],
|
||||
filters=filters,
|
||||
group_by="supplier",
|
||||
order_by="value desc",
|
||||
@@ -186,7 +186,7 @@ def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"sales_partner as name",
|
||||
f"sum({select_field}) as value",
|
||||
{"SUM": select_field, "as": "value"},
|
||||
],
|
||||
filters=filters,
|
||||
group_by="sales_partner",
|
||||
@@ -210,7 +210,7 @@ def get_all_sales_person(date_range, company, field=None, limit=0):
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`tabSales Team`.sales_person as name",
|
||||
"sum(`tabSales Team`.allocated_amount) as value",
|
||||
{"SUM": "`tabSales Team`.allocated_amount", "as": "value"},
|
||||
],
|
||||
filters=filters,
|
||||
group_by="`tabSales Team`.sales_person",
|
||||
|
||||
@@ -31,7 +31,7 @@ def get(
|
||||
|
||||
warehouses = frappe.get_list(
|
||||
"Bin",
|
||||
fields=["warehouse", "sum(stock_value) stock_value"],
|
||||
fields=["warehouse", {"SUM": "stock_value", "as": "stock_value"}],
|
||||
filters={"warehouse": ["IN", warehouses], "stock_value": [">", 0]},
|
||||
group_by="warehouse",
|
||||
order_by="stock_value DESC",
|
||||
|
||||
@@ -405,8 +405,9 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
|
||||
serial_nos = get_serial_nos(serial_no)
|
||||
batches = frappe.get_all(
|
||||
"Serial No",
|
||||
fields=["distinct batch_no"],
|
||||
fields=["batch_no"],
|
||||
filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)},
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
if not batches:
|
||||
|
||||
@@ -320,12 +320,13 @@ def get_inventory_documents(
|
||||
|
||||
return frappe.get_all(
|
||||
"DocField",
|
||||
fields=["distinct parent"],
|
||||
fields=["parent"],
|
||||
filters=and_filters,
|
||||
or_filters=or_filters,
|
||||
start=start,
|
||||
page_length=page_len,
|
||||
as_list=1,
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -382,7 +383,7 @@ def get_inventory_dimensions():
|
||||
return frappe.get_all(
|
||||
"Inventory Dimension",
|
||||
fields=[
|
||||
"distinct target_fieldname as fieldname",
|
||||
"target_fieldname as fieldname",
|
||||
"source_fieldname",
|
||||
"reference_document as doctype",
|
||||
"validate_negative_stock",
|
||||
@@ -390,6 +391,7 @@ def get_inventory_dimensions():
|
||||
],
|
||||
filters={"disabled": 0},
|
||||
order_by="creation",
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"col_break3",
|
||||
"amount",
|
||||
"base_amount",
|
||||
"has_corrective_cost"
|
||||
"has_corrective_cost",
|
||||
"has_operating_cost"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -70,13 +71,20 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Corrective Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_operating_cost",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Operating Cost",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-09 10:22:20.286641",
|
||||
"modified": "2025-07-16 15:27:59.175530",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Landed Cost Taxes and Charges",
|
||||
|
||||
@@ -21,6 +21,7 @@ class LandedCostTaxesandCharges(Document):
|
||||
exchange_rate: DF.Float
|
||||
expense_account: DF.Link | None
|
||||
has_corrective_cost: DF.Check
|
||||
has_operating_cost: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -346,6 +346,9 @@ frappe.ui.form.on("Material Request", {
|
||||
label: __("For Warehouse"),
|
||||
options: "Warehouse",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
return { filters: { company: frm.doc.company } };
|
||||
},
|
||||
},
|
||||
{ fieldname: "qty", fieldtype: "Float", label: __("Quantity"), reqd: 1, default: 1 },
|
||||
{
|
||||
|
||||
@@ -121,7 +121,12 @@ def get_indexed_packed_items_table(doc):
|
||||
"""
|
||||
indexed_table = {}
|
||||
for packed_item in doc.get("packed_items"):
|
||||
key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname)
|
||||
key = (
|
||||
packed_item.parent_item,
|
||||
packed_item.item_code,
|
||||
packed_item.idx if doc.is_new() else packed_item.parent_detail_docname,
|
||||
)
|
||||
|
||||
indexed_table[key] = packed_item
|
||||
|
||||
return indexed_table
|
||||
@@ -182,7 +187,11 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re
|
||||
exists, pi_row = False, {}
|
||||
|
||||
# check if row already exists in packed items table
|
||||
key = (main_item_row.item_code, packing_item.item_code, main_item_row.name)
|
||||
key = (
|
||||
main_item_row.item_code,
|
||||
packing_item.item_code,
|
||||
main_item_row.idx if doc.is_new() else main_item_row.name,
|
||||
)
|
||||
if packed_items_table.get(key):
|
||||
pi_row, exists = packed_items_table.get(key), True
|
||||
|
||||
|
||||
@@ -593,7 +593,7 @@ class TestPickList(IntegrationTestCase):
|
||||
for dn in frappe.get_all(
|
||||
"Delivery Note",
|
||||
filters={"against_pick_list": pick_list.name, "customer": "_Test Customer"},
|
||||
fields={"name"},
|
||||
fields=["name"],
|
||||
):
|
||||
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
|
||||
self.assertEqual(dn_item.item_code, "_Test Item")
|
||||
@@ -604,7 +604,7 @@ class TestPickList(IntegrationTestCase):
|
||||
for dn in frappe.get_all(
|
||||
"Delivery Note",
|
||||
filters={"against_pick_list": pick_list.name, "customer": "_Test Customer 1"},
|
||||
fields={"name"},
|
||||
fields=["name"],
|
||||
):
|
||||
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
|
||||
self.assertEqual(dn_item.item_code, "_Test Item 2")
|
||||
@@ -637,7 +637,7 @@ class TestPickList(IntegrationTestCase):
|
||||
pick_list_1.submit()
|
||||
create_delivery_note(pick_list_1.name)
|
||||
for dn in frappe.get_all(
|
||||
"Delivery Note", filters={"against_pick_list": pick_list_1.name}, fields={"name"}
|
||||
"Delivery Note", filters={"against_pick_list": pick_list_1.name}, fields=["name"]
|
||||
):
|
||||
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
|
||||
if dn_item.item_code == "_Test Item":
|
||||
|
||||
@@ -1333,7 +1333,7 @@ def get_item_wise_returned_qty(pr_doc):
|
||||
"Purchase Receipt",
|
||||
fields=[
|
||||
"`tabPurchase Receipt Item`.purchase_receipt_item",
|
||||
"sum(abs(`tabPurchase Receipt Item`.qty)) as qty",
|
||||
{"SUM": [{"ABS": "`tabPurchase Receipt Item`.qty"}], "as": "qty"},
|
||||
],
|
||||
filters=[
|
||||
["Purchase Receipt", "docstatus", "=", 1],
|
||||
|
||||
@@ -18,6 +18,9 @@ def get_data():
|
||||
"Purchase Order": ["items", "purchase_order"],
|
||||
"Project": ["items", "project"],
|
||||
},
|
||||
"internal_and_external_links": {
|
||||
"Purchase Invoice": ["items", "purchase_invoice"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Related"),
|
||||
|
||||
@@ -1900,7 +1900,7 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
||||
data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pr_return.name, "docstatus": 1},
|
||||
fields=["SUM(stock_value_difference) as stock_value_difference"],
|
||||
fields=[{"SUM": "stock_value_difference", "as": "stock_value_difference"}],
|
||||
)[0]
|
||||
|
||||
self.assertEqual(abs(data["stock_value_difference"]), 400.00)
|
||||
|
||||
@@ -1393,7 +1393,36 @@ class SerialandBatchBundle(Document):
|
||||
if self.voucher_type == "POS Invoice":
|
||||
return
|
||||
|
||||
if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1:
|
||||
child_doctype = self.voucher_type + " Item"
|
||||
mapper = {
|
||||
"Asset Capitalization": "Asset Capitalization Stock Item",
|
||||
"Asset Repair": "Asset Repair Consumed Item",
|
||||
"Stock Entry": "Stock Entry Detail",
|
||||
}.get(self.voucher_type)
|
||||
|
||||
if mapper:
|
||||
child_doctype = mapper
|
||||
|
||||
if self.voucher_type == "Delivery Note" and not frappe.db.exists(
|
||||
"Delivery Note Item", self.voucher_detail_no
|
||||
):
|
||||
child_doctype = "Packed Item"
|
||||
|
||||
elif self.voucher_type == "Sales Invoice" and not frappe.db.exists(
|
||||
"Sales Invoice Item", self.voucher_detail_no
|
||||
):
|
||||
child_doctype = "Packed Item"
|
||||
|
||||
elif self.voucher_type == "Subcontracting Receipt" and not frappe.db.exists(
|
||||
"Subcontracting Receipt Item", self.voucher_detail_no
|
||||
):
|
||||
child_doctype = "Subcontracting Receipt Supplied Item"
|
||||
|
||||
if (
|
||||
frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1
|
||||
and self.voucher_detail_no
|
||||
and frappe.db.exists(child_doctype, self.voucher_detail_no)
|
||||
):
|
||||
msg = f"""The {self.voucher_type} {bold(self.voucher_no)}
|
||||
is in submitted state, please cancel it first"""
|
||||
frappe.throw(_(msg))
|
||||
|
||||
@@ -976,12 +976,10 @@ frappe.ui.form.on("Stock Entry Detail", {
|
||||
no_batch_serial_number_value = true;
|
||||
}
|
||||
|
||||
if (
|
||||
no_batch_serial_number_value &&
|
||||
!frappe.flags.hide_serial_batch_dialog &&
|
||||
!frappe.flags.dialog_set
|
||||
) {
|
||||
frappe.flags.dialog_set = true;
|
||||
if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) {
|
||||
if (!frappe.flags.dialog_set) {
|
||||
frappe.flags.dialog_set = true;
|
||||
}
|
||||
erpnext.stock.select_batch_and_serial_no(frm, d);
|
||||
} else {
|
||||
frappe.flags.dialog_set = false;
|
||||
|
||||
@@ -2420,7 +2420,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
data = frappe.get_all(
|
||||
"Work Order Operation",
|
||||
filters={"parent": self.work_order},
|
||||
fields=["max(process_loss_qty) as process_loss_qty"],
|
||||
fields=[{"MAX": "process_loss_qty", "as": "process_loss_qty"}],
|
||||
)
|
||||
|
||||
if data and data[0].process_loss_qty is not None:
|
||||
@@ -3145,7 +3145,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
stock_entries_child_list.append(d.ste_detail)
|
||||
transferred_qty = frappe.get_all(
|
||||
"Stock Entry Detail",
|
||||
fields=["sum(qty) as qty"],
|
||||
fields=[{"SUM": "qty", "as": "qty"}],
|
||||
filters={
|
||||
"against_stock_entry": d.against_stock_entry,
|
||||
"ste_detail": d.ste_detail,
|
||||
@@ -3417,6 +3417,26 @@ def get_work_order_details(work_order, company):
|
||||
}
|
||||
|
||||
|
||||
def get_consumed_operating_cost(wo_name, bom_no):
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
child_table = frappe.qb.DocType("Landed Cost Taxes and Charges")
|
||||
query = (
|
||||
frappe.qb.from_(child_table)
|
||||
.join(table)
|
||||
.on(child_table.parent == table.name)
|
||||
.select(Sum(child_table.amount).as_("consumed_cost"))
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.work_order == wo_name)
|
||||
& (table.purpose == "Manufacture")
|
||||
& (table.bom_no == bom_no)
|
||||
& (child_table.has_operating_cost == 1)
|
||||
)
|
||||
)
|
||||
cost = query.run(pluck="consumed_cost")
|
||||
return cost[0] if cost and cost[0] else 0
|
||||
|
||||
|
||||
def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
operating_cost_per_unit = 0
|
||||
if work_order:
|
||||
@@ -3434,7 +3454,9 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
|
||||
for d in work_order.get("operations"):
|
||||
if flt(d.completed_qty):
|
||||
operating_cost_per_unit += flt(d.actual_operating_cost) / flt(d.completed_qty)
|
||||
operating_cost_per_unit += flt(
|
||||
d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no)
|
||||
) / flt(d.completed_qty - work_order.produced_qty)
|
||||
elif work_order.qty:
|
||||
operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from uuid import uuid4
|
||||
import frappe
|
||||
from frappe.core.page.permission_manager.permission_manager import reset
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.query_builder.functions import Timestamp
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, add_to_date, flt, today
|
||||
|
||||
@@ -1281,12 +1282,16 @@ class TestStockLedgerEntry(IntegrationTestCase, StockTestMixin):
|
||||
item=item, from_warehouse=source_warehouse, to_warehouse=target_warehouse, qty=1_728.0
|
||||
)
|
||||
|
||||
filters = {"voucher_no": transfer.name, "voucher_type": transfer.doctype, "is_cancelled": 0}
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["*"],
|
||||
filters=filters,
|
||||
order_by="timestamp(posting_date, posting_time), creation",
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
sles = (
|
||||
frappe.qb.from_(sle)
|
||||
.select("*")
|
||||
.where(sle.voucher_no == transfer.name)
|
||||
.where(sle.voucher_type == transfer.doctype)
|
||||
.where(sle.is_cancelled == 0)
|
||||
.orderby(Timestamp(sle.posting_date, sle.posting_time))
|
||||
.orderby(sle.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
self.assertEqual(abs(sles[0].stock_value_difference), sles[1].stock_value_difference)
|
||||
|
||||
|
||||
@@ -977,6 +977,7 @@ class StockReconciliation(StockController):
|
||||
is_customer_item = frappe.get_cached_value("Item", d.item_code, "is_customer_provided_item")
|
||||
if is_customer_item and d.valuation_rate:
|
||||
d.valuation_rate = 0.0
|
||||
d.allow_zero_valuation_rate = 1
|
||||
changed_any_values = True
|
||||
|
||||
if changed_any_values:
|
||||
|
||||
@@ -196,12 +196,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "allow_zero_valuation_rate",
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "barcode",
|
||||
@@ -268,7 +266,7 @@
|
||||
"grid_page_length": 50,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-28 22:40:30.086415",
|
||||
"modified": "2025-11-20 15:27:13.868179",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reconciliation Item",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Conversion Factor",
|
||||
"non_negative": 1,
|
||||
"oldfieldname": "conversion_factor",
|
||||
"oldfieldtype": "Float"
|
||||
}
|
||||
@@ -31,13 +32,14 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:57.645955",
|
||||
"modified": "2025-11-19 21:27:13.968771",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "UOM Conversion Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import json
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.query_builder import Field
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.caching import request_cache
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
@@ -197,10 +199,12 @@ def get_children(doctype, parent=None, company=None, is_root=False, include_disa
|
||||
include_disabled = json.loads(include_disabled)
|
||||
|
||||
fields = ["name as value", "is_group as expandable"]
|
||||
|
||||
filters = [
|
||||
["ifnull(`parent_warehouse`, '')", "=", parent],
|
||||
[IfNull(Field("parent_warehouse"), ""), "=", parent],
|
||||
["company", "in", (company, None, "")],
|
||||
]
|
||||
|
||||
if frappe.db.has_column(doctype, "disabled") and not include_disabled:
|
||||
filters.append(["disabled", "=", False])
|
||||
|
||||
@@ -236,7 +240,9 @@ def get_child_warehouses(warehouse):
|
||||
|
||||
def get_warehouses_based_on_account(account, company=None):
|
||||
warehouses = []
|
||||
for d in frappe.get_all("Warehouse", fields=["name", "is_group"], filters={"account": account}):
|
||||
for d in frappe.get_all(
|
||||
"Warehouse", fields=["name", "is_group"], filters={"account": account, "disabled": 0}
|
||||
):
|
||||
if d.is_group:
|
||||
warehouses.extend(get_child_warehouses(d.name))
|
||||
else:
|
||||
|
||||
@@ -115,7 +115,7 @@ def get_stock_ledger_entries(report_filters):
|
||||
"posting_time",
|
||||
"company",
|
||||
"warehouse",
|
||||
"(stock_value_difference / actual_qty) as valuation_rate",
|
||||
{"DIV": ["stock_value_difference", "actual_qty"], "as": "valuation_rate"},
|
||||
]
|
||||
|
||||
filters = {"is_cancelled": 0}
|
||||
|
||||
@@ -143,9 +143,9 @@ def get_stock_details_map(variant_list):
|
||||
stock_details = frappe.db.get_all(
|
||||
"Bin",
|
||||
fields=[
|
||||
"sum(planned_qty) as planned_qty",
|
||||
"sum(actual_qty) as actual_qty",
|
||||
"sum(projected_qty) as projected_qty",
|
||||
{"SUM": "planned_qty", "as": "planned_qty"},
|
||||
{"SUM": "actual_qty", "as": "actual_qty"},
|
||||
{"SUM": "projected_qty", "as": "projected_qty"},
|
||||
"item_code",
|
||||
],
|
||||
filters={"item_code": ["in", variant_list]},
|
||||
@@ -167,7 +167,7 @@ def get_buying_price_map(variant_list):
|
||||
buying = frappe.db.get_all(
|
||||
"Item Price",
|
||||
fields=[
|
||||
"avg(price_list_rate) as avg_rate",
|
||||
{"AVG": "price_list_rate", "as": "avg_rate"},
|
||||
"item_code",
|
||||
],
|
||||
filters={"item_code": ["in", variant_list], "buying": 1},
|
||||
@@ -185,7 +185,7 @@ def get_selling_price_map(variant_list):
|
||||
selling = frappe.db.get_all(
|
||||
"Item Price",
|
||||
fields=[
|
||||
"avg(price_list_rate) as avg_rate",
|
||||
{"AVG": "price_list_rate", "as": "avg_rate"},
|
||||
"item_code",
|
||||
],
|
||||
filters={"item_code": ["in", variant_list], "selling": 1},
|
||||
|
||||
@@ -183,14 +183,15 @@ def get_voucher_type(doctype, txt, searchfield, start, page_len, filters):
|
||||
child_doctypes = frappe.get_all(
|
||||
"DocField",
|
||||
filters={"fieldname": "serial_and_batch_bundle"},
|
||||
fields=["distinct parent as parent"],
|
||||
fields=["parent"],
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
query_filters = {"options": ["in", [d.parent for d in child_doctypes]]}
|
||||
if txt:
|
||||
query_filters["parent"] = ["like", f"%{txt}%"]
|
||||
|
||||
return frappe.get_all("DocField", filters=query_filters, fields=["distinct parent"], as_list=True)
|
||||
return frappe.get_all("DocField", filters=query_filters, fields=["parent"], as_list=True, distinct=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -61,7 +61,7 @@ def get_stock_ledger_data(report_filters, filters):
|
||||
"name",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"sum(stock_value_difference) as stock_value",
|
||||
{"SUM": "stock_value_difference", "as": "stock_value"},
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
],
|
||||
@@ -88,7 +88,10 @@ def get_gl_data(report_filters, filters):
|
||||
"name",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value",
|
||||
{
|
||||
"SUB": [{"SUM": "debit_in_account_currency"}, {"SUM": "credit_in_account_currency"}],
|
||||
"as": "account_value",
|
||||
},
|
||||
],
|
||||
group_by="voucher_type, voucher_no",
|
||||
)
|
||||
|
||||
@@ -546,7 +546,10 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
|
||||
|
||||
opening_data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["sum(actual_qty) as qty_after_transaction", "sum(stock_value_difference) as stock_value"],
|
||||
fields=[
|
||||
{"SUM": "actual_qty", "as": "qty_after_transaction"},
|
||||
{"SUM": "stock_value_difference", "as": "stock_value"},
|
||||
],
|
||||
filters=query_filters,
|
||||
)[0]
|
||||
|
||||
|
||||
@@ -1511,7 +1511,7 @@ def get_batchwise_qty(voucher_type, voucher_no):
|
||||
batches = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": ("in", bundles), "batch_no": ("is", "set")},
|
||||
fields=["batch_no", "SUM(qty) as qty"],
|
||||
fields=["batch_no", {"SUM": "qty", "as": "qty"}],
|
||||
group_by="batch_no",
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Timestamp
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.stock.utils import scan_barcode
|
||||
@@ -20,11 +21,23 @@ class StockTestMixin:
|
||||
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
|
||||
if sle_filters:
|
||||
filters.update(sle_filters)
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["*"],
|
||||
filters=filters,
|
||||
order_by="timestamp(posting_date, posting_time), creation",
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.select("*")
|
||||
.where(sle.voucher_no == doc.name)
|
||||
.where(sle.voucher_type == doc.doctype)
|
||||
.where(sle.is_cancelled == 0)
|
||||
)
|
||||
if sle_filters:
|
||||
for key, value in sle_filters.items():
|
||||
query = query.where(sle[key] == value)
|
||||
|
||||
sles = (
|
||||
query.orderby(Timestamp(sle.posting_date, sle.posting_time))
|
||||
.orderby(sle.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
self.assertGreaterEqual(len(sles), len(expected_sles))
|
||||
|
||||
|
||||
269
erpnext/workspace_sidebar/settings.json
Normal file
269
erpnext/workspace_sidebar/settings.json
Normal file
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"creation": "2025-11-17 13:19:05.050624",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar",
|
||||
"header_icon": "setting",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.051033",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "home",
|
||||
"idx": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Settings",
|
||||
"link_type": "Workspace",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "gj96mk2v27",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.050624",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "crm",
|
||||
"idx": 2,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "CRM Settings",
|
||||
"link_to": "CRM Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "evs5nlr1q0",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.053190",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "sell",
|
||||
"idx": 3,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Selling Settings",
|
||||
"link_to": "Selling Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "gj9u1k3i56",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.053492",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "buying",
|
||||
"idx": 4,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Buying Settings",
|
||||
"link_to": "Buying Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "gj9ct0hfhj",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.052094",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "accounting",
|
||||
"idx": 5,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounts Settings",
|
||||
"link_to": "Accounts Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "gj996hcboc",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.052884",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "stock",
|
||||
"idx": 6,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Stock Settings",
|
||||
"link_to": "Stock Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "gj9hup4r96",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.050624",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "building-2",
|
||||
"idx": 7,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Manufacturing Settings",
|
||||
"link_to": "Manufacturing Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "e0c9du5q9a",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.051420",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "printer",
|
||||
"idx": 8,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Print Settings",
|
||||
"link_to": "Print Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "gj9j587oit",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.051734",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "computer",
|
||||
"idx": 9,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "System Settings",
|
||||
"link_to": "System Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "gj9uqukv96",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.052565",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "earth",
|
||||
"idx": 10,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Global Defaults",
|
||||
"link_to": "Global Defaults",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "gj9koe7aa7",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"creation": "2025-11-17 13:19:05.050624",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar Item",
|
||||
"icon": "projects",
|
||||
"idx": 11,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Projects Settings",
|
||||
"link_to": "Projects Settings",
|
||||
"link_type": "DocType",
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"name": "e0ccorpl3c",
|
||||
"owner": "Administrator",
|
||||
"parent": "Settings",
|
||||
"parentfield": "items",
|
||||
"parenttype": "Workspace Sidebar",
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2025-11-19 16:15:06.422138",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Settings",
|
||||
"owner": "Administrator",
|
||||
"title": "Settings"
|
||||
}
|
||||
Reference in New Issue
Block a user