mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-02 21:18:27 +00:00
Merge pull request #50087 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -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.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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
|
||||
|
||||
-->
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 += [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -284,15 +284,15 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_message():
|
||||
return """<span class="indicator">
|
||||
Valid till :
|
||||
return f"""<span class="indicator">
|
||||
{_("Valid Till")}:
|
||||
</span>
|
||||
<span class="indicator orange">
|
||||
Expires in a week or less
|
||||
{_("Expires in a week or less")}
|
||||
</span>
|
||||
|
||||
<span class="indicator red">
|
||||
Expires today / Already Expired
|
||||
{_("Expires today or already expired")}
|
||||
</span>"""
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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}%"))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."))
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user