Merge remote-tracking branch 'upstream/version-15-hotfix' into mergify/bp/version-15-hotfix/pr-49875

This commit is contained in:
barredterra
2025-11-21 00:46:43 +01:00
49 changed files with 351 additions and 141 deletions

View File

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

View File

@@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_account",

View File

@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
from erpnext.accounts.utils import (
build_qb_match_conditions,
get_advance_payment_doctypes,
@@ -994,11 +995,7 @@ class ReceivablePayableReport:
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
cost_center_list = [
center.name
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
]
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
def add_common_filters(self):

View File

@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -174,7 +174,7 @@ def add_solvency_ratios(
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
for year in years:
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
profit_after_tax = flt(total_income.get(year)) - flt(total_expense.get(year))
share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
debt_equity_ratio[year] = calculate_ratio(total_liability.get(year), share_holder_fund, precision)
@@ -199,7 +199,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
avg_data = {}
for d in ["Receivable", "Payable", "Stock"]:
avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters)
avg_data[frappe.scrub(d)] = avg_ratio_balance(d, period_list, precision, filters)
avg_debtors, avg_creditors, avg_stock = (
avg_data.get("receivable"),

View File

@@ -566,6 +566,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
else:
update_value_in_dict(consolidated_gle, key, gle)
if filters.get("include_dimensions"):
dimensions = [*accounting_dimensions, "cost_center", "project"]
for dimension in dimensions:
if val := gle.get(dimension):
gle[dimension] = _(val)
for value in consolidated_gle.values():
update_value_in_dict(totals, "total", value)
update_value_in_dict(totals, "closing", value)

View File

@@ -4,7 +4,9 @@
import frappe
from frappe import _
from frappe.utils import getdate
from frappe.utils import flt, getdate
from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
@@ -43,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
precision = get_currency_precision()
out = []
entries = {}
@@ -72,17 +75,28 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
if net_total_map.get((voucher_type, name)):
values = net_total_map.get((voucher_type, name))
if values:
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
# back calculate total amount from rate and tax_amount
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
total_amount = grand_total = base_total
elif voucher_type == "Purchase Invoice":
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
(voucher_type, name)
)
else:
total_amount, grand_total, base_total = net_total_map.get((voucher_type, name))
if tax_amount and rate:
# back calculate total amount from rate and tax_amount
total_amount = flt((tax_amount * 100) / rate, precision=precision)
else:
total_amount = values[0]
grand_total = values[1]
base_total = values[2]
if voucher_type == "Purchase Invoice":
bill_no = values[3]
bill_date = values[4]
else:
total_amount += entry.credit

View File

@@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: function () {
var company = frappe.query_report.get_filter_value("company");
return {
doctype: "Cost Center",
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Project",
},
{

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.report.financial_statements import (
filter_accounts,
filter_out_zero_value_rows,
get_cost_centers_with_children,
set_gl_entries_by_account,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
@@ -103,10 +104,6 @@ def get_data(filters):
opening_balances = get_opening_balances(filters, ignore_is_opening)
# add filter inside list so that the query in financial_statements.py doesn't break
if filters.project:
filters.project = [filters.project]
set_gl_entries_by_account(
filters.company,
filters.from_date,
@@ -270,18 +267,12 @@ def get_opening_balance(
opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher")
if filters.cost_center:
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
cost_center = frappe.qb.DocType("Cost Center")
opening_balance = opening_balance.where(
closing_balance.cost_center.isin(
frappe.qb.from_(cost_center)
.select("name")
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
)
closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center")))
)
if filters.project:
opening_balance = opening_balance.where(closing_balance.project == filters.project)
opening_balance = opening_balance.where(closing_balance.project.isin(filters.project))
if frappe.db.count("Finance Book"):
if filters.get("include_default_book_entries"):

View File

@@ -371,7 +371,6 @@
"label": "Other Details"
},
{
"allow_on_submit": 1,
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
@@ -379,7 +378,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"options": "Draft\nSubmitted\nCancelled\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"read_only": 1
},
{
@@ -597,7 +596,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-10-23 22:43:33.634452",
"modified": "2025-11-17 18:01:51.417942",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -103,6 +103,7 @@ class Asset(AccountsController):
status: DF.Literal[
"Draft",
"Submitted",
"Cancelled",
"Partially Depreciated",
"Fully Depreciated",
"Sold",

View File

@@ -139,7 +139,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
"options": "Asset",
"reqd": 1
},
@@ -250,7 +250,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-07-29 15:14:34.044564",
"modified": "2025-11-17 18:35:54.575265",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",

View File

@@ -60,6 +60,17 @@ class AssetRepair(AccountsController):
if self.get("stock_items"):
self.set_stock_items_cost()
self.calculate_total_repair_cost()
self.validate_purchase_invoice_status()
def validate_purchase_invoice_status(self):
if self.purchase_invoice:
docstatus = frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "docstatus")
if docstatus == 0:
frappe.throw(
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
get_link_to_form("Purchase Invoice", self.purchase_invoice)
)
)
def validate_asset(self):
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):

View File

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

View File

@@ -93,6 +93,7 @@ status_map = {
["Draft", None],
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
[
"Completed",

View File

@@ -432,7 +432,7 @@ def _set_missing_values(source, target):
@frappe.whitelist()
def get_lead_details(lead, posting_date=None, company=None):
def get_lead_details(lead, posting_date=None, company=None, doctype=None):
if not lead:
return {}
@@ -454,7 +454,7 @@ def get_lead_details(lead, posting_date=None, company=None):
}
)
set_address_details(out, lead, "Lead", company=company)
set_address_details(out, lead, "Lead", doctype=doctype, company=company)
taxes_and_charges = set_taxes(
None,

View File

@@ -548,12 +548,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
},
{
@@ -591,7 +593,6 @@
},
{
"default": "0",
"depends_on": "eval:doc.track_semi_finished_goods === 0",
"fieldname": "fg_based_operating_cost",
"fieldtype": "Check",
"label": "Finished Goods based Operating Cost"
@@ -640,7 +641,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2025-10-29 17:43:12.966753",
"modified": "2025-11-19 16:17:15.925156",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@@ -465,7 +465,7 @@ class BOM(WebsiteGenerator):
)
)
def get_rm_rate(self, arg):
def get_rm_rate(self, arg, notify=True):
"""Get raw material rate as per selected method, if bom exists takes bom cost"""
rate = 0
if not self.rm_cost_as_per:
@@ -491,7 +491,7 @@ class BOM(WebsiteGenerator):
),
alert=True,
)
else:
elif notify:
frappe.msgprint(
_("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]),
alert=True,
@@ -796,7 +796,8 @@ class BOM(WebsiteGenerator):
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
"sourced_by_supplier": d.sourced_by_supplier,
}
},
notify=False,
)
d.base_rate = flt(d.rate) * flt(self.conversion_rate)

View File

@@ -38,6 +38,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",
},
};
});
},
refresh: function (frm) {

View File

@@ -79,18 +79,35 @@ erpnext.financial_statements = {
},
open_general_ledger: function (data) {
if (!data.account && !data.accounts) return;
let project = $.grep(frappe.query_report.filters, function (e) {
let filters = frappe.query_report.filters;
let project = $.grep(filters, function (e) {
return e.df.fieldname == "project";
});
let cost_center = $.grep(filters, function (e) {
return e.df.fieldname == "cost_center";
});
frappe.route_options = {
account: data.account || data.accounts,
company: frappe.query_report.get_filter_value("company"),
from_date: data.from_date || data.year_start_date,
to_date: data.to_date || data.year_end_date,
project: project && project.length > 0 ? project[0].$input.val() : "",
project: project && project.length > 0 ? project[0].get_value() : "",
cost_center: cost_center && cost_center.length > 0 ? cost_center[0].get_value() : "",
};
filters.forEach((f) => {
if (f.df.fieldtype == "MultiSelectList") {
if (f.df.fieldname in frappe.route_options) return;
let value = f.get_value();
if (value && value.length > 0) {
frappe.route_options[f.df.fieldname] = value;
}
}
});
let report = "General Ledger";
if (["Payable", "Receivable"].includes(data.account_type)) {

View File

@@ -420,25 +420,36 @@ $.extend(erpnext.utils, {
if (!frappe.boot.setup_complete) {
return;
}
const today = frappe.datetime.get_today();
if (!date) {
date = frappe.datetime.get_today();
date = today;
}
let fiscal_year = "";
frappe.call({
method: "erpnext.accounts.utils.get_fiscal_year",
args: {
date: date,
boolean: boolean,
},
async: false,
callback: function (r) {
if (r.message) {
if (with_dates) fiscal_year = r.message;
else fiscal_year = r.message[0];
}
},
});
if (
frappe.boot.current_fiscal_year &&
date >= frappe.boot.current_fiscal_year[1] &&
date <= frappe.boot.current_fiscal_year[2]
) {
if (with_dates) fiscal_year = frappe.boot.current_fiscal_year;
else fiscal_year = frappe.boot.current_fiscal_year[0];
} else if (today != date) {
frappe.call({
method: "erpnext.accounts.utils.get_fiscal_year",
type: "GET", // make it cacheable
args: {
date: date,
boolean: boolean,
},
async: false,
callback: function (r) {
if (r.message) {
if (with_dates) fiscal_year = r.message;
else fiscal_year = r.message[0];
}
},
});
}
return fiscal_year;
},

View File

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

View File

@@ -252,6 +252,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
lead: this.frm.doc.party_name,
posting_date: this.frm.doc.transaction_date,
company: this.frm.doc.company,
doctype: this.frm.doc.doctype,
},
callback: function (r) {
if (r.message) {

View File

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

View File

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

View File

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

View File

@@ -103,6 +103,7 @@
"total_monthly_sales",
"column_break_goals",
"default_selling_terms",
"default_sales_contact",
"default_warehouse_for_sales_return",
"credit_limit",
"transactions_annual_history",
@@ -851,6 +852,12 @@
"fieldtype": "Select",
"label": "Reconciliation Takes Effect On",
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
},
{
"fieldname": "default_sales_contact",
"fieldtype": "Link",
"label": "Default Sales Contact",
"options": "Contact"
}
],
"icon": "fa fa-building",
@@ -858,7 +865,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-08-25 18:34:03.602046",
"modified": "2025-11-16 16:51:27.624096",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

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

View File

@@ -3,8 +3,11 @@
import frappe
from frappe.defaults import get_user_default
from frappe.utils import cint
from erpnext.accounts.utils import get_fiscal_years
def boot_session(bootinfo):
"""boot session - send website info if guest"""
@@ -53,6 +56,11 @@ def boot_session(bootinfo):
)
party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""")
fiscal_year = get_fiscal_years(
frappe.utils.nowdate(), company=get_user_default("company"), boolean=True
)
if fiscal_year:
bootinfo.current_fiscal_year = fiscal_year[0]
bootinfo.party_account_types = frappe._dict(party_account_types)
bootinfo.sysdefaults.demo_company = frappe.db.get_single_value("Global Defaults", "demo_company")

View File

@@ -55,6 +55,11 @@ class ItemPrice(Document):
if not frappe.db.exists("Item", self.item_code):
frappe.throw(_("Item {0} not found.").format(self.item_code))
if self.uom and not frappe.db.exists(
"UOM Conversion Detail", {"parenttype": "Item", "parent": self.item_code, "uom": self.uom}
):
frappe.throw(_("UOM {0} not found in Item {1}").format(self.uom, self.item_code))
def update_price_list_details(self):
if self.price_list:
price_list_details = frappe.db.get_value(

View File

@@ -332,6 +332,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 },
{

View File

@@ -108,7 +108,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
@@ -169,7 +174,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

View File

@@ -893,7 +893,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed",
"options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
@@ -1300,7 +1300,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2025-08-06 16:41:02.690658",
"modified": "2025-11-12 19:53:48.173096",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -117,7 +117,15 @@ class PurchaseReceipt(BuyingController):
shipping_address_display: DF.SmallText | None
shipping_rule: DF.Link | None
status: DF.Literal[
"", "Draft", "Partly Billed", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"
"",
"Draft",
"Partly Billed",
"To Bill",
"Completed",
"Return",
"Return Issued",
"Cancelled",
"Closed",
]
subcontracting_receipt: DF.Link | None
supplied_items: DF.Table[PurchaseReceiptItemSupplied]

View File

@@ -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"),

View File

@@ -11,7 +11,7 @@ frappe.listview_settings["Purchase Receipt"] = {
"currency",
],
get_indicator: function (doc) {
if (cint(doc.is_return) == 1) {
if (cint(doc.is_return) == 1 && doc.status == "Return") {
return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];

View File

@@ -455,6 +455,7 @@ class TestPurchaseReceipt(FrappeTestCase):
# Check if Original PR updated
self.assertEqual(pr.items[0].returned_qty, 2)
self.assertEqual(pr.per_returned, 40)
self.assertEqual(returned.status, "Return")
from erpnext.controllers.sales_and_purchase_return import make_return_doc
@@ -2128,7 +2129,7 @@ class TestPurchaseReceipt(FrappeTestCase):
return_pr.items[0].stock_qty = 0.0
return_pr.submit()
self.assertEqual(return_pr.status, "To Bill")
self.assertEqual(return_pr.status, "Return")
pi = make_purchase_invoice(return_pr.name)
pi.submit()

View File

@@ -1347,7 +1347,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))
@@ -2297,7 +2326,11 @@ def get_auto_batch_nos(kwargs):
stock_ledgers_batches = get_stock_ledgers_batches(kwargs)
pos_invoice_batches = get_reserved_batches_for_pos(kwargs)
sre_reserved_batches = get_reserved_batches_for_sre(kwargs)
sre_reserved_batches = frappe._dict()
if not kwargs.ignore_reserved_stock:
sre_reserved_batches = get_reserved_batches_for_sre(kwargs)
picked_batches = frappe._dict()
if kwargs.get("is_pick_list"):
picked_batches = get_picked_batches(kwargs)

View File

@@ -951,12 +951,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;
@@ -1338,8 +1336,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]);
}
if (!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
if (!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
if (this.frm.doc.from_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
if (this.frm.doc.to_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
if (cint(frappe.user_defaults?.use_serial_batch_fields)) {
frappe.model.set_value(row.doctype, row.name, "use_serial_batch_fields", 1);

View File

@@ -1136,7 +1136,9 @@ class StockEntry(StockController):
"qty": row.transfer_qty * -1,
}
).update_serial_and_batch_entries()
elif not row.serial_and_batch_bundle:
elif not row.serial_and_batch_bundle and frappe.get_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
):
bundle_doc = SerialBatchCreation(
{
"item_code": row.item_code,

View File

@@ -979,6 +979,7 @@ class StockReconciliation(StockController):
is_customer_item = frappe.db.get_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:

View File

@@ -45,6 +45,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
def test_reco_for_moving_average(self):
self._test_reco_sle_gle("Moving Average")
@change_settings("Stock Settings", {"allow_negative_stock": 1})
def _test_reco_sle_gle(self, valuation_method):
item_code = self.make_item(properties={"valuation_method": valuation_method}).name

View File

@@ -185,12 +185,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",
@@ -256,7 +254,7 @@
],
"istable": 1,
"links": [],
"modified": "2025-03-12 16:34:51.326821",
"modified": "2025-11-20 15:27:13.868179",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
@@ -267,4 +265,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -678,6 +678,36 @@ class TestStockReservationEntry(FrappeTestCase):
# Test - 1: ValidationError should be thrown as the inwarded stock is reserved.
self.assertRaises(frappe.ValidationError, se.cancel)
@change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1})
def test_reserved_stock_validation_for_batch_item(self):
item_properties = {
"is_stock_item": 1,
"valuation_rate": 100,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "SRBV-.#####.",
}
sr_item = make_item(item_code="Test Reserve Item", properties=item_properties)
# inward 100 qty of stock
create_material_receipt(items={sr_item.name: sr_item}, warehouse=self.warehouse, qty=100)
# reserve 80 qty from sales order
so = make_sales_order(item_code=sr_item.name, warehouse=self.warehouse, qty=80)
so.create_stock_reservation_entries()
# create a material issue entry including the reserved qty 10
se = make_stock_entry(
item_code=sr_item.name,
qty=30,
from_warehouse=self.warehouse,
rate=100,
purpose="Material Issue",
do_not_submit=True,
)
# validation for reserved stock should be thrown
self.assertRaises(frappe.ValidationError, se.submit)
def tearDown(self) -> None:
cancel_all_stock_reservation_entries()
return super().tearDown()

View File

@@ -221,7 +221,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:

View File

@@ -259,7 +259,7 @@ class SerialBatchBundle:
and not self.sle.serial_and_batch_bundle
and self.item_details.has_batch_no == 1
and (
self.item_details.create_new_batch
(self.item_details.create_new_batch and self.sle.actual_qty > 0)
or (
frappe.db.get_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"

View File

@@ -2240,9 +2240,11 @@ def validate_reserved_stock(kwargs):
kwargs.ignore_voucher_nos = [kwargs.voucher_no]
if kwargs.serial_no:
kwargs.serial_nos = kwargs.serial_no.split("\n")
validate_reserved_serial_nos(kwargs)
elif kwargs.batch_no:
kwargs.batch_nos = [kwargs.batch_no]
validate_reserved_batch_nos(kwargs)
elif kwargs.serial_and_batch_bundle:
@@ -2311,6 +2313,7 @@ def validate_reserved_batch_nos(kwargs):
"posting_date": kwargs.posting_date,
"posting_time": kwargs.posting_time,
"ignore_voucher_nos": kwargs.ignore_voucher_nos,
"ignore_reserved_stock": True,
}
)
)