Merge pull request #50087 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
Diptanil Saha
2025-10-14 19:27:40 +05:30
committed by GitHub
46 changed files with 342 additions and 131 deletions

View File

@@ -6,7 +6,7 @@ Feature requests are also a great way to take the product forward. New ideas can
When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want.
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com).
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.frappe.io](https://discuss.frappe.io/c/erpnext/6).
### Reply and Closing Policy

View File

@@ -9,7 +9,7 @@ body:
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6)
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
2. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Forum
url: https://discuss.erpnext.com/
url: https://discuss.frappe.io/c/erpnext/6
about: For general QnA, discussions and community help.

View File

@@ -11,7 +11,7 @@ assignees: ''
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
@@ -21,7 +21,7 @@ Please keep in mind that we get many many requests and we can't possibly work on
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
1. Certified ERPNext partners: https://erpnext.com/partners
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
2. Developer community on ERPNext forums: https://discuss.frappe.io/c/framework/5
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
-->

View File

@@ -64,7 +64,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.

View File

@@ -46,7 +46,8 @@ def validate_service_stop_date(doc):
if (
old_stop_dates
and old_stop_dates.get(item.name)
and item.service_stop_date != old_stop_dates.get(item.name)
and item.service_stop_date
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))

View File

@@ -82,7 +82,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-29 11:37:42.678556",
"modified": "2025-10-13 15:11:58.300836",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Payment Ledger Entry",

View File

@@ -34,3 +34,15 @@ class AdvancePaymentLedgerEntry(Document):
and not frappe.flags.is_reverse_depr_entry
):
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
def on_doctype_update():
frappe.db.add_index(
"Advance Payment Ledger Entry",
["against_voucher_type", "against_voucher_no"],
)
frappe.db.add_index(
"Advance Payment Ledger Entry",
["voucher_type", "voucher_no"],
)

View File

@@ -131,8 +131,8 @@ class GLEntry(Document):
if not self.is_cancelled and not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
# skipping validation for payroll entry creation in case party is not required
if not frappe.flags.party_not_required_for_receivable_payable:
if not frappe.flags.party_not_required: # skipping validation if party is not required
if account_type == "Receivable":
frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format(

View File

@@ -59,6 +59,7 @@
"addtional_info",
"mode_of_payment",
"payment_order",
"party_not_required",
"column_break3",
"is_opening",
"stock_entry",
@@ -543,6 +544,14 @@
"label": "Is System Generated",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "party_not_required",
"fieldtype": "Check",
"hidden": 1,
"label": "Party Not Required",
"no_copy": 1
}
],
"icon": "fa fa-file-text",
@@ -557,7 +566,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2024-07-18 15:32:29.413598",
"modified": "2025-09-29 13:05:46.982277",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -72,6 +72,7 @@ class JournalEntry(AccountsController):
multi_currency: DF.Check
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
paid_loan: DF.Data | None
party_not_required: DF.Check
pay_to_recd_from: DF.Data | None
payment_order: DF.Link | None
posting_date: DF.Date
@@ -543,10 +544,10 @@ class JournalEntry(AccountsController):
for d in self.get("accounts"):
account_type = frappe.get_cached_value("Account", d.account, "account_type")
# skipping validation for payroll entry creation
skip_validation = frappe.flags.party_not_required_for_receivable_payable
if account_type in ["Receivable", "Payable"]:
if not (d.party_type and d.party) and not skip_validation:
if (
not (d.party_type and d.party) and not self.party_not_required
): # skipping validation if party_not_required is passed via payroll entry
frappe.throw(
_(
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
@@ -1139,6 +1140,11 @@ class JournalEntry(AccountsController):
}
)
# set flag to skip party validation
account_type = frappe.get_cached_value("Account", d.account, "account_type")
if account_type in ["Receivable", "Payable"] and self.party_not_required:
frappe.flags.party_not_required = True
gl_map.append(
self.get_gl_dict(
row,
@@ -1166,6 +1172,7 @@ class JournalEntry(AccountsController):
merge_entries=merge_entries,
update_outstanding=update_outstanding,
)
frappe.flags.party_not_required = False
if cancel:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))

View File

@@ -286,7 +286,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-07-25 04:45:28.117715",
"modified": "2025-09-29 13:01:48.916517",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -2640,6 +2640,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
@change_settings(
"Buying Settings", {"maintain_same_rate": 0, "set_landed_cost_based_on_purchase_invoice_rate": 1}
)
def test_pr_status_rate_adjusted_from_pi(self):
pr = make_purchase_receipt(qty=5, rate=100)
pi = create_purchase_invoice_from_receipt(pr.name)
pi.submit()
pr.reload()
# Inital check
self.assertEqual(pr.status, "Completed")
pi.reload()
pi.cancel()
pi = create_purchase_invoice_from_receipt(pr.name)
pi.items[0].rate = 80
pi.submit()
pr.reload()
# Test 1 : Adjustment amount is negative
self.assertEqual(pr.status, "Completed")
pi.reload()
pi.cancel()
pi = create_purchase_invoice_from_receipt(pr.name)
pi.items[0].rate = 120
pi.submit()
pr.reload()
# Test 2 : Adjustment amount is positive
self.assertEqual(pr.status, "Completed")
def test_opening_invoice_rounding_adjustment_validation(self):
pi = make_purchase_invoice(do_not_save=1)
pi.items[0].rate = 99.98

View File

@@ -912,7 +912,8 @@
"label": "Rejected Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
"print_hide": 1,
"search_index": 1
},
{
"fieldname": "wip_composite_asset",
@@ -983,7 +984,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-03-12 16:33:13.453290",
"modified": "2025-10-14 13:01:54.441511",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -993,4 +994,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -798,6 +798,15 @@ frappe.ui.form.on("Sales Invoice", {
},
};
};
frm.set_query("sales_person", "sales_team", function () {
return {
filters: {
is_group: 0,
enabled: 1,
},
};
});
},
onload: function (frm) {
frm.redemption_conversion_factor = null;

View File

@@ -354,7 +354,7 @@ def get_asset_details_for_grouped_by_category(filters):
# nosemgrep
return frappe.db.sql(
f"""
SELECT a.name,
SELECT a.name, a.asset_name,
ifnull(sum(case when a.purchase_date < %(from_date)s then
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
a.gross_purchase_amount
@@ -583,6 +583,14 @@ def get_columns(filters):
"width": 120,
}
)
columns.append(
{
"label": _("Asset Name"),
"fieldname": "asset_name",
"fieldtype": "Data",
"width": 140,
}
)
columns += [
{

View File

@@ -507,7 +507,8 @@ def depreciate_asset(asset_doc, date, notes):
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
asset_doc.reload()
cancel_depreciation_entries(asset_doc, date)
if not frappe.flags.is_composite_component:
cancel_depreciation_entries(asset_doc, date)
@erpnext.allow_regional

View File

@@ -492,14 +492,18 @@ class AssetCapitalization(StockController):
asset = frappe.get_doc("Asset", item.asset)
if asset.calculate_depreciation:
notes = _(
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload()
frappe.flags.is_composite_component = True
try:
notes = _(
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload()
finally:
frappe.flags.is_composite_component = False
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset,

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_link_to_form
from frappe.utils import cstr, get_link_to_form
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
@@ -142,18 +142,10 @@ class AssetMovement(Document):
def update_asset_location_and_custodian(self, asset_id, location, employee):
asset = frappe.get_doc("Asset", asset_id)
updates = {}
if employee and employee != asset.custodian:
updates["custodian"] = employee
elif not employee and asset.custodian:
updates["custodian"] = ""
if cstr(employee) != asset.custodian:
frappe.db.set_value("Asset", asset_id, "custodian", cstr(employee))
if location and location != asset.location:
updates["location"] = location
if updates:
frappe.db.set_value("Asset", asset_id, updates)
frappe.db.set_value("Asset", asset_id, "location", location)
def log_asset_activity(self, asset_id, location, employee):
if location and employee:

View File

@@ -319,6 +319,7 @@ def get_asset_value_adjustment_map(filters, finance_book):
.select(asset.name.as_("asset"), Sum(gle.debit - gle.credit).as_("adjustment_amount"))
.where(gle.account == aca.fixed_asset_account)
.where(gle.is_cancelled == 0)
.where(gle.is_opening == "No")
.where(company.name == filters.company)
.where(asset.docstatus == 1)
)

View File

@@ -284,15 +284,15 @@ def get_columns(filters):
def get_message():
return """<span class="indicator">
Valid till : &nbsp;&nbsp;
return f"""<span class="indicator">
{_("Valid Till")}:&nbsp;&nbsp;
</span>
<span class="indicator orange">
Expires in a week or less
{_("Expires in a week or less")}
</span>
&nbsp;&nbsp;
<span class="indicator red">
Expires today / Already Expired
{_("Expires today or already expired")}
</span>"""

View File

@@ -637,7 +637,8 @@ class SubcontractingController(StockController):
if use_serial_batch_fields:
rm_obj.use_serial_batch_fields = 1
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if not self.flags.get("reset_raw_materials"):
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if self.doctype == "Subcontracting Receipt":
if not use_serial_batch_fields:

View File

@@ -24,6 +24,7 @@ frappe.ui.form.on("Production Plan", {
query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
filters: {
company: frm.doc.company,
item_code: frm.doc.item_code,
},
};
});
@@ -105,6 +106,8 @@ frappe.ui.form.on("Production Plan", {
__("View")
);
let has_create_buttons = false;
if (frm.doc.status !== "Completed") {
if (frm.doc.status === "Closed") {
frm.add_custom_button(
@@ -134,6 +137,7 @@ frappe.ui.form.on("Production Plan", {
},
__("Create")
);
has_create_buttons = true;
}
if (
@@ -148,12 +152,13 @@ frappe.ui.form.on("Production Plan", {
},
__("Create")
);
has_create_buttons = true;
}
}
}
if (frm.doc.status !== "Closed") {
frm.page.set_inner_btn_group_as_primary(__("Create"));
if (has_create_buttons && frm.doc.status !== "Closed") {
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
}
frm.trigger("material_requirement");

View File

@@ -1543,6 +1543,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
include_safety_stock = doc.get("include_safety_stock")
so_item_details = frappe._dict()
existing_sub_assembly_items = set()
sub_assembly_items = defaultdict(int)
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
@@ -1576,7 +1577,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
item_details = {}
if doc.get("sub_assembly_items"):
item_details = get_raw_materials_of_sub_assembly_items(
so_item_details[doc.get("sales_order")].keys() if so_item_details else [],
existing_sub_assembly_items,
item_details,
company,
bom_no,
@@ -1839,7 +1840,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(Sum(child.required_bom_qty))
.select(Sum(child.quantity * child.conversion_factor))
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
@@ -1955,6 +1956,7 @@ def get_raw_materials_of_sub_assembly_items(
sub_assembly_items,
planned_qty=planned_qty,
)
existing_sub_assembly_items.add(item.item_code)
else:
if not item.conversion_factor and item.purchase_uom:
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
@@ -1992,6 +1994,9 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page
if filters.get("sales_orders"):
query = query.where(so_table.name.isin(filters.get("sales_orders")))
if filters.get("item_code"):
query = query.where(table.item_code == filters.get("item_code"))
if txt:
query = query.where(table.parent.like(f"%{txt}%"))

View File

@@ -1637,11 +1637,17 @@ class TestProductionPlan(FrappeTestCase):
def test_calculation_of_sub_assembly_items(self):
make_item("Sub Assembly Item ", properties={"is_stock_item": 1})
make_item("Sub Assembly Item 2", properties={"is_stock_item": 1})
make_item("RM Item 1", properties={"is_stock_item": 1})
make_item("RM Item 2", properties={"is_stock_item": 1})
make_item("_Test FG Item 3", properties={"is_stock_item": 1})
make_item("_Test FG Item 4", properties={"is_stock_item": 1})
make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"])
make_bom(item="Sub Assembly Item 2", raw_materials=["RM Item 2"])
make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"])
make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"])
make_bom(item="_Test FG Item 3", raw_materials=["RM Item 1"])
make_bom(item="_Test FG Item 4", raw_materials=["Sub Assembly Item 2"])
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -1677,12 +1683,39 @@ class TestProductionPlan(FrappeTestCase):
"warehouse": "_Test Warehouse - _TC",
},
)
# Assembly item with similar RM item
plan.append(
"po_items",
{
"use_multi_level_bom": 1,
"item_code": "_Test FG Item 3",
"bom_no": frappe.db.get_value("Item", "_Test FG Item 3", "default_bom"),
"planned_qty": 10,
"planned_start_date": now_datetime(),
"stock_uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
},
)
# Sub-assembly item with similar RM item
plan.append(
"po_items",
{
"use_multi_level_bom": 1,
"item_code": "_Test FG Item 4",
"bom_no": frappe.db.get_value("Item", "_Test FG Item 4", "default_bom"),
"planned_qty": 10,
"planned_start_date": now_datetime(),
"stock_uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
},
)
plan.save()
plan.get_sub_assembly_items()
self.assertEqual(plan.sub_assembly_items[0].qty, 20)
self.assertEqual(plan.sub_assembly_items[1].qty, 50)
self.assertEqual(plan.sub_assembly_items[0].qty, 20) # Sub Assembly For FG 1
self.assertEqual(plan.sub_assembly_items[1].qty, 50) # Sub Assembly For FG 2
self.assertEqual(plan.sub_assembly_items[2].qty, 10) # Sub Assembly For FG 4
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
@@ -1690,8 +1723,11 @@ class TestProductionPlan(FrappeTestCase):
mr_items = get_items_for_material_requests(plan.as_dict())
self.assertEqual(mr_items[0].get("quantity"), 80)
self.assertEqual(mr_items[1].get("quantity"), 70)
# RM Item 1 (FG1 (100 + 100) + FG2 (50) + FG3 (10) - 90 in stock - 80 sub assembly stock)
self.assertEqual(mr_items[0].get("quantity"), 90)
# RM Item 2 (FG1 (100) + FG2 (50) + FG4 (10) - 80 sub assembly stock)
self.assertEqual(mr_items[1].get("quantity"), 80)
def test_production_plan_for_partial_sub_assembly_items(self):
from erpnext.controllers.status_updater import OverAllowanceError

View File

@@ -113,6 +113,13 @@ class ProductionPlanReport:
self.orders = query.run(as_dict=True)
def get_raw_materials(self):
"""Retrieve raw materials and source warehouses for production orders.
This method collects BOM or Work Order items depending on the selected
filter and updates `self.raw_materials_dict`, `self.warehouses`,
and `self.item_codes` accordingly.
"""
if not self.orders:
return
self.warehouses = [d.warehouse for d in self.orders]
@@ -135,7 +142,7 @@ class ProductionPlanReport:
)
or []
)
self.warehouses.extend([d.source_warehouse for d in raw_materials])
self.warehouses.extend([d.warehouse for d in raw_materials])
else:
bom_nos = []

View File

@@ -1,15 +1,17 @@
import urllib.parse
import frappe
def get_context(context):
if frappe.form_dict.project:
context.parents = [
{"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project}
]
context.success_url = "/projects?project=" + frappe.form_dict.project
if project := frappe.form_dict.project:
title = frappe.utils.data.escape_html(project)
route = "/projects?" + urllib.parse.urlencode({"project": project})
context.parents = [{"title": title, "route": route}]
context.success_url = route
elif context.doc and context.doc.get("project"):
context.parents = [
{"title": context.doc.project, "route": "/projects?project=" + context.doc.project}
]
context.success_url = "/projects?project=" + context.doc.project
elif context.doc and (project := context.doc.get("project")):
title = frappe.utils.data.escape_html(project)
route = "/projects?" + urllib.parse.urlencode({"project": project})
context.parents = [{"title": title, "route": route}]
context.success_url = route

View File

@@ -171,13 +171,15 @@ erpnext.buying = {
shipping_address: this.frm.doc.shipping_address
},
callback: (r) => {
this.frm.set_value("billing_address", r.message.primary_address || "");
if (!this.frm.doc.billing_address)
this.frm.set_value("billing_address", r.message.primary_address || "");
if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
this.frm.set_value(
"shipping_address",
r.message.shipping_address || this.frm.doc.shipping_address || ""
);
if (
!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") ||
this.frm.doc.shipping_address
)
return;
this.frm.set_value("shipping_address", r.message.shipping_address || "");
},
});
erpnext.utils.set_letter_head(this.frm)

View File

@@ -1022,19 +1022,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} else {
set_pricing();
}
};
}
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
if (!is_drop_ship) {
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
}
if (
frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) &&
!this.frm.doc.shipping_address
) {
let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
if (!is_drop_ship) {
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
}
} else {
set_party_account(set_pricing);
}

View File

@@ -109,6 +109,7 @@ erpnext.sales_common = {
);
this.toggle_editable_price_list_rate();
this.change_warehouse_labels_for_return();
}
company() {
@@ -500,6 +501,33 @@ erpnext.sales_common = {
this.frm.set_value("discount_amount", 0);
this.frm.set_value("additional_discount_percentage", 0);
}
is_return() {
let reset = !this.frm.doc.is_return;
this.change_warehouse_labels_for_return(reset);
}
change_warehouse_labels_for_return(reset) {
// swap source and target warehouse labels for return
let source_warehouse_label = __("Source Warehouse");
let target_warehouse_label = __("Set Target Warehouse");
if (this.frm.doc.doctype == "Delivery Note") {
source_warehouse_label = __("Set Source Warehouse");
}
if (reset) {
// reset to original labels
this.frm.set_df_property("set_warehouse", "label", source_warehouse_label);
this.frm.set_df_property("set_target_warehouse", "label", target_warehouse_label);
return;
}
if (this.frm.doc.is_return) {
this.frm.set_df_property("set_warehouse", "label", target_warehouse_label);
this.frm.set_df_property("set_target_warehouse", "label", source_warehouse_label);
}
}
};
},
};

View File

@@ -457,7 +457,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
(["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) &&
!this.frm.doc.is_return) ||
(this.frm.doc.doctype === "Stock Entry" &&
this.frm.doc.purpose === "Material Receipt")
(this.frm.doc.purpose === "Material Receipt" ||
(this.frm.doc.purpose === "Manufacture" && this.item.is_finished_item)))
) {
is_inward = true;
}

View File

@@ -44,6 +44,15 @@ frappe.ui.form.on("Sales Order", {
};
});
frm.set_query("sales_person", "sales_team", function () {
return {
filters: {
is_group: 0,
enabled: 1,
},
};
});
frm.set_df_property("packed_items", "cannot_add_rows", true);
frm.set_df_property("packed_items", "cannot_delete_rows", true);
},

View File

@@ -262,6 +262,20 @@ def update_roles():
def create_default_role_profiles():
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
if frappe.db.exists("Role Profile", role_profile_name):
role_profile = frappe.get_doc("Role Profile", role_profile_name)
existing_roles = [row.role for row in role_profile.roles]
role_profile.roles = [row for row in role_profile.roles if row.role in roles]
for role in roles:
if role not in existing_roles:
role_profile.append("roles", {"role": role})
role_profile.save(ignore_permissions=True)
continue
role_profile = frappe.new_doc("Role Profile")
role_profile.role_profile = role_profile_name
for role in roles:

View File

@@ -74,6 +74,8 @@ def update_page_info(bootinfo):
def bootinfo(bootinfo):
if bootinfo.get("user") and bootinfo["user"].get("name"):
bootinfo["user"]["employee"] = ""
frappe.session.data.employee = ""
employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name")
if employee:
bootinfo["user"]["employee"] = employee
frappe.session.data.employee = employee

View File

@@ -158,7 +158,9 @@ class Batch(Document):
@frappe.whitelist()
def recalculate_batch_qty(self):
batches = get_batch_qty(batch_no=self.name, item_code=self.item, for_stock_levels=True)
batches = get_batch_qty(
batch_no=self.name, item_code=self.item, for_stock_levels=True, consider_negative_batches=True
)
batch_qty = 0.0
if batches:
for row in batches:
@@ -260,6 +262,7 @@ def get_batch_qty(
"posting_date": posting_date,
"posting_time": posting_time,
"batch_no": batch_no,
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
"ignore_voucher_nos": ignore_voucher_nos,
"for_stock_levels": for_stock_levels,
"consider_negative_batches": consider_negative_batches,

View File

@@ -334,6 +334,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
if (
doc.docstatus == 1 &&
!doc.is_return &&
doc.per_returned != 100 &&
doc.status != "Closed" &&
flt(doc.per_billed) < 100 &&
frappe.model.can_create("Sales Invoice")

View File

@@ -10,6 +10,8 @@ from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder import DocType
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import cint, flt
from erpnext.accounts.party import get_due_date
@@ -790,35 +792,39 @@ def get_list_context(context=None):
def get_invoiced_qty_map(delivery_note):
"""returns a map: {dn_detail: invoiced_qty}"""
invoiced_qty_map = {}
sii = DocType("Sales Invoice Item")
for dn_detail, qty in frappe.db.sql(
"""select dn_detail, qty from `tabSales Invoice Item`
where delivery_note=%s and docstatus=1""",
delivery_note,
):
if not invoiced_qty_map.get(dn_detail):
invoiced_qty_map[dn_detail] = 0
invoiced_qty_map[dn_detail] += qty
invoiced_qty_map = frappe._dict(
(
frappe.qb.from_(sii)
.select(sii.dn_detail, Sum(sii.qty).as_("qty"))
.where((sii.delivery_note == delivery_note) & (sii.docstatus == 1))
.groupby(sii.dn_detail)
).run()
)
return invoiced_qty_map
def get_returned_qty_map(delivery_note):
"""returns a map: {so_detail: returned_qty}"""
dn = DocType("Delivery Note")
dni = DocType("Delivery Note Item")
returned_qty_map = frappe._dict(
frappe.db.sql(
"""select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
where dn.name = dn_item.parent
and dn.docstatus = 1
and dn.is_return = 1
and dn.return_against = %s
and dn_item.qty <= 0
group by dn_item.item_code
""",
delivery_note,
)
(
frappe.qb.from_(dni)
.join(dn)
.on(dn.name == dni.parent)
.select(dni.dn_detail, Sum(Abs(dni.qty)).as_("qty"))
.where(
(dn.docstatus == 1)
& (dn.is_return == 1)
& (dn.return_against == delivery_note)
& (dni.qty <= 0)
)
.groupby(dni.dn_detail)
).run()
)
return returned_qty_map

View File

@@ -1115,7 +1115,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
buying_settings = frappe.get_single("Buying Settings")
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
total_amount, total_billed_amount = 0, 0
total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
if adjust_incoming_rate:
@@ -1155,6 +1155,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
) * item.qty
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
pi_landed_cost_amount += adjusted_amt
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
elif amount and item.billed_amt > amount:
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
@@ -1165,6 +1166,9 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
)
)
if pi_landed_cost_amount < 0:
total_billed_amount += abs(pi_landed_cost_amount)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)

View File

@@ -778,7 +778,8 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Material Request Item",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "expense_account",
@@ -1038,7 +1039,8 @@
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle"
"options": "Serial and Batch Bundle",
"search_index": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0",
@@ -1147,7 +1149,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-03-12 17:10:43.780622",
"modified": "2025-10-14 12:59:20.384056",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
@@ -1158,4 +1160,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -294,7 +294,7 @@ class SerialandBatchBundle(Document):
}
)
if self.returned_against and self.docstatus == 1:
if (self.returned_against or self.voucher_type == "Stock Reconciliation") and self.docstatus == 1:
kwargs["ignore_voucher_detail_no"] = self.voucher_detail_no
if self.docstatus == 1:
@@ -2677,7 +2677,10 @@ def get_stock_ledgers_for_serial_nos(kwargs):
else:
query = query.where(stock_ledger_entry[field] == kwargs.get(field))
if kwargs.voucher_no:
if kwargs.ignore_voucher_detail_no:
query = query.where(stock_ledger_entry.voucher_detail_no != kwargs.ignore_voucher_detail_no)
elif kwargs.voucher_no:
query = query.where(stock_ledger_entry.voucher_no != kwargs.voucher_no)
return query.run(as_dict=True)

View File

@@ -1323,18 +1323,9 @@ class TestStockEntry(FrappeTestCase):
posting_date="2021-07-02", # Illegal SE
purpose="Material Transfer",
),
dict(
item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
posting_date="2021-07-02", # Illegal SE
purpose="Material Transfer",
),
]
self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries)
self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle_batch(self):

View File

@@ -188,6 +188,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Basic Rate (as per Stock UOM)",
"non_negative": 1,
"oldfieldname": "incoming_rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@@ -446,7 +447,8 @@
"no_copy": 1,
"options": "Stock Entry",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "ste_detail",
@@ -454,7 +456,8 @@
"label": "Stock Entry Child",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "column_break_51",
@@ -613,7 +616,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-26 21:01:58.544797",
"modified": "2025-10-14 15:10:38.373099",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -589,6 +589,10 @@ class StockReconciliation(StockController):
if row.get(field):
key.append(row.get(field))
for dimension in get_inventory_dimensions():
if row.get(dimension.get("fieldname")):
key.append(row.get(dimension.get("fieldname")))
if key in item_warehouse_combinations:
self.validation_messages.append(
_get_msg(row_num, _("Same item and warehouse combination already entered."))

View File

@@ -1385,12 +1385,12 @@ def get_batch_current_qty(batch):
def throw_negative_batch_validation(batch_no, qty):
frappe.msgprint(
# This validation is important for backdated stock transactions with batch items
frappe.throw(
_(
"The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry."
).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)),
title=_("Warning!"),
indicator="orange",
title=_("Negative Stock Error"),
)

View File

@@ -732,6 +732,10 @@ class update_entries_after:
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
dependant_sle.voucher_no
):
if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"):
return
val["transfer_entry_to_repost"] = True
self.distinct_item_warehouses[key] = val
self.new_items_found = True
@@ -888,9 +892,8 @@ class update_entries_after:
sle.stock_value = self.wh_data.stock_value
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
if not sle.is_adjustment_entry:
sle.stock_value_difference = stock_value_difference
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
sle.stock_value_difference = stock_value_difference
if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0:
sle.stock_value_difference = (
get_stock_value_difference(
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no

View File

@@ -195,6 +195,7 @@ class SubcontractingReceipt(SubcontractingController):
@frappe.whitelist()
def reset_raw_materials(self):
self.supplied_items = []
self.flags.reset_raw_materials = True
self.create_raw_materials_supplied()
def validate_closed_subcontracting_order(self):