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. 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 ### Reply and Closing Policy

View File

@@ -9,7 +9,7 @@ body:
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: 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 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. - 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 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. maintainers to reproduce, the faster it'll be fixed.

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Community Forum - name: Community Forum
url: https://discuss.erpnext.com/ url: https://discuss.frappe.io/c/erpnext/6
about: For general QnA, discussions and community help. 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: 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 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 2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion. 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. 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: 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 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 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. 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. 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. 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 ( if (
old_stop_dates old_stop_dates
and old_stop_dates.get(item.name) 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)) frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))

View File

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

View File

@@ -34,3 +34,15 @@ class AdvancePaymentLedgerEntry(Document):
and not frappe.flags.is_reverse_depr_entry and not frappe.flags.is_reverse_depr_entry
): ):
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None) 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): if not self.is_cancelled and not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type") 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": if account_type == "Receivable":
frappe.throw( frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format( _("{0} {1}: Customer is required against Receivable account {2}").format(

View File

@@ -59,6 +59,7 @@
"addtional_info", "addtional_info",
"mode_of_payment", "mode_of_payment",
"payment_order", "payment_order",
"party_not_required",
"column_break3", "column_break3",
"is_opening", "is_opening",
"stock_entry", "stock_entry",
@@ -543,6 +544,14 @@
"label": "Is System Generated", "label": "Is System Generated",
"no_copy": 1, "no_copy": 1,
"read_only": 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", "icon": "fa fa-file-text",
@@ -557,7 +566,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2024-07-18 15:32:29.413598", "modified": "2025-09-29 13:05:46.982277",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@@ -72,6 +72,7 @@ class JournalEntry(AccountsController):
multi_currency: DF.Check multi_currency: DF.Check
naming_series: DF.Literal["ACC-JV-.YYYY.-"] naming_series: DF.Literal["ACC-JV-.YYYY.-"]
paid_loan: DF.Data | None paid_loan: DF.Data | None
party_not_required: DF.Check
pay_to_recd_from: DF.Data | None pay_to_recd_from: DF.Data | None
payment_order: DF.Link | None payment_order: DF.Link | None
posting_date: DF.Date posting_date: DF.Date
@@ -543,10 +544,10 @@ class JournalEntry(AccountsController):
for d in self.get("accounts"): for d in self.get("accounts"):
account_type = frappe.get_cached_value("Account", d.account, "account_type") 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 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( frappe.throw(
_( _(
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}" "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( gl_map.append(
self.get_gl_dict( self.get_gl_dict(
row, row,
@@ -1166,6 +1172,7 @@ class JournalEntry(AccountsController):
merge_entries=merge_entries, merge_entries=merge_entries,
update_outstanding=update_outstanding, update_outstanding=update_outstanding,
) )
frappe.flags.party_not_required = False
if cancel: if cancel:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))

View File

@@ -286,7 +286,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-07-25 04:45:28.117715", "modified": "2025-09-29 13:01:48.916517",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "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) 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): def test_opening_invoice_rounding_adjustment_validation(self):
pi = make_purchase_invoice(do_not_save=1) pi = make_purchase_invoice(do_not_save=1)
pi.items[0].rate = 99.98 pi.items[0].rate = 99.98

View File

@@ -912,7 +912,8 @@
"label": "Rejected Serial and Batch Bundle", "label": "Rejected Serial and Batch Bundle",
"no_copy": 1, "no_copy": 1,
"options": "Serial and Batch Bundle", "options": "Serial and Batch Bundle",
"print_hide": 1 "print_hide": 1,
"search_index": 1
}, },
{ {
"fieldname": "wip_composite_asset", "fieldname": "wip_composite_asset",
@@ -983,7 +984,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-03-12 16:33:13.453290", "modified": "2025-10-14 13:01:54.441511",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",
@@ -993,4 +994,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "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) { onload: function (frm) {
frm.redemption_conversion_factor = null; frm.redemption_conversion_factor = null;

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import 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 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): def update_asset_location_and_custodian(self, asset_id, location, employee):
asset = frappe.get_doc("Asset", asset_id) asset = frappe.get_doc("Asset", asset_id)
updates = {} if cstr(employee) != asset.custodian:
if employee and employee != asset.custodian: frappe.db.set_value("Asset", asset_id, "custodian", cstr(employee))
updates["custodian"] = employee
elif not employee and asset.custodian:
updates["custodian"] = ""
if location and location != asset.location: if location and location != asset.location:
updates["location"] = location frappe.db.set_value("Asset", asset_id, "location", location)
if updates:
frappe.db.set_value("Asset", asset_id, updates)
def log_asset_activity(self, asset_id, location, employee): def log_asset_activity(self, asset_id, location, employee):
if location and 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")) .select(asset.name.as_("asset"), Sum(gle.debit - gle.credit).as_("adjustment_amount"))
.where(gle.account == aca.fixed_asset_account) .where(gle.account == aca.fixed_asset_account)
.where(gle.is_cancelled == 0) .where(gle.is_cancelled == 0)
.where(gle.is_opening == "No")
.where(company.name == filters.company) .where(company.name == filters.company)
.where(asset.docstatus == 1) .where(asset.docstatus == 1)
) )

View File

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

View File

@@ -637,7 +637,8 @@ class SubcontractingController(StockController):
if use_serial_batch_fields: if use_serial_batch_fields:
rm_obj.use_serial_batch_fields = 1 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 self.doctype == "Subcontracting Receipt":
if not use_serial_batch_fields: 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", query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
filters: { filters: {
company: frm.doc.company, company: frm.doc.company,
item_code: frm.doc.item_code,
}, },
}; };
}); });
@@ -105,6 +106,8 @@ frappe.ui.form.on("Production Plan", {
__("View") __("View")
); );
let has_create_buttons = false;
if (frm.doc.status !== "Completed") { if (frm.doc.status !== "Completed") {
if (frm.doc.status === "Closed") { if (frm.doc.status === "Closed") {
frm.add_custom_button( frm.add_custom_button(
@@ -134,6 +137,7 @@ frappe.ui.form.on("Production Plan", {
}, },
__("Create") __("Create")
); );
has_create_buttons = true;
} }
if ( if (
@@ -148,12 +152,13 @@ frappe.ui.form.on("Production Plan", {
}, },
__("Create") __("Create")
); );
has_create_buttons = true;
} }
} }
}
if (frm.doc.status !== "Closed") { if (has_create_buttons && frm.doc.status !== "Closed") {
frm.page.set_inner_btn_group_as_primary(__("Create")); frm.page.set_inner_btn_group_as_primary(__("Create"));
}
} }
frm.trigger("material_requirement"); 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") include_safety_stock = doc.get("include_safety_stock")
so_item_details = frappe._dict() so_item_details = frappe._dict()
existing_sub_assembly_items = set()
sub_assembly_items = defaultdict(int) sub_assembly_items = defaultdict(int)
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"): 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 = {} item_details = {}
if doc.get("sub_assembly_items"): if doc.get("sub_assembly_items"):
item_details = get_raw_materials_of_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, item_details,
company, company,
bom_no, bom_no,
@@ -1839,7 +1840,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
frappe.qb.from_(table) frappe.qb.from_(table)
.inner_join(child) .inner_join(child)
.on(table.name == child.parent) .on(table.name == child.parent)
.select(Sum(child.required_bom_qty)) .select(Sum(child.quantity * child.conversion_factor))
.where( .where(
(table.docstatus == 1) (table.docstatus == 1)
& (child.item_code == item_code) & (child.item_code == item_code)
@@ -1955,6 +1956,7 @@ def get_raw_materials_of_sub_assembly_items(
sub_assembly_items, sub_assembly_items,
planned_qty=planned_qty, planned_qty=planned_qty,
) )
existing_sub_assembly_items.add(item.item_code)
else: else:
if not item.conversion_factor and item.purchase_uom: if not item.conversion_factor and item.purchase_uom:
item.conversion_factor = get_uom_conversion_factor(item.item_code, 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"): if filters.get("sales_orders"):
query = query.where(so_table.name.isin(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: if txt:
query = query.where(table.parent.like(f"%{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): def test_calculation_of_sub_assembly_items(self):
make_item("Sub Assembly Item ", properties={"is_stock_item": 1}) 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 1", properties={"is_stock_item": 1})
make_item("RM Item 2", 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", 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", 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 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 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -1677,12 +1683,39 @@ class TestProductionPlan(FrappeTestCase):
"warehouse": "_Test Warehouse - _TC", "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.save()
plan.get_sub_assembly_items() plan.get_sub_assembly_items()
self.assertEqual(plan.sub_assembly_items[0].qty, 20) self.assertEqual(plan.sub_assembly_items[0].qty, 20) # Sub Assembly For FG 1
self.assertEqual(plan.sub_assembly_items[1].qty, 50) 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 ( from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests, get_items_for_material_requests,
@@ -1690,8 +1723,11 @@ class TestProductionPlan(FrappeTestCase):
mr_items = get_items_for_material_requests(plan.as_dict()) mr_items = get_items_for_material_requests(plan.as_dict())
self.assertEqual(mr_items[0].get("quantity"), 80) # RM Item 1 (FG1 (100 + 100) + FG2 (50) + FG3 (10) - 90 in stock - 80 sub assembly stock)
self.assertEqual(mr_items[1].get("quantity"), 70) 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): def test_production_plan_for_partial_sub_assembly_items(self):
from erpnext.controllers.status_updater import OverAllowanceError from erpnext.controllers.status_updater import OverAllowanceError

View File

@@ -113,6 +113,13 @@ class ProductionPlanReport:
self.orders = query.run(as_dict=True) self.orders = query.run(as_dict=True)
def get_raw_materials(self): 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: if not self.orders:
return return
self.warehouses = [d.warehouse for d in self.orders] self.warehouses = [d.warehouse for d in self.orders]
@@ -135,7 +142,7 @@ class ProductionPlanReport:
) )
or [] or []
) )
self.warehouses.extend([d.source_warehouse for d in raw_materials]) self.warehouses.extend([d.warehouse for d in raw_materials])
else: else:
bom_nos = [] bom_nos = []

View File

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

View File

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

View File

@@ -1022,19 +1022,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} else { } else {
set_pricing(); set_pricing();
} }
};
} if (
frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && ["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) &&
['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); ) {
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 (!is_drop_ship) {
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
}
} else { } else {
set_party_account(set_pricing); set_party_account(set_pricing);
} }

View File

@@ -109,6 +109,7 @@ erpnext.sales_common = {
); );
this.toggle_editable_price_list_rate(); this.toggle_editable_price_list_rate();
this.change_warehouse_labels_for_return();
} }
company() { company() {
@@ -500,6 +501,33 @@ erpnext.sales_common = {
this.frm.set_value("discount_amount", 0); this.frm.set_value("discount_amount", 0);
this.frm.set_value("additional_discount_percentage", 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) && (["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) &&
!this.frm.doc.is_return) || !this.frm.doc.is_return) ||
(this.frm.doc.doctype === "Stock Entry" && (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; 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_add_rows", true);
frm.set_df_property("packed_items", "cannot_delete_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(): def create_default_role_profiles():
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items(): 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 = frappe.new_doc("Role Profile")
role_profile.role_profile = role_profile_name role_profile.role_profile = role_profile_name
for role in roles: for role in roles:

View File

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

View File

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

View File

@@ -334,6 +334,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
if ( if (
doc.docstatus == 1 && doc.docstatus == 1 &&
!doc.is_return && !doc.is_return &&
doc.per_returned != 100 &&
doc.status != "Closed" && doc.status != "Closed" &&
flt(doc.per_billed) < 100 && flt(doc.per_billed) < 100 &&
frappe.model.can_create("Sales Invoice") 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.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values 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 frappe.utils import cint, flt
from erpnext.accounts.party import get_due_date 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): def get_invoiced_qty_map(delivery_note):
"""returns a map: {dn_detail: invoiced_qty}""" """returns a map: {dn_detail: invoiced_qty}"""
invoiced_qty_map = {} sii = DocType("Sales Invoice Item")
for dn_detail, qty in frappe.db.sql( invoiced_qty_map = frappe._dict(
"""select dn_detail, qty from `tabSales Invoice Item` (
where delivery_note=%s and docstatus=1""", frappe.qb.from_(sii)
delivery_note, .select(sii.dn_detail, Sum(sii.qty).as_("qty"))
): .where((sii.delivery_note == delivery_note) & (sii.docstatus == 1))
if not invoiced_qty_map.get(dn_detail): .groupby(sii.dn_detail)
invoiced_qty_map[dn_detail] = 0 ).run()
invoiced_qty_map[dn_detail] += qty )
return invoiced_qty_map return invoiced_qty_map
def get_returned_qty_map(delivery_note): def get_returned_qty_map(delivery_note):
"""returns a map: {so_detail: returned_qty}""" """returns a map: {so_detail: returned_qty}"""
dn = DocType("Delivery Note")
dni = DocType("Delivery Note Item")
returned_qty_map = frappe._dict( returned_qty_map = frappe._dict(
frappe.db.sql( (
"""select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty frappe.qb.from_(dni)
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn .join(dn)
where dn.name = dn_item.parent .on(dn.name == dni.parent)
and dn.docstatus = 1 .select(dni.dn_detail, Sum(Abs(dni.qty)).as_("qty"))
and dn.is_return = 1 .where(
and dn.return_against = %s (dn.docstatus == 1)
and dn_item.qty <= 0 & (dn.is_return == 1)
group by dn_item.item_code & (dn.return_against == delivery_note)
""", & (dni.qty <= 0)
delivery_note, )
) .groupby(dni.dn_detail)
).run()
) )
return returned_qty_map 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") buying_settings = frappe.get_single("Buying Settings")
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") 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) item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
if adjust_incoming_rate: if adjust_incoming_rate:
@@ -1155,6 +1155,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
) * item.qty ) * item.qty
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) 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) item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
elif amount and item.billed_amt > amount: elif amount and item.billed_amt > amount:
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100 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) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed) pr_doc.db_set("per_billed", percent_billed)

View File

@@ -778,7 +778,8 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Material Request Item", "label": "Material Request Item",
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "expense_account", "fieldname": "expense_account",
@@ -1038,7 +1039,8 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle", "label": "Rejected Serial and Batch Bundle",
"no_copy": 1, "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", "depends_on": "eval:doc.use_serial_batch_fields === 0",
@@ -1147,7 +1149,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-03-12 17:10:43.780622", "modified": "2025-10-14 12:59:20.384056",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",
@@ -1158,4 +1160,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "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 kwargs["ignore_voucher_detail_no"] = self.voucher_detail_no
if self.docstatus == 1: if self.docstatus == 1:
@@ -2677,7 +2677,10 @@ def get_stock_ledgers_for_serial_nos(kwargs):
else: else:
query = query.where(stock_ledger_entry[field] == kwargs.get(field)) 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) query = query.where(stock_ledger_entry.voucher_no != kwargs.voucher_no)
return query.run(as_dict=True) return query.run(as_dict=True)

View File

@@ -1323,18 +1323,9 @@ class TestStockEntry(FrappeTestCase):
posting_date="2021-07-02", # Illegal SE posting_date="2021-07-02", # Illegal SE
purpose="Material Transfer", 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}) @change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle_batch(self): def test_future_negative_sle_batch(self):

View File

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

View File

@@ -589,6 +589,10 @@ class StockReconciliation(StockController):
if row.get(field): if row.get(field):
key.append(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: if key in item_warehouse_combinations:
self.validation_messages.append( self.validation_messages.append(
_get_msg(row_num, _("Same item and warehouse combination already entered.")) _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): 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." "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)), ).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)),
title=_("Warning!"), title=_("Negative Stock Error"),
indicator="orange",
) )

View File

@@ -732,6 +732,10 @@ class update_entries_after:
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry( elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
dependant_sle.voucher_no 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.distinct_item_warehouses[key] = val
self.new_items_found = True self.new_items_found = True
@@ -888,9 +892,8 @@ class update_entries_after:
sle.stock_value = self.wh_data.stock_value sle.stock_value = self.wh_data.stock_value
sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_queue = json.dumps(self.wh_data.stock_queue)
if not sle.is_adjustment_entry: sle.stock_value_difference = stock_value_difference
sle.stock_value_difference = stock_value_difference if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0:
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
sle.stock_value_difference = ( sle.stock_value_difference = (
get_stock_value_difference( get_stock_value_difference(
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no 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() @frappe.whitelist()
def reset_raw_materials(self): def reset_raw_materials(self):
self.supplied_items = [] self.supplied_items = []
self.flags.reset_raw_materials = True
self.create_raw_materials_supplied() self.create_raw_materials_supplied()
def validate_closed_subcontracting_order(self): def validate_closed_subcontracting_order(self):