Skip to content

fix(buying): decouple accepted and rejected quantities in purchase returns#51505

Closed
Aakash1o1 wants to merge 2 commits intofrappe:developfrom
Aakash1o1:develop
Closed

fix(buying): decouple accepted and rejected quantities in purchase returns#51505
Aakash1o1 wants to merge 2 commits intofrappe:developfrom
Aakash1o1:develop

Conversation

@Aakash1o1
Copy link

Problem

Currently, the system validation for Purchase Returns aggregates all previously returned items into a single bucket, regardless of whether they were returned from the Accepted warehouse or the Rejected warehouse.

This leads to a validation error in mixed scenarios:

  1. Scenario: Purchase Receipt has 10 Accepted and 5 Rejected items.
  2. Action: User returns 3 Rejected items.
  3. Issue: The system subtracts these 3 items from the "Accepted" balance (10 - 3 = 7).
  4. Result: When the user later tries to return the 10 Accepted items, the system throws an error: "Cannot return more than 7".

The Fix

This PR decouples the return logic so that Accepted and Rejected returns are tracked and validated separately.

  1. SQL Aggregation (get_already_returned_items):

    • Updated the query to split returned quantities into two buckets: qty (Accepted) and rejected_qty_returned (Rejected).
    • Added frappe.db.has_column check to ensure backward compatibility with Doctypes that lack the rejected warehouse field (e.g., Purchase Invoice).
    • Added IS NULL handling to ensure legacy data is treated as "Accepted" returns.
  2. Validation Logic (validate_quantity):

    • Refactored the validation loop to compare the current return against its specific historical bucket.
    • Accepted Return: Validates against Original Accepted Qty - Previous Accepted Returns.
    • Rejected Return: Validates against Original Rejected Qty - Previous Rejected Returns.
    • Unit Consistency: Fixed logic to ensure stock_qty is validated against Stock UOM values, while qty is validated against Item UOM values, preventing unit mismatch errors.

Tests Added

  • test_return_rejected_does_not_block_accepted_return (Purchase Receipt): Verifies that returning rejected items does not reduce the returnable balance of accepted items.
  • test_purchase_return_fallback_for_missing_column (Purchase Invoice): Verifies that standard returns continue to work on Doctypes where the rejected warehouse column does not exist (crash prevention).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 5, 2026

📝 Walkthrough

Walkthrough

Adds handling to distinguish rejected vs accepted returned quantities when creating purchase returns. Updates sales_and_purchase_return.py to select returned quantity keys dynamically (rejected_qty_returned / rejected_stock_qty_returned vs standard fields), adjusts reference/current quantity calculations with conversion factors, and enhances get_already_returned_items to aggregate rejected and accepted returns only when the doctype supports the return_qty_from_rejected_warehouse flag. Adds integration tests: one for Purchase Invoice fallback when the rejected column is missing, and one for Purchase Receipt ensuring rejected returns do not block accepted returns.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

buying, stock, needs-tests

Suggested reviewers

  • ruthra-kumar
  • rohitwaghchaure
  • mihir-kandoi

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main fix: decoupling accepted and rejected quantities in purchase returns, which is the core change across all modified files.
Description check ✅ Passed The description comprehensively explains the problem, the fix, and includes details about SQL aggregation, validation logic changes, and tests added, all directly related to the changeset.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8135f4 and 11296b5.

📒 Files selected for processing (3)
  • erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
  • erpnext/controllers/sales_and_purchase_return.py
  • erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
  • erpnext/controllers/sales_and_purchase_return.py
  • erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
⏰ 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

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Fix all issues with AI Agents 🤖
In @erpnext/controllers/sales_and_purchase_return.py:
- Around line 218-224: The `stock_qty` branch inside the else is unreachable
because `column == "stock_qty" and not
args.get("return_qty_from_rejected_warehouse")` is already handled earlier;
remove the dead code (the block that multiplies reference_qty/current_stock_qty
by ref.get("conversion_factor", 1.0)/args.get("conversion_factor", 1.0)) or, if
conversion is required for the non-rejected `stock_qty` path, move that
conversion into the earlier `if column == "stock_qty" and not
args.get("return_qty_from_rejected_warehouse")` branch so that reference_qty and
current_stock_qty are multiplied by ref.get("conversion_factor", 1.0) and
args.get("conversion_factor", 1.0) respectively; update only the logic using the
variables column, reference_qty, current_stock_qty, ref.get("conversion_factor")
and args.get("conversion_factor") and remove the unreachable block referencing
return_qty_from_rejected_warehouse.
🧹 Nitpick comments (1)
erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py (1)

2968-2985: Test name/docstring claim a “missing column” scenario that isn’t currently exercised

This test does validate that a normal Purchase Invoice return goes through without crashing, but it never simulates the case where Purchase Invoice Item lacks the return_qty_from_rejected_warehouse column, which is what the docstring and PR description mention. As a result, the new fallback branch in get_already_returned_items (when frappe.db.has_column(...) is false) is still untested.

To actually cover that code path, consider temporarily monkey‑patching frappe.db.has_column inside this test so that, for "Purchase Invoice Item" and "return_qty_from_rejected_warehouse", it returns False, and then restoring it in a finally block. That will exercise the compatibility logic without touching the real schema.

You might also optionally assert against pi_return (the return document) or just drop the status assertion if the main goal is strictly “does not crash”, but that’s secondary to the coverage gap above.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72aa27a and c8135f4.

📒 Files selected for processing (3)
  • erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
  • erpnext/controllers/sales_and_purchase_return.py
  • erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
🧰 Additional context used
🧠 Learnings (1)
📚 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/accounts/doctype/purchase_invoice/test_purchase_invoice.py
  • erpnext/controllers/sales_and_purchase_return.py
  • erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
🧬 Code graph analysis (2)
erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py (1)
erpnext/controllers/sales_and_purchase_return.py (1)
  • make_return_doc (394-707)
erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py (1)
erpnext/controllers/sales_and_purchase_return.py (1)
  • make_return_doc (394-707)
⏰ 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 (4)
erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py (1)

4723-4763: Good regression coverage for decoupling accepted vs rejected return quantities

This test cleanly reproduces the mixed accepted/rejected scenario and verifies that a prior rejected‑qty return no longer reduces the allowed accepted return quantity. The use of return_against_rejected_qty=True and a subsequent standard return hits exactly the paths changed in the controller. Looks solid.

erpnext/controllers/sales_and_purchase_return.py (3)

193-216: Logic for separating accepted and rejected return buckets looks correct.

The bucket selection properly distinguishes between accepted and rejected quantities based on the return_qty_from_rejected_warehouse flag. The conversion factor is correctly applied when converting rejected_qty to stock units.

Minor: Lines 217 and 221 have trailing whitespace.


287-305: Good defensive check for backward compatibility.

The frappe.db.has_column check ensures this code works with doctypes that don't have the return_qty_from_rejected_warehouse field (like older Purchase Invoice implementations). The conditional SQL aggregation correctly splits quantities into accepted and rejected buckets, treating NULL values as accepted returns for legacy data compatibility.


340-341: LGTM!

The new fields are correctly added to the returned items dictionary, enabling the validation logic to check accepted and rejected returns separately.

@rohitwaghchaure rohitwaghchaure self-assigned this Jan 5, 2026
@mihir-kandoi
Copy link
Collaborator

closing in favor of #51514

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants