Webshop-14-Hypothesis-Discount-Deadline-Visibility
14 - Hypothesis: Making Discounts and Deadlines Visible
Parent: [Webshop-Index Webshop Index] Previous: [Webshop-13-Discount-Visibility-and-Urgency 13 - Discount Visibility & Urgency] Source: Comfac Webshop Wiki - Chapter 14
Prerequisite: Seamless Baseline First
This hypothesis is NOT to be executed until the comfac-webshop fork runs seamlessly as a drop-in replacement on a production-like ERPNext instance.
Execution order:
1. Phase 0 (Staging Sandbox): Clone the production ERPNext instance, install comfac-webshop, validate it works identically to the existing webshop. No feature changes. Pass all validation checks.
2. Phase 1 (Experiment Clone): Once Phase 0 is proven stable, clone THAT successful staging instance into a second sandbox. This is where we implement and test the hypothesis below.
3. Phase 2 (Iterate): Test with sample products, pricing rules, coupons, and deadline-bearing offers on the experiment clone. Validate every scenario. Only when everything passes do we merge back to the main fork.
Do not skip Phase 0.
Core Thesis
All the data we need already exists on the Quotation document. ERPNext's pricing engine populates discount fields on every cart save. We do not need to change the backend calculation logic. We need to:
1. Surface existing hidden fields in the cart templates 2. Enrich the decorator to pass pricing rule metadata (deadlines, titles) to the frontend 3. Add summation logic in the payment summary template 4. Style it so customers immediately see value and urgency
Part 1: The Fields We Will Target
Per-Item Fields (Quotation Item - already populated)
| Field | Type | What It Contains | Currently Used? |
|---------|--------|------------------------|----------------------|
price_list_rate
|
Currency | Original unit price from the Price List (before any discount) | NO |
rate
|
Currency | Final unit price (after discount applied) | YES |
discount_percentage
|
Percent | The % discount applied by the Pricing Rule | NO |
discount_amount
|
Currency | Per-unit discount in currency | NO |
amount
|
Currency | rate * qty = line total after discount
|
YES |
is_free_item
|
Check | True if item was added by a "Free Item" pricing rule | YES |
pricing_rules
|
Small Text | JSON string of Pricing Rule document names | NO |
Document-Level Fields (Quotation - already populated)
| Field | Type | What It Contains | Currently Used? |
|---------|--------|------------------------|----------------------|
total
|
Currency | Sum of all amount values (item totals before tax)
|
YES |
net_total
|
Currency | Total after additional discounts | YES |
grand_total
|
Currency | Final total with taxes | YES |
discount_amount
|
Currency | Additional discount on the overall total | NO |
additional_discount_percentage
|
Percent | % discount on total | NO |
coupon_code
|
Link | Applied Coupon Code document name | PARTIAL |
Pricing Rule Fields (need to be looked up from pricing_rules JSON)
| Field | Type | What It Contains | Purpose for Us |
|---------|--------|------------------------|---------------------|
title
|
Data | Human-readable name (e.g., "Spring GPU Sale") | Display offer name |
valid_from
|
Date | Start date of the offer | Show when offer started |
valid_upto
|
Date | End date / DEADLINE of the offer | URGENCY DISPLAY |
discount_percentage
|
Percent | The discount % this rule applies | Confirm which rule gave what discount |
Part 2: The Summation System
Improved Summation Formula
python
Per-item savings:
if item.is_free_item:
item_savings = item.price_list_rate * item.qty (entire value is free)
elif item.price_list_rate and item.price_list_rate > item.rate:
item_savings = (item.price_list_rate - item.rate) * item.qty
else:
item_savings = 0
total_item_savings = sum(item_savings for all items)
Additional discount savings:
doc_discount = doc.discount_amount or 0
Grand savings:
total_savings = total_item_savings + doc_discount
Part 3: The Deadline System
Data Chain for Deadlines
Pricing Rule (has valid_upto date)
↓ applied during set_price_list_and_item_details()
Quotation Item.pricing_rules = '["PRLE-0001", "PRLE-0002"]' (JSON string)
↓ we parse this in the decorator
decorate_quotation_doc() enriches each item with:
d.offer_details = [
{"title": "Spring Sale", "valid_upto": "2026-03-01", "days_left": 10},
...
]
↓ template renders deadline badges
Backend Helper
python
def get_offer_details_for_item(item):
"""Parse pricing_rules JSON and fetch deadline/title from each Pricing Rule."""
import json
offers = []
if not item.pricing_rules:
return offers
try:
rule_names = json.loads(item.pricing_rules)
except (json.JSONDecodeError, TypeError):
return offers
for rule_name in rule_names:
rule = frappe.get_cached_doc("Pricing Rule", rule_name)
offer = {
"title": rule.title or rule.name,
"valid_upto": rule.valid_upto,
"days_left": None,
}
if rule.valid_upto:
from frappe.utils import date_diff, today
offer["days_left"] = date_diff(rule.valid_upto, today())
offers.append(offer)
return offers
Part 4: File-by-File Change Map
File 1: webshop/webshop/shopping_cart/cart.py
Function: decorate_quotation_doc()
Change: Add offer_details list to each item. Also compute and add doc.original_total, doc.total_savings, doc.has_any_discount.
File 2: templates/includes/cart/cart_items.html
Macro: item_subtotal
Change: Add strike-through original price when discounted, discount percentage badge, "You save" amount, and offer deadline badges.
File 3: templates/includes/cart/cart_payment_summary.html
Change: Add original subtotal (struck through) if discounts exist, savings row, and additional discount row.
File 4: templates/includes/cart/cart_items_total.html
Change: Show original total (struck through) if doc.has_any_discount.
File 5: public/scss/webshop_cart.scss
Change: Add styles for discount badges, strikethrough, and urgency indicators.
Part 5: What We Do NOT Need to Change
| Component | Why No Change Needed |
set_price_list_and_rate()
|
Already resets and repopulates fields on every save |
apply_cart_settings()
|
Already calls calculate_taxes_and_totals() which does all the math
|
update_cart()
|
Already re-renders all three template fragments on AJAX update |
| ERPNext Pricing Rules | Standard feature - we just read the results |
| ERPNext Quotation DocType | All fields we need already exist |
shopping_cart.js
|
AJAX update already replaces affected DOM elements |
Part 6: Risk Assessment
| Risk | Likelihood | Mitigation |
price_list_rate is 0 or null
|
Medium | Guard with {% if item.price_list_rate and item.price_list_rate > 0 %}
|
pricing_rules JSON malformed
|
Low | Wrap in try/except, return empty list |
| Pricing Rule deleted but referenced | Low | Use frappe.db.exists() check
|
| Performance hit from lookups | Low | get_cached_doc() uses Frappe cache
|
discount_percentage is 0 but discount_amount is set
|
Medium | Use price_list_rate - rate comparison
|
Free items with no price_list_rate
|
Medium | Skip savings calculation for these |
| Multiple pricing rules per item | Medium | Show all offer details; sum net savings |
Part 7: Verification Checklist
| # | Scenario | Check |
| 1 | Item with percentage discount | Shows $100 → $80 (-20%), "You save $20" |
| 2 | Item with amount discount | Shows $100 → $85, "You save $15" |
| 3 | Item with no discount | Shows $100 only, no strikethrough |
| 4 | Free item from pricing rule | Shows "FREE" badge, value in savings |
| 5 | Coupon code applied | Savings row reflects coupon |
| 6 | Offer expiring in 3 days | Red text "Only 3 days left!" |
| 7 | Offer expiring in 10 days | Warning text "Offer ends March 1, 2026" |
| 8 | Offer with no deadline | No deadline shown, discount visible |
| 9 | Multiple offers on one item | All offer titles/deadlines listed |
| 10 | Additional discount on total | "Additional Discount -$50" row |
| 11 | Cart with mixed items | Only discounted items show strikethrough |
| 12 | AJAX qty update | All discount info re-renders |
| 13 | Mobile view | Discount badges readable |
| 14 | price_list_rate is null
|
Falls back showing rate only |
Summary
price_list_rate, discount_percentage, pricing_rules, is_free_item) + 2 hidden Quotation fields (discount_amount, additional_discount_percentage) that ERPNext already populates, and displaying them in 4 template files + 1 Python decorator.
What we're NOT doing: No new DocTypes, no pricing engine changes, no database migrations, no JS logic changes. The AJAX update cycle already re-renders all affected templates.
Files touched: 5 total (1 Python, 3 HTML templates, 1 SCSS)
Navigation: [Webshop-Index Webshop Index] | [Webshop-13-Discount-Visibility-and-Urgency Previous: 13 - Discount Visibility] | [Webshop-Index Webshop Index]