feat: Support for Alternative Items in Quotation (#33874)

* feat: Filter out alternative item rows in taxes and totals for Quotation

- Added a Quotation Item field `is_alternative_item`
- Use filtered rows for taxes and totals computation

(cherry picked from commit 91982d1e4f)

# Conflicts:
#	erpnext/selling/doctype/quotation_item/quotation_item.json

* feat: Consider filtered items table in JS for totals computation

- Set `_items` as filtered rows if quotation else the entire table. Set at entry point of JS API
- Use `_items` instead of `items` to compute taxes and charges. Exclude alternative item rows

(cherry picked from commit f19eadab9a)

* feat: Dialog to select alternative item before creating Sales order

- Users can leave the row blank in the dialog if original item is to be used
- Else users can select an alternative item against an original item
- In the document, users must check `Is Alternative Item` if needed and also specify which item it is an altenrative to since there are no documented mappings

(cherry picked from commit cef7dfd0b4)

# Conflicts:
#	erpnext/selling/doctype/quotation/quotation.js
#	erpnext/selling/doctype/quotation_item/quotation_item.json

* feat: Filter rows to be mapped on server side mapping function

- Pass dialog selections to `make_sales_order`
- Map either original item or its alternative depending on mapping
- Only qty check for simple rows (without alternatives and not an alternative itself)

(cherry picked from commit 94cacb60de)

* chore: Validate 'alternative_to' field values, must be a valid non-alterntaive item from table

(cherry picked from commit fa9b327501)

* fix: Iterate over list instead of map's output and formatting

(cherry picked from commit ece6358e60)

* fix: Consider only ordered alternative/original item for Quotation status

- The original and its alternatives make a set of items where one is chosen
- While setting order status of Quotation, check if the chosen item from the set is fully ordered or not
- Filter out unselected items from the set
- Create a map containing the set of items and if they were ordered or not for ease of grouping
- The simple items will work as it used to

(cherry picked from commit b3fe7c6dad)

* chore: Code simplification

- Map is not required, avoid filter multiple times, use single loop instead
- Better variable name
- Reduce LOC

(cherry picked from commit 03321f5f13)

* refactor: Order based alternative items mapping

- Alternatives must be followed by a non-alternative item row
- On submit, store non-alternative rows in hidden checkbox to avoid recomputation
- Check for valid/mappable rows by row name
- UI: Select from table rows.Add single row for original/alternative item in dialog
- UI: Indicator for alternative items in dialog grid
- UI: Indicator legend and description of table
- DB: Added check field 'Has Alternative Item' not to be confused with 'Has Alternative' in Mfg

(cherry picked from commit db2076db69)

# Conflicts:
#	erpnext/selling/doctype/quotation_item/quotation_item.json

* test: Alternative items in Quotation

- Taxes and totals, mapping, back updation

(cherry picked from commit 74fab53e28)

* fix: Use block variable

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit 3c96791d52)

* fix: Handle `Get Items From` in Sales Order

- Map all non alternatives from Quotation to SO if no selected items
- Show disclaimer mentioning that Qtns with alternatives must be mapped to SO from the Qtn form

(cherry picked from commit 19456127cf)

* fix: Map only non alternative items from Quotation in Sales Invoice

- Since there's no item selection, only Quotation selection :/

(cherry picked from commit 6b789e2f04)

* fix: Merge conflicts

---------

Co-authored-by: marination <maricadsouza221197@gmail.com>
This commit is contained in:
mergify[bot]
2023-03-16 11:50:19 +05:30
committed by GitHub
parent 68f9863ae5
commit 9f7da21c93
7 changed files with 405 additions and 42 deletions

View File

@@ -24,11 +24,19 @@ class calculate_taxes_and_totals(object):
def __init__(self, doc: Document):
self.doc = doc
frappe.flags.round_off_applicable_accounts = []
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
def filter_rows(self):
"""Exclude rows, that do not fulfill the filter criteria, from totals computation."""
items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
return items
def calculate(self):
if not len(self.doc.get("items")):
if not len(self._items):
return
self.discount_amount_applied = False
@@ -70,7 +78,7 @@ class calculate_taxes_and_totals(object):
if hasattr(self.doc, "tax_withholding_net_total"):
sum_net_amount = 0
sum_base_net_amount = 0
for item in self.doc.get("items"):
for item in self._items:
if hasattr(item, "apply_tds") and item.apply_tds:
sum_net_amount += item.net_amount
sum_base_net_amount += item.base_net_amount
@@ -79,7 +87,7 @@ class calculate_taxes_and_totals(object):
self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self):
for item in self.doc.get("items"):
for item in self._items:
if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
@@ -137,7 +145,7 @@ class calculate_taxes_and_totals(object):
return
if not self.discount_amount_applied:
for item in self.doc.get("items"):
for item in self._items:
self.doc.round_floats_in(item)
if item.discount_percentage == 100:
@@ -236,7 +244,7 @@ class calculate_taxes_and_totals(object):
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return
for item in self.doc.get("items"):
for item in self._items:
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0
@@ -317,7 +325,7 @@ class calculate_taxes_and_totals(object):
self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
for item in self.doc.get("items"):
for item in self._items:
self.doc.total += item.amount
self.doc.total_qty += item.qty
self.doc.base_total += item.base_amount
@@ -354,7 +362,7 @@ class calculate_taxes_and_totals(object):
]
)
for n, item in enumerate(self.doc.get("items")):
for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
for i, tax in enumerate(self.doc.get("taxes")):
# tax_amount represents the amount of tax for the current step
@@ -363,7 +371,7 @@ class calculate_taxes_and_totals(object):
# Adjust divisional loss to the last item
if tax.charge_type == "Actual":
actual_tax_dict[tax.idx] -= current_tax_amount
if n == len(self.doc.get("items")) - 1:
if n == len(self._items) - 1:
current_tax_amount += actual_tax_dict[tax.idx]
# accumulate tax amount into tax.tax_amount
@@ -391,7 +399,7 @@ class calculate_taxes_and_totals(object):
)
# set precision in the last item iteration
if n == len(self.doc.get("items")) - 1:
if n == len(self._items) - 1:
self.round_off_totals(tax)
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
@@ -570,7 +578,7 @@ class calculate_taxes_and_totals(object):
def calculate_total_net_weight(self):
if self.doc.meta.get_field("total_net_weight"):
self.doc.total_net_weight = 0.0
for d in self.doc.items:
for d in self._items:
if d.total_weight:
self.doc.total_net_weight += d.total_weight
@@ -630,7 +638,7 @@ class calculate_taxes_and_totals(object):
if total_for_discount_amount:
# calculate item amount after Discount Amount
for i, item in enumerate(self.doc.get("items")):
for i, item in enumerate(self._items):
distributed_amount = (
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
)
@@ -643,7 +651,7 @@ class calculate_taxes_and_totals(object):
self.doc.apply_discount_on == "Net Total"
or not taxes
or total_for_discount_amount == self.doc.net_total
) and i == len(self.doc.get("items")) - 1:
) and i == len(self._items) - 1:
discount_amount_loss = flt(
self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total")
)