diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index c205bb11..b0ae39ec 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -10,7 +10,6 @@ declare module 'vue' {
export interface GlobalComponents {
Annoucements: typeof import('./src/components/Annoucements.vue')['default']
AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.vue')['default']
- AppHeader: typeof import('./src/components/AppHeader.vue')['default']
Apps: typeof import('./src/components/Apps.vue')['default']
AppSidebar: typeof import('./src/components/AppSidebar.vue')['default']
AssessmentModal: typeof import('./src/components/Modals/AssessmentModal.vue')['default']
@@ -74,7 +73,6 @@ declare module 'vue' {
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default']
JobCard: typeof import('./src/components/JobCard.vue')['default']
- LayoutHeader: typeof import('./src/components/LayoutHeader.vue')['default']
LessonContent: typeof import('./src/components/LessonContent.vue')['default']
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
Link: typeof import('./src/components/Controls/Link.vue')['default']
diff --git a/frontend/src/components/Settings/CouponDetails.vue b/frontend/src/components/Settings/CouponDetails.vue
index 3e3fff36..b6e0b382 100644
--- a/frontend/src/components/Settings/CouponDetails.vue
+++ b/frontend/src/components/Settings/CouponDetails.vue
@@ -24,15 +24,28 @@
:label="__('Discount Amount')"
type="number"
/>
-
-
+
+
-
{{ __('Applicable Items (optional)') }}
+
{{ __('Select Courses/Batches') }}*
-
-
-
(row.reference_name = opt)" />
+
+
+ (row.reference_name = opt)" />
@@ -135,7 +148,6 @@ function save() {
emit('saved')
},
onError(err) {
- console.log('Save error:', err)
toast.error(err.messages?.[0] || err.message || err)
}
})
@@ -147,7 +159,6 @@ function save() {
emit('saved')
},
onError(err) {
- console.log('Insert error:', err)
toast.error(err.messages?.[0] || err.message || err)
}
})
diff --git a/frontend/src/components/Settings/Coupons.vue b/frontend/src/components/Settings/Coupons.vue
index 078f4846..1f2611c5 100644
--- a/frontend/src/components/Settings/Coupons.vue
+++ b/frontend/src/components/Settings/Coupons.vue
@@ -27,6 +27,7 @@
{{ __('Expires On') }} |
{{ __('Usage') }} |
{{ __('Active') }} |
+ |
@@ -43,6 +44,13 @@
{{ __('Enabled') }}
{{ __('Disabled') }}
+
+
+ |
@@ -52,11 +60,14 @@
diff --git a/frontend/src/components/Settings/TransactionDetails.vue b/frontend/src/components/Settings/TransactionDetails.vue
index a3e38933..ac5b8e1d 100644
--- a/frontend/src/components/Settings/TransactionDetails.vue
+++ b/frontend/src/components/Settings/TransactionDetails.vue
@@ -67,43 +67,32 @@
/>
-
-
- {{ __('Coupon (if applied)') }}
-
-
-
-
-
-
-
+
+
+
+
@@ -114,6 +103,13 @@
v-model="transactionData.payment_id"
/>
+
+
+
+
diff --git a/frontend/src/components/Settings/Transactions.vue b/frontend/src/components/Settings/Transactions.vue
index 0c6904fb..cb713023 100644
--- a/frontend/src/components/Settings/Transactions.vue
+++ b/frontend/src/components/Settings/Transactions.vue
@@ -73,7 +73,7 @@
:disabled="true"
/>
- {{ getCurrencySymbol(row['currency']) }} {{ row[column.key] }}
+ {{ getCurrencySymbol(row['currency']) }} {{ row['total_amount'] }}
{{ row[column.key] }}
@@ -153,6 +153,7 @@ const transactions = createListResource({
'payment_for_certificate',
'currency',
'amount',
+ 'total_amount',
'order_id',
'payment_id',
'gstin',
diff --git a/frontend/src/pages/Billing.vue b/frontend/src/pages/Billing.vue
index cad12d4f..bf7ff3cd 100644
--- a/frontend/src/pages/Billing.vue
+++ b/frontend/src/pages/Billing.vue
@@ -35,6 +35,10 @@
{{ orderSummary.data.original_amount_formatted }}
+
+
{{ __('Discount') }}
+
-{{ orderSummary.data.discount_amount_formatted }}
+
-
-
{{ __('Discount') }}
-
-{{ orderSummary.data.discount_amount_formatted }}
-
@@ -63,9 +63,9 @@
{{ __('Coupon') }}
-
-
-
+
+
+
@@ -174,6 +174,7 @@ import { reactive, inject, onMounted, computed, ref } from 'vue'
import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
+import { X } from 'lucide-vue-next'
const user = inject('$user')
const { brand } = sessionStore()
@@ -239,6 +240,7 @@ const applyCoupon = createResource({
},
onSuccess(data) {
orderSummary.data = data
+ console.log('orderSummary.data - ', orderSummary.data)
appliedCoupon.value = couponCode.value
toast.success(__('Coupon applied'))
},
@@ -266,18 +268,20 @@ const setBillingDetails = (data) => {
const paymentLink = createResource({
url: 'lms.lms.payments.get_payment_link',
makeParams(values) {
- return {
+ let data={
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
docname: props.name,
title: orderSummary.data.title,
amount: orderSummary.data.original_amount,
- total_amount: orderSummary.data.amount,
+ discount_amount: orderSummary.data.discount_amount || 0,
+ gst_amount: orderSummary.data.gst_applied || 0,
currency: orderSummary.data.currency,
address: billingDetails,
redirect_to: redirectTo.value,
payment_for_certificate: props.type == 'certificate',
coupon_code: appliedCoupon.value,
}
+ return data
},
})
@@ -308,6 +312,7 @@ function applyCouponCode() {
function removeCoupon() {
appliedCoupon.value = null
+ couponCode.value = ''
orderSummary.reload()
}
diff --git a/lms/lms/doctype/lms_coupon/lms_coupon.py b/lms/lms/doctype/lms_coupon/lms_coupon.py
index fc8f33b8..abb0ee55 100644
--- a/lms/lms/doctype/lms_coupon/lms_coupon.py
+++ b/lms/lms/doctype/lms_coupon/lms_coupon.py
@@ -9,14 +9,15 @@ from frappe.utils import nowdate
class LMSCoupon(Document):
def validate(self):
- # Normalize code to uppercase and strip spaces
if self.code:
self.code = self.code.strip().upper()
if not self.code:
- frappe.throw(_("Coupon code is required."))
+ frappe.throw(_("Coupon code is required"))
+
+ if len(self.code) < 6:
+ frappe.throw(_("Coupon code must be atleast 6 characters"))
- # Ensure uniqueness of code (case-insensitive)
if self.name:
existing = frappe.db.exists(
"LMS Coupon",
@@ -27,30 +28,49 @@ class LMSCoupon(Document):
)
else:
existing = frappe.db.exists("LMS Coupon", {"code": self.code})
+
if existing:
- frappe.throw(_("Coupon code already exists."))
+ frappe.throw(_("Coupon code is already taken. Use a different one"))
if not self.discount_type:
- frappe.throw(_("Discount type is required."))
+ frappe.throw(_("Discount type is required"))
if self.discount_type == "Percent":
- if self.percent_off is None:
- frappe.throw(_("Percent Off is required for Percent discount type."))
- if not (0 < float(self.percent_off) <= 100):
- frappe.throw(_("Percent Off must be between 1 and 100."))
- # Clear the other field to avoid confusion
+ if not self.percent_off or self.percent_off == "":
+ frappe.throw(_("Discount percentage is required"))
+ try:
+ percent_value = float(self.percent_off)
+ if not (0 < percent_value <= 100):
+ frappe.throw(_("Discount percentage must be between 1 and 100"))
+ except (ValueError, TypeError):
+ frappe.throw(_("Discount percentage must be a valid number"))
self.amount_off = None
if self.discount_type == "Amount":
- if self.amount_off is None:
- frappe.throw(_("Amount Off is required for Amount discount type."))
- if float(self.amount_off) < 0:
- frappe.throw(_("Amount Off cannot be negative."))
- # Clear the other field
+ if not self.amount_off or self.amount_off == "":
+ frappe.throw(_("Discount amount is required"))
+ try:
+ amount_value = float(self.amount_off)
+ if amount_value < 0:
+ frappe.throw(_("Discount amount cannot be negative"))
+ except (ValueError, TypeError):
+ frappe.throw(_("Discount amount must be a valid number"))
self.percent_off = None
- if self.usage_limit is not None and int(self.usage_limit) < 0:
- frappe.throw(_("Usage limit cannot be negative."))
+ if self.usage_limit is not None and self.usage_limit != "":
+ try:
+ usage_value = int(self.usage_limit)
+ if usage_value < 0:
+ frappe.throw(_("Usage limit cannot be negative"))
+ except (ValueError, TypeError):
+ frappe.throw(_("Usage limit must be a valid number"))
if self.expires_on and str(self.expires_on) < nowdate():
- frappe.throw(_("Expiry date cannot be in the past."))
+ frappe.throw(_("Expiry date cannot be in the past"))
+
+ if not self.get("applicable_items") or len(self.get("applicable_items")) == 0:
+ frappe.throw(_("Please select atleast one course or batch"))
+
+ for item in self.get("applicable_items"):
+ if not item.get("reference_name"):
+ frappe.throw(_("Please select a valid course or batch"))
diff --git a/lms/lms/doctype/lms_payment/lms_payment.json b/lms/lms/doctype/lms_payment/lms_payment.json
index fec68f94..7a5a549e 100644
--- a/lms/lms/doctype/lms_payment/lms_payment.json
+++ b/lms/lms/doctype/lms_payment/lms_payment.json
@@ -19,10 +19,9 @@
"currency",
"amount",
"coupon",
- "discount_type",
- "discount_percent",
"discount_amount",
- "amount_with_gst",
+ "gst_amount",
+ "total_amount",
"column_break_yxpl",
"order_id",
"payment_id",
@@ -57,17 +56,6 @@
"label": "Coupon",
"options": "LMS Coupon"
},
- {
- "fieldname": "discount_type",
- "fieldtype": "Select",
- "label": "Discount Type",
- "options": "\nPercent\nAmount"
- },
- {
- "fieldname": "discount_percent",
- "fieldtype": "Percent",
- "label": "Discount Percent"
- },
{
"fieldname": "discount_amount",
"fieldtype": "Currency",
@@ -144,12 +132,6 @@
"options": "User",
"reqd": 1
},
- {
- "depends_on": "eval:doc.currency == \"INR\";",
- "fieldname": "amount_with_gst",
- "fieldtype": "Currency",
- "label": "Amount with GST"
- },
{
"fieldname": "payment_for_document_type",
"fieldtype": "Select",
@@ -176,6 +158,21 @@
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Payment for Certificate"
+ },
+ {
+ "depends_on": "eval:doc.currency == \"INR\";",
+ "fieldname": "gst_amount",
+ "fieldtype": "Currency",
+ "label": "GST Amount",
+ "options": "currency"
+ },
+ {
+ "fieldname": "total_amount",
+ "fieldtype": "Currency",
+ "label": "Total Amount",
+ "options": "currency",
+ "read_only": 1,
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
@@ -189,8 +186,8 @@
"link_fieldname": "payment"
}
],
- "modified": "2025-09-23 11:04:00.462274",
- "modified_by": "sayali@frappe.io",
+ "modified": "2025-10-13 15:25:56.127625",
+ "modified_by": "Administrator",
"module": "LMS",
"name": "LMS Payment",
"owner": "Administrator",
diff --git a/lms/lms/doctype/lms_payment/lms_payment.py b/lms/lms/doctype/lms_payment/lms_payment.py
index 6cec4170..710cc304 100644
--- a/lms/lms/doctype/lms_payment/lms_payment.py
+++ b/lms/lms/doctype/lms_payment/lms_payment.py
@@ -5,12 +5,15 @@ import frappe
from frappe import _
from frappe.email.doctype.email_template.email_template import get_email_template
from frappe.model.document import Document
-from frappe.utils import add_days, nowdate
+from frappe.utils import add_days, nowdate, flt
class LMSPayment(Document):
- pass
-
+ def validate(self):
+ amount = flt(self.amount or 0, self.precision("amount"))
+ discount = flt(self.discount_amount or 0, self.precision("discount_amount"))
+ gst = flt(self.gst_amount or 0, self.precision("gst_amount"))
+ self.total_amount = flt(amount - discount + gst, self.precision("total_amount"))
def send_payment_reminder():
outgoing_email_account = frappe.get_cached_value(
diff --git a/lms/lms/payments.py b/lms/lms/payments.py
index 6bc3fe32..4c8ab330 100644
--- a/lms/lms/payments.py
+++ b/lms/lms/payments.py
@@ -1,6 +1,5 @@
import frappe
-
def get_payment_gateway():
return frappe.db.get_single_value("LMS Settings", "payment_gateway")
@@ -19,49 +18,46 @@ def validate_currency(payment_gateway, currency):
@frappe.whitelist()
def get_payment_link(
- doctype,
- docname,
- title,
- amount,
- total_amount,
- currency,
- address,
- redirect_to,
- payment_for_certificate,
- coupon_code=None,
+ doctype,
+ docname,
+ title,
+ amount,
+ discount_amount,
+ gst_amount,
+ currency,
+ address,
+ redirect_to,
+ payment_for_certificate,
+ coupon_code=None,
):
payment_gateway = get_payment_gateway()
address = frappe._dict(address)
- amount_with_gst = total_amount if total_amount != amount else 0
- coupon_context = None
- # Coupon application only for courses/batches
- if doctype in ["LMS Course", "LMS Batch"] and coupon_code:
- try:
- from lms.lms.utils import apply_coupon
+ coupon_context = None
+ if doctype in ["LMS Course", "LMS Batch"] and coupon_code:
+ try:
+ from lms.lms.utils import apply_coupon
+ coupon_context = apply_coupon(doctype, docname, coupon_code)
+ except Exception:
+ pass
- applied = apply_coupon(doctype, docname, coupon_code)
- # Override total_amount based on validated coupon calculation
- total_amount = applied.get("amount", total_amount)
- coupon_context = applied
- except Exception:
- # Ignore coupon errors here; frontend handles validation
- pass
-
- payment = record_payment(
- address,
- doctype,
- docname,
- amount,
- currency,
- amount_with_gst,
- payment_for_certificate,
- coupon_context,
- )
+ payment = record_payment(
+ address,
+ doctype,
+ docname,
+ amount,
+ currency,
+ discount_amount,
+ gst_amount,
+ payment_for_certificate,
+ coupon_context,
+ )
controller = get_controller(payment_gateway)
payment_details = {
- "amount": total_amount,
+ "amount": amount - discount_amount + gst_amount,
+ "discount_amount": discount_amount,
+ "gst_amount": gst_amount,
"title": f"Payment for {doctype} {title} {docname}",
"description": f"{address.billing_name}'s payment for {title}",
"reference_doctype": doctype,
@@ -83,14 +79,15 @@ def get_payment_link(
def record_payment(
- address,
- doctype,
- docname,
- amount,
- currency,
- amount_with_gst=0,
- payment_for_certificate=0,
- coupon_context=None,
+ address,
+ doctype,
+ docname,
+ amount,
+ currency,
+ discount_amount=0,
+ gst_amount=0,
+ payment_for_certificate=0,
+ coupon_context=None,
):
address = frappe._dict(address)
address_name = save_address(address)
@@ -103,7 +100,8 @@ def record_payment(
"address": address_name,
"amount": amount,
"currency": currency,
- "amount_with_gst": amount_with_gst,
+ "discount_amount": discount_amount,
+ "gst_amount": gst_amount,
"gstin": address.gstin,
"pan": address.pan,
"source": address.source,
@@ -112,15 +110,15 @@ def record_payment(
"payment_for_certificate": payment_for_certificate,
}
)
- if coupon_context:
- payment_doc.update(
- {
- "coupon": coupon_context.get("coupon"),
- "discount_type": coupon_context.get("discount_type"),
- "discount_percent": coupon_context.get("discount_percent"),
- "discount_amount": coupon_context.get("discount_amount"),
- }
- )
+ if coupon_context:
+ payment_doc.update(
+ {
+ "coupon": coupon_context.get("coupon"),
+ "discount_type": coupon_context.get("discount_type"),
+ "discount_percent": coupon_context.get("discount_percent"),
+ "discount_amount": coupon_context.get("discount_amount"),
+ }
+ )
payment_doc.save(ignore_permissions=True)
return payment_doc