Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
9e5cd28
docs: add implementation plan for ERPNext Coupon Code sync with gift …
bvisible Jan 12, 2026
d2a64f3
feat(gift-cards): implement phases 1-3 for ERPNext Coupon Code sync
bvisible Jan 12, 2026
ce50590
feat(gift-cards): implement gift card API, splitting logic, and front…
bvisible Jan 12, 2026
d151bfd
fix(gift-card): allow payment completion when discount covers total
bvisible Jan 12, 2026
f8b6c1e
fix(wallet): graceful error handling for loyalty to wallet conversion
bvisible Jan 13, 2026
3eb0177
fix(payment): set discount type to amount when coupon is already applied
bvisible Jan 13, 2026
833ef3d
debug: add more logging to trace discount persistence
bvisible Jan 13, 2026
eb17e12
fix: persist gift card discount directly to database after save
bvisible Jan 13, 2026
5ce2d88
fix: use posa_coupon_code custom field for gift card tracking
bvisible Jan 13, 2026
9c8e32c
refactor: update Coupon Code custom fields for native ERPNext integra…
bvisible Jan 13, 2026
6bd16a1
refactor(gift-cards): remove POS Coupon dependency, use ERPNext Coupo…
bvisible Jan 13, 2026
e34a269
refactor(offers): use ERPNext Coupon Code instead of POS Coupon
bvisible Jan 13, 2026
e12c8df
feat(migration): add patch to migrate POS Coupons to ERPNext Coupon Code
bvisible Jan 13, 2026
b14d306
feat(erpnext): add Create Gift Card button to Coupon Code list
bvisible Jan 13, 2026
5091779
refactor: complete Phase 8 cleanup - remove POS Coupon dependency
bvisible Jan 13, 2026
771595d
test: add comprehensive test suite for ERPNext Coupon Code integration
bvisible Jan 14, 2026
c340326
fix: remove duplicate customer custom field from fixtures
bvisible Jan 14, 2026
a49beaf
fix: use Promotional coupon type instead of Gift Card
bvisible Jan 14, 2026
56c0061
fix: add customer field and use unique hash for coupon names
bvisible Jan 14, 2026
3be1f12
fix: update_coupon signature and pricing rule is_cumulative
bvisible Jan 14, 2026
7520d10
fix(gift-cards): increment used counter on each gift card usage
bvisible Jan 14, 2026
85f05bb
feat(edit-item): allow rate editing for zero-price items (gift cards)
bvisible Jan 14, 2026
5dddc52
feat(pos): auto-open edit dialog for zero-price items (gift cards)
bvisible Jan 14, 2026
fd901f8
fix(cart): allow rate update for zero-price items in cart store
bvisible Jan 14, 2026
afef70f
feat(gift-cards): add get_gift_cards_from_invoice API endpoint
bvisible Jan 14, 2026
703f204
feat(gift-cards): add GiftCardCreatedDialog and debug logging
bvisible Jan 14, 2026
e2253ef
chore: remove debug logging and mark Phase 10 as complete
bvisible Jan 14, 2026
44f1c2a
fix(offers): return discount fields for promotional coupons in valida…
bvisible Jan 14, 2026
c25da85
fix(offers): map discount_type values for frontend compatibility
bvisible Jan 14, 2026
41a70bb
fix: address PR #96 review feedback
bvisible Jan 14, 2026
56877dc
fix: persist gift card amount used for reliable balance tracking
bvisible Jan 14, 2026
7a6fc6d
feat(gift-cards): restore balance on invoice returns (Credit Notes)
bvisible Jan 15, 2026
3ad004a
fix(gift-cards): correct partial return calculation for balance resto…
bvisible Jan 15, 2026
ab6e77e
fix(coupons): include POS Next gift cards in gift card filter
bvisible Jan 15, 2026
310377d
refactor(pos-settings): remove obsolete sync_with_erpnext_coupon field
bvisible Jan 16, 2026
8e06bb9
fix(gift-cards): calculate discount on net total after pricing rules
bvisible Jan 16, 2026
b657e65
Fix: use discount_amount instead of additional_discount_amount for gi…
bvisible Jan 30, 2026
9bc096d
feat(coupons): use native ERPNext coupon_code field on Sales Invoice
bvisible Feb 5, 2026
4ed37fc
Merge remote-tracking branch 'upstream/develop' into feature/erpnext-…
bvisible Feb 18, 2026
efe964a
fix(fixtures): skip coupon_code custom field on ERPNext v16+
bvisible Feb 18, 2026
5846bd0
fix(fixtures): use importlib to locate ERPNext package path
bvisible Feb 18, 2026
3571c41
fix(fixtures): remove coupon_code from JSON, create programmatically …
bvisible Feb 18, 2026
e753512
fix(invoices): re-enforce ignore_pricing_rule after set_missing_values()
bvisible Feb 19, 2026
da05005
fix(gift_cards): look up coupon by doc name OR coupon_code field
bvisible Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ dev-dist/
TODO.md
.claude/settings.json
WhatsApp Image 2026-02-18 at 11.09.47 AM.jpeg
CLAUDE.md
1 change: 1 addition & 0 deletions POS/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare module 'vue' {
CustomerDialog: typeof import('./src/components/sale/CustomerDialog.vue')['default']
DraftInvoicesDialog: typeof import('./src/components/sale/DraftInvoicesDialog.vue')['default']
EditItemDialog: typeof import('./src/components/sale/EditItemDialog.vue')['default']
GiftCardCreatedDialog: typeof import('./src/components/sale/GiftCardCreatedDialog.vue')['default']
InstallAppBadge: typeof import('./src/components/common/InstallAppBadge.vue')['default']
InvoiceCart: typeof import('./src/components/sale/InvoiceCart.vue')['default']
InvoiceDetailDialog: typeof import('./src/components/invoices/InvoiceDetailDialog.vue')['default']
Expand Down
93 changes: 69 additions & 24 deletions POS/src/components/sale/CouponDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
d="M18 9H2v5a2 2 0 002 2h12a2 2 0 002-2V9zM4 13a1 1 0 011-1h1a1 1 0 110 2H5a1 1 0 01-1-1zm5-1a1 1 0 100 2h1a1 1 0 100-2H9z"
clip-rule="evenodd" />
</svg>
<span>{{ __('My Gift Cards ({0})', [giftCards.length]) }}</span>
<span>{{ __('Available Gift Cards ({0})', [giftCards.length]) }}</span>
</div>
</label>
<div class="flex flex-col gap-2 max-h-60 overflow-y-auto pe-1">
Expand All @@ -68,15 +68,26 @@
<h4 class="text-sm font-bold text-gray-900">
{{ card.coupon_code }}
</h4>
<p class="text-xs text-gray-600">{{ card.coupon_name }}</p>
<p class="text-xs text-gray-600">
{{ card.coupon_name }}
<span v-if="!card.customer" class="text-purple-500">({{ __('Anonymous') }})</span>
</p>
</div>
</div>
</div>
<svg class="w-5 h-5 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
<div class="flex items-center gap-2">
<div class="text-end">
<p class="text-sm font-bold text-purple-700">
{{ formatCurrency(card.balance || card.gift_card_amount || card.discount_amount) }}
</p>
<p class="text-xs text-gray-500">{{ __('Balance') }}</p>
</div>
<svg class="w-5 h-5 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
</div>
</div>
</div>
</div>
Expand All @@ -93,12 +104,12 @@
</svg>
</div>
<h4 class="text-sm font-bold text-green-900">
{{ __('Coupon Applied Successfully!') }}
{{ appliedDiscount.isGiftCard ? __('Gift Card Applied!') : __('Coupon Applied Successfully!') }}
</h4>
</div>
<div class="bg-white rounded-lg p-3">
<div class="flex justify-between items-center mb-2">
<span class="text-xs text-gray-600">{{ __('Coupon Code') }}</span>
<span class="text-xs text-gray-600">{{ appliedDiscount.isGiftCard ? __('Gift Card Code') : __('Coupon Code') }}</span>
<span class="text-sm font-bold text-gray-900">{{ appliedDiscount.code }}</span>
</div>
<div class="flex justify-between items-center">
Expand All @@ -107,6 +118,17 @@
-{{ formatCurrency(appliedDiscount.amount) }}
</span>
</div>
<!-- Gift Card Balance Info -->
<div v-if="appliedDiscount.isGiftCard && appliedDiscount.availableBalance" class="mt-2 pt-2 border-t border-gray-200">
<div class="flex justify-between items-center text-xs">
<span class="text-gray-500">{{ __('Card Balance') }}</span>
<span class="text-gray-700">{{ formatCurrency(appliedDiscount.availableBalance) }}</span>
</div>
<div v-if="appliedDiscount.remainingBalance > 0" class="flex justify-between items-center text-xs mt-1">
<span class="text-purple-600">{{ __('Remaining after purchase') }}</span>
<span class="text-purple-700 font-medium">{{ formatCurrency(appliedDiscount.remainingBalance) }}</span>
</div>
</div>
</div>
</div>

Expand Down Expand Up @@ -160,7 +182,12 @@ const props = defineProps({
subtotal: {
type: Number,
required: true,
note: __("Cart subtotal BEFORE tax - used for discount calculations"),
note: __("Cart subtotal BEFORE tax - used for regular coupon discount calculations"),
},
netTotal: {
type: Number,
required: true,
note: __("Net total AFTER pricing rules but BEFORE additional discount - used for gift card calculations"),
},
items: Array,
posProfile: String,
Expand Down Expand Up @@ -282,6 +309,7 @@ async function applyCoupon() {
}

const coupon = validationData.coupon
const isGiftCard = coupon.coupon_type === "Gift Card" || coupon.is_gift_card

// Check minimum amount (on subtotal before tax)
if (coupon.min_amount && props.subtotal < coupon.min_amount) {
Expand All @@ -290,22 +318,36 @@ async function applyCoupon() {
return
}

// Calculate discount on subtotal (before tax) using centralized helper
// Transform server coupon format to discount object format
const discountObj = {
percentage: coupon.discount_type === "Percentage" ? coupon.discount_percentage : 0,
amount: coupon.discount_type === "Amount" ? coupon.discount_amount : 0,
}

let discountAmount = calculateDiscountAmount(discountObj, props.subtotal)

// Apply maximum discount limit if specified
if (coupon.max_amount && discountAmount > coupon.max_amount) {
discountAmount = coupon.max_amount
let discountAmount = 0
let availableBalance = 0
let remainingBalance = 0

if (isGiftCard) {
// Gift card: use balance as discount, cap at netTotal (after pricing rules)
// This is critical: gift card discount must be based on the ACTUAL amount to pay
// after pricing rules have been applied, not the original subtotal
availableBalance = coupon.balance || coupon.gift_card_amount || coupon.discount_amount || 0
discountAmount = Math.min(availableBalance, props.netTotal)
remainingBalance = availableBalance - discountAmount
} else {
// Regular coupon: calculate based on discount type (uses original subtotal)
const discountObj = {
percentage: coupon.discount_type === "Percentage" ? coupon.discount_percentage : 0,
amount: coupon.discount_type === "Amount" ? coupon.discount_amount : 0,
}
discountAmount = calculateDiscountAmount(discountObj, props.subtotal)

// Apply maximum discount limit if specified
if (coupon.max_amount && discountAmount > coupon.max_amount) {
discountAmount = coupon.max_amount
}
}

// Clamp discount to subtotal to prevent negative totals
discountAmount = Math.min(discountAmount, props.subtotal)
// Clamp discount to the appropriate total based on coupon type
// Gift cards: clamp to netTotal (after pricing rules)
// Regular coupons: clamp to subtotal (original prices)
const maxDiscount = isGiftCard ? props.netTotal : props.subtotal
discountAmount = Math.min(discountAmount, maxDiscount)

appliedDiscount.value = {
name: coupon.coupon_name || coupon.coupon_code,
Expand All @@ -315,6 +357,9 @@ async function applyCoupon() {
type: coupon.discount_type,
coupon: coupon,
apply_on: coupon.apply_on,
isGiftCard: isGiftCard,
availableBalance: isGiftCard ? availableBalance : null,
remainingBalance: isGiftCard ? remainingBalance : null,
}

emit("discount-applied", appliedDiscount.value)
Expand Down
15 changes: 13 additions & 2 deletions POS/src/components/sale/CouponManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
{{ coupon.coupon_code }}
</p>
<Badge
v-if="coupon.coupon_type === 'Gift Card'"
v-if="coupon.coupon_type === 'Gift Card' || coupon.pos_next_gift_card"
variant="subtle"
theme="purple"
size="sm"
Expand Down Expand Up @@ -648,8 +648,19 @@ const filteredCoupons = computed(() => {
}

// Filter by type
// Note: POS Next gift cards have coupon_type='Promotional' but pos_next_gift_card=1
if (filterType.value !== "all") {
filtered = filtered.filter((c) => c.coupon_type === filterType.value)
filtered = filtered.filter((c) => {
if (filterType.value === "Gift Card") {
// Include both ERPNext gift cards (coupon_type) and POS Next gift cards (pos_next_gift_card)
return c.coupon_type === "Gift Card" || c.pos_next_gift_card === 1
}
if (filterType.value === "Promotional") {
// Only show promotional coupons that are NOT gift cards
return c.coupon_type === "Promotional" && !c.pos_next_gift_card
}
return c.coupon_type === filterType.value
})
}

return filtered
Expand Down
14 changes: 11 additions & 3 deletions POS/src/components/sale/EditItemDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,18 @@ const hasPricingRules = computed(() => {
return Boolean(localItem.value.pricing_rules) && localItem.value.pricing_rules.length > 0
})

// Rate editing is allowed only if:
// 1. POS Settings allows rate editing AND
// 2. Item does NOT have pricing rules (promotional offers) applied
// Zero-price items (e.g., gift cards) always allow rate editing
const isZeroPriceItem = computed(() => {
if (!localItem.value) return false
const originalRate = localItem.value.price_list_rate || localItem.value.rate || 0
return originalRate === 0
})

// Rate editing is allowed if:
// 1. Item has zero price (gift cards with custom value), OR
// 2. POS Settings allows rate editing AND item has no pricing rules applied
const canEditRate = computed(() => {
if (isZeroPriceItem.value) return true
return settingsStore.allowUserToEditRate && !hasPricingRules.value
})

Expand Down
Loading
Loading