-
Notifications
You must be signed in to change notification settings - Fork 10.1k
feat: Allowing closing individual items in sales order #51485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
04186e4 to
ddb34d1
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #51485 +/- ##
===========================================
+ Coverage 79.10% 79.12% +0.01%
===========================================
Files 1179 1179
Lines 121362 121414 +52
===========================================
+ Hits 96007 96064 +57
+ Misses 25355 25350 -5
🚀 New features to boost your workflow:
|
|
d7ff5d8 to
9a8d9e0
Compare
📝 WalkthroughWalkthroughAdds item-level closing for Sales Order items by introducing an Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI Agents
In @erpnext/selling/doctype/sales_order/sales_order.py:
- Around line 2081-2114: In close_or_reopen_selected_items, the subcontracted
Sales Order check (so.is_subcontracted) must run immediately after loading the
Sales Order (so = frappe.get_doc(...)) and before any modifications, saving
(so.save()) or calls to so.update_reserved_qty(), so move the existing
frappe.throw(_("Cannot close items in a subcontracted Sales Order")) to right
after the so variable is created and return/throw early to prevent changing
rows, saving the doc, or updating reserved quantities for subcontracted orders.
- Line 1967: The code erroneously calls so.get("items", {"is_closed": 0})
treating the second arg as a filter; replace this by retrieving the items list
(e.g. so_items = so.get("items", []) or set so.items = so.get("items") ) and
apply the filter when iterating: loop over so_items and skip entries where
getattr(item, "is_closed", 0) is truthy (use getattr(item, "is_closed", 0) to
safely handle packed_items that may lack the field); ensure subsequent logic
uses the filtered list or skips closed items rather than relying on get() to
filter.
In @erpnext/selling/doctype/sales_order/test_sales_order.py:
- Around line 64-66: The test uses incorrect non-boolean integers for the
boolean Check field is_stock_item; update the three make_item calls
(make_item("_Test SO Item Level Closing 1", {"is_stock_item": 4}),
make_item("_Test SO Item Level Closing 2", {"is_stock_item": 5}),
make_item("_Test SO Item Level Closing 3", {"is_stock_item": 6})) to use the
correct boolean/Check value of 1 (or True) for is_stock_item so the items are
properly treated as stock items.
🧹 Nitpick comments (4)
erpnext/stock/stock_balance.py (1)
140-140: Minor formatting inconsistency in SQL.The logic correctly filters out closed items, but the spacing around the equals operator should be consistent with the rest of the SQL query.
🔎 Suggested formatting fix
- and so_item.is_closed=0 and so.status not in ('On Hold', 'Closed'))) + and so_item.is_closed = 0 and so.status not in ('On Hold', 'Closed')))erpnext/patches/v16_0/update_sales_order_item_status.py (1)
9-15: Minor optimization: remove redundant WHERE condition.Line 12 duplicates the JOIN condition from line 9 (
sales_order.name == sales_order_item.parent). This redundancy doesn't affect correctness but adds unnecessary overhead during patch execution.🔎 Suggested optimization
frappe.qb.update(sales_order_item).join(sales_order).on(sales_order.name == sales_order_item.parent).set( sales_order_item.is_closed, 1 ).where( - (sales_order.name == sales_order_item.parent) - & (sales_order.status == "Closed") + (sales_order.status == "Closed") & (sales_order_item.is_closed == 0) ).run()erpnext/selling/doctype/sales_order/sales_order.js (2)
1794-1866: Translate dialog title.The dialog title at line 1824 should be wrapped in the translation function for internationalization.
🔎 Suggested fix
var d = new frappe.ui.Dialog({ - title: "Re-open Selected Items", + title: __("Re-open Selected Items"), size: "large",
1868-1962: Translate dialog title.The dialog title at line 1898 should be wrapped in the translation function for internationalization.
🔎 Suggested fix
var d = new frappe.ui.Dialog({ - title: "Close Selected Items", + title: __("Close Selected Items"), size: "large",
📜 Review details
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
erpnext/manufacturing/doctype/production_plan/production_plan.pyerpnext/manufacturing/doctype/work_order/work_order.pyerpnext/patches.txterpnext/patches/v16_0/update_sales_order_item_status.pyerpnext/public/js/utils.jserpnext/selling/doctype/sales_order/sales_order.jserpnext/selling/doctype/sales_order/sales_order.pyerpnext/selling/doctype/sales_order/test_sales_order.pyerpnext/selling/doctype/sales_order_item/sales_order_item.jsonerpnext/selling/doctype/sales_order_item/sales_order_item.pyerpnext/selling/report/item_wise_sales_history/item_wise_sales_history.pyerpnext/selling/report/sales_order_analysis/sales_order_analysis.pyerpnext/stock/stock_balance.py
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.
Applied to files:
erpnext/patches/v16_0/update_sales_order_item_status.pyerpnext/selling/report/item_wise_sales_history/item_wise_sales_history.pyerpnext/selling/report/sales_order_analysis/sales_order_analysis.pyerpnext/manufacturing/doctype/production_plan/production_plan.pyerpnext/manufacturing/doctype/work_order/work_order.pyerpnext/selling/doctype/sales_order/sales_order.pyerpnext/selling/doctype/sales_order_item/sales_order_item.pyerpnext/stock/stock_balance.pyerpnext/selling/doctype/sales_order/test_sales_order.py
📚 Learning: 2025-09-30T11:04:46.510Z
Learnt from: rohitwaghchaure
Repo: frappe/erpnext PR: 49766
File: erpnext/manufacturing/doctype/production_plan/production_plan.py:1717-1717
Timestamp: 2025-09-30T11:04:46.510Z
Learning: In the Production Plan's `get_items_for_material_requests` function in `erpnext/manufacturing/doctype/production_plan/production_plan.py`, always use `data.get("sales_order")` instead of `doc.get("sales_order")` when iterating over `po_items`. This ensures raw materials are correctly grouped by each production item's respective Sales Order, not a global document-level Sales Order.
Applied to files:
erpnext/selling/doctype/sales_order/sales_order.jserpnext/manufacturing/doctype/production_plan/production_plan.py
📚 Learning: 2025-08-12T22:10:55.921Z
Learnt from: LewisMojica
Repo: frappe/erpnext PR: 49108
File: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:929-953
Timestamp: 2025-08-12T22:10:55.921Z
Learning: In ERPNext stock management, there's a critical distinction between availability and reservation calculations: availability functions (like get_bundle_availability) should filter by current status (disabled=0) to determine what can be sold now, while reservation functions (like get_bundle_pos_reserved_qty) should include all historical transactions regardless of current bundle status to accurately reflect stock that was actually consumed. This prevents stock accounting errors when bundle configurations change after sales.
Applied to files:
erpnext/selling/doctype/sales_order/sales_order.pyerpnext/stock/stock_balance.py
📚 Learning: 2025-08-12T21:33:27.483Z
Learnt from: LewisMojica
Repo: frappe/erpnext PR: 49108
File: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:871-871
Timestamp: 2025-08-12T21:33:27.483Z
Learning: In ERPNext Product Bundle configurations, items can have qty = 0, which causes division by zero errors in POS bundle availability calculations. The fix is to use a high fallback value (like 1000000) instead of 0 when item.qty is zero, so these items don't constrain bundle availability.
Applied to files:
erpnext/selling/doctype/sales_order/sales_order.py
🧬 Code graph analysis (1)
erpnext/selling/doctype/sales_order/test_sales_order.py (1)
erpnext/selling/doctype/sales_order/sales_order.py (1)
close_or_reopen_selected_items(2082-2114)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Summary
🔇 Additional comments (21)
erpnext/patches.txt (1)
459-459: LGTM!The patch entry is correctly placed and follows the standard naming convention for v16_0 patches.
erpnext/selling/doctype/sales_order_item/sales_order_item.py (1)
50-50: LGTM!The type hint for the new
is_closedfield is correctly added in the TYPE_CHECKING block and properly formatted.erpnext/manufacturing/doctype/production_plan/production_plan.py (1)
1545-1545: LGTM!The filter correctly excludes closed Sales Order Items from production planning queries, aligning with the PR's item-level closure feature.
erpnext/manufacturing/doctype/work_order/work_order.py (1)
392-406: Consider adding item-level closure filter to the Sales Order query.The
query_sales_orderfunction filters out Sales Orders with status "Closed" but does not check theis_closedfield on individual Sales Order Items. Since this autocomplete usesor_filtersto match theproduction_itemagainst Sales Order Items, a Sales Order Item withis_closed = 1would still be returned. Adding a filter to exclude closed items would align with the item-level closure feature introduced in this PR.erpnext/selling/report/sales_order_analysis/sales_order_analysis.py (2)
81-82: LGTM!The addition of
soi.is_closedto the SELECT query is clean and correctly retrieves the new field for reporting.
330-335: LGTM!The "Is Closed" column definition follows the established pattern and correctly exposes the item-level closure status in the report UI.
erpnext/public/js/utils.js (1)
613-637: LGTM!The filtering logic correctly excludes closed Sales Order items from the update dialog, preventing accidental modifications while leaving other document types unaffected. This provides good UX defense-in-depth alongside backend validations.
erpnext/selling/doctype/sales_order_item/sales_order_item.json (1)
1015-1022: LGTM!The
is_closedfield definition is well-structured:
allow_on_submit: 1correctly enables closing/reopening after SO submissionin_list_view: 1provides necessary visibility in the items grid- Check fieldtype is appropriate for this boolean flag
erpnext/selling/doctype/sales_order/test_sales_order.py (5)
102-108: LGTM!Good test coverage for validating that fully delivered items cannot be closed, preventing data inconsistency.
111-128: LGTM!Excellent validation that closing an item correctly releases its reserved quantity in the Bin, ensuring inventory accuracy.
131-146: LGTM!Good test coverage ensuring that closed items cannot be updated via
update_child_qty_rate, maintaining data integrity.
149-151: LGTM!Well-designed test verifying that pick lists correctly exclude closed items, showing only the remaining open item for picking.
153-167: LGTM!Comprehensive test validating that reopening a closed item correctly restores its reserved quantity, ensuring the close/reopen workflow is fully reversible.
erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py (1)
127-133: LGTM! Column integration looks correct.The "Is Closed" column is properly added to the report with correct field type (Check), and the data flow from query → row dict → column display is consistent.
Also applies to: 169-169, 218-218
erpnext/selling/doctype/sales_order/sales_order.js (2)
1000-1010: LGTM! Button placement is appropriate.The "Close selected items" and "Re-open selected items" buttons are correctly placed under the Status dropdown when the order has pending delivery or billing.
1724-1724: LGTM! Correctly excludes closed items.The additional
!d.is_closedcheck properly prevents closed items from being included in the pending quantity calculation for Purchase Order creation.erpnext/selling/doctype/sales_order/sales_order.py (5)
594-599: LGTM! Status update integration is correct.The integration with
close_or_reopen_selected_itemsproperly handles item-level state changes when the Sales Order status is updated to Draft (reopen) or Closed.
1080-1081: LGTM! Correctly excludes closed items from Material Request.The added condition properly prevents closed Sales Order items from being included in Material Request generation.
1197-1200: LGTM! Correctly excludes closed items from Delivery Note.The condition properly filters out closed items during Delivery Note creation.
1439-1443: LGTM! Correctly excludes closed items from Sales Invoice.The condition properly prevents closed items from being included in Sales Invoice generation.
1890-1890: LGTM! Correctly excludes closed items from Pick List.The condition properly prevents closed items from being included in Pick List creation.
9a8d9e0 to
361e3c9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
erpnext/patches/v16_0/update_sales_order_item_status.py (1)
4-17: Preserve and restore the originalauto_commit_on_many_writessetting.The patch hardcodes the reset to
0at line 17, but this will incorrectly change the setting if it was already1before the patch ran.🔎 Proposed fix
def execute(): + original_auto_commit = frappe.db.auto_commit_on_many_writes frappe.db.auto_commit_on_many_writes = 1 sales_order = frappe.qb.DocType("Sales Order") sales_order_item = frappe.qb.DocType("Sales Order Item") frappe.qb.update(sales_order_item).join(sales_order).on(sales_order.name == sales_order_item.parent).set( sales_order_item.is_closed, 1 ).where( (sales_order.name == sales_order_item.parent) & (sales_order.status == "Closed") & (sales_order_item.is_closed == 0) ).run() - frappe.db.auto_commit_on_many_writes = 0 + frappe.db.auto_commit_on_many_writes = original_auto_commiterpnext/selling/doctype/sales_order/sales_order.py (1)
2083-2117: Consider streamlining the status update when all items are closed.At lines 2114-2116, when all items are closed via selective closing, the code calls
so.update_status("Closed"), which in turn (at line 597) invokesclose_or_reopen_selected_items(self.name, "Close", all_items_closed=True)again. Since all items are already marked closed and saved at line 2108, this second call performs redundant iteration over the items without changing their state. While not incorrect, this results in a minor inefficiency.💡 Alternative approach
Consider directly updating the status and calling the necessary hooks without re-invoking
close_or_reopen_selected_items:if not all_items_closed and all(d.is_closed for d in so.items): so.status = "Closed" - so.update_status("Closed") + so.set_status(update=True, status="Closed") + so.update_subcontracting_order_status() + so.notify_update() + clear_doctype_notifications(so) return TrueNote: This mirrors the logic in
update_status()but avoids the redundant call toclose_or_reopen_selected_items.erpnext/selling/doctype/sales_order/sales_order.js (1)
1868-1962: Implementation is correct, but consider UX refinement for the "Select all" checkbox.The method is technically sound and correctly handles all scenarios. However, the "Select all items" checkbox defaulting to checked (line 1905) might lead to confusion:
- Users clicking "Close" without reviewing will close the entire Sales Order, not individual items
- The hidden table when "Select all" is checked reduces visibility into what will happen
Consider one of these UX improvements:
- Default the checkbox to unchecked to encourage explicit selection
- Add a warning message when "Select all" is checked explaining it will close the entire SO
- Show a different dialog for "close all" vs "close selected"
That said, the implementation correctly handles the logic: closing all items properly triggers the full SO closure, which is the right behavior.
📜 Review details
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
erpnext/manufacturing/doctype/production_plan/production_plan.pyerpnext/manufacturing/doctype/work_order/work_order.pyerpnext/patches.txterpnext/patches/v16_0/update_sales_order_item_status.pyerpnext/public/js/utils.jserpnext/selling/doctype/sales_order/sales_order.jserpnext/selling/doctype/sales_order/sales_order.pyerpnext/selling/doctype/sales_order/test_sales_order.pyerpnext/selling/doctype/sales_order_item/sales_order_item.jsonerpnext/selling/doctype/sales_order_item/sales_order_item.pyerpnext/selling/report/item_wise_sales_history/item_wise_sales_history.pyerpnext/selling/report/sales_order_analysis/sales_order_analysis.pyerpnext/stock/stock_balance.py
🚧 Files skipped from review as they are similar to previous changes (9)
- erpnext/patches.txt
- erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
- erpnext/selling/doctype/sales_order_item/sales_order_item.json
- erpnext/stock/stock_balance.py
- erpnext/manufacturing/doctype/work_order/work_order.py
- erpnext/manufacturing/doctype/production_plan/production_plan.py
- erpnext/public/js/utils.js
- erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
- erpnext/selling/doctype/sales_order_item/sales_order_item.py
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.
Applied to files:
erpnext/selling/doctype/sales_order/test_sales_order.pyerpnext/selling/doctype/sales_order/sales_order.pyerpnext/patches/v16_0/update_sales_order_item_status.py
📚 Learning: 2025-09-30T11:04:46.510Z
Learnt from: rohitwaghchaure
Repo: frappe/erpnext PR: 49766
File: erpnext/manufacturing/doctype/production_plan/production_plan.py:1717-1717
Timestamp: 2025-09-30T11:04:46.510Z
Learning: In the Production Plan's `get_items_for_material_requests` function in `erpnext/manufacturing/doctype/production_plan/production_plan.py`, always use `data.get("sales_order")` instead of `doc.get("sales_order")` when iterating over `po_items`. This ensures raw materials are correctly grouped by each production item's respective Sales Order, not a global document-level Sales Order.
Applied to files:
erpnext/selling/doctype/sales_order/sales_order.pyerpnext/selling/doctype/sales_order/sales_order.js
📚 Learning: 2025-08-12T22:10:55.921Z
Learnt from: LewisMojica
Repo: frappe/erpnext PR: 49108
File: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:929-953
Timestamp: 2025-08-12T22:10:55.921Z
Learning: In ERPNext stock management, there's a critical distinction between availability and reservation calculations: availability functions (like get_bundle_availability) should filter by current status (disabled=0) to determine what can be sold now, while reservation functions (like get_bundle_pos_reserved_qty) should include all historical transactions regardless of current bundle status to accurately reflect stock that was actually consumed. This prevents stock accounting errors when bundle configurations change after sales.
Applied to files:
erpnext/selling/doctype/sales_order/sales_order.py
📚 Learning: 2025-08-12T21:33:27.483Z
Learnt from: LewisMojica
Repo: frappe/erpnext PR: 49108
File: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:871-871
Timestamp: 2025-08-12T21:33:27.483Z
Learning: In ERPNext Product Bundle configurations, items can have qty = 0, which causes division by zero errors in POS bundle availability calculations. The fix is to use a high fallback value (like 1000000) instead of 0 when item.qty is zero, so these items don't constrain bundle availability.
Applied to files:
erpnext/selling/doctype/sales_order/sales_order.py
🧬 Code graph analysis (2)
erpnext/selling/doctype/sales_order/test_sales_order.py (1)
erpnext/selling/doctype/sales_order/sales_order.py (1)
close_or_reopen_selected_items(2084-2117)
erpnext/selling/doctype/sales_order/sales_order.js (2)
erpnext/stock/doctype/stock_entry/stock_entry.js (5)
d(345-378)d(399-399)d(709-712)d(727-727)values(714-714)erpnext/accounts/doctype/sales_invoice/sales_invoice.js (3)
d(1064-1118)me(75-75)me(409-409)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: Patch Test
- GitHub Check: Python Unit Tests (2)
- GitHub Check: Python Unit Tests (1)
- GitHub Check: Python Unit Tests (3)
- GitHub Check: Python Unit Tests (4)
- GitHub Check: Summary
🔇 Additional comments (4)
erpnext/selling/doctype/sales_order/test_sales_order.py (1)
61-168: Excellent test coverage for item-level closing feature.The test comprehensively validates:
- Prevention of closing fully delivered items
- Reserved quantity updates on closing/reopening
- Prevention of updating closed items
- Pick list creation excluding closed items
- Proper state restoration on reopening
The test structure is clear and follows existing patterns in the file.
erpnext/selling/doctype/sales_order/sales_order.js (3)
999-1010: LGTM! UI buttons properly integrated.The "Close selected items" and "Re-open selected items" buttons are correctly placed in the Status group with appropriate visibility conditions. The implementation follows existing patterns in the file.
1724-1724: Correct logic to exclude closed items from Purchase Order creation.The added condition
&& !d.is_closedproperly filters out closed Sales Order items when determining which items should be included in the Purchase Order dialog. This aligns with the expected behavior where closed items should not be ordered.
1794-1866: Well-implemented dialog for reopening selected items.The method correctly:
- Filters to show only closed items
- Presents a clear dialog for item selection
- Validates user input before making server call
- Provides appropriate feedback on success
The implementation follows established patterns in the codebase.
| items = [] | ||
| so = frappe.get_doc("Sales Order", sales_order) | ||
| if so.is_subcontracted: | ||
| frappe.throw(_("Cannot close items in a subcontracted Sales Order")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If subcontracting order has created then the users should not be able to close the sales order, otherwise they can close the sales order

This adds the feature: #39030
By this feature we now allow the items to be closed at individual level in sales order. It ensures the following things:
no-docs