diff --git a/frontend/src/components/Settings/CouponDetails.vue b/frontend/src/components/Settings/CouponDetails.vue index b6e0b382..8a552af4 100644 --- a/frontend/src/components/Settings/CouponDetails.vue +++ b/frontend/src/components/Settings/CouponDetails.vue @@ -2,7 +2,15 @@ @@ -87,7 +116,9 @@ const doc = ref({ applicable_items: [], }) -const dialogTitle = computed(() => (props.couponId === 'new' ? __('New Coupon') : __('Edit Coupon'))) +const dialogTitle = computed(() => + props.couponId === 'new' ? __('New Coupon') : __('Edit Coupon') +) const getDoc = createResource({ url: 'frappe.client.get', @@ -106,14 +137,22 @@ watch( if (props.couponId && props.couponId !== 'new') { getDoc.submit() } else { - doc.value = { code: '', discount_type: 'Percent', active: 1, applicable_items: [] } + doc.value = { + code: '', + discount_type: 'Percent', + active: 1, + applicable_items: [], + } } } } ) function addRow() { - doc.value.applicable_items.push({ reference_doctype: 'LMS Course', reference_name: null }) + doc.value.applicable_items.push({ + reference_doctype: 'LMS Course', + reference_name: null, + }) } function removeRow(idx) { doc.value.applicable_items.splice(idx, 1) @@ -141,28 +180,33 @@ function handleCodeInput(event) { function save() { if (props.couponId && props.couponId !== 'new') { - saveDoc.submit({}, { - onSuccess() { - toast.success(__('Saved')) - show.value = false - emit('saved') - }, - onError(err) { - toast.error(err.messages?.[0] || err.message || err) + saveDoc.submit( + {}, + { + onSuccess() { + toast.success(__('Saved')) + show.value = false + emit('saved') + }, + onError(err) { + toast.error(err.messages?.[0] || err.message || err) + }, } - }) + ) } else { - insertDoc.submit({}, { - onSuccess() { - toast.success(__('Saved')) - show.value = false - emit('saved') - }, - onError(err) { - toast.error(err.messages?.[0] || err.message || err) + insertDoc.submit( + {}, + { + onSuccess() { + toast.success(__('Saved')) + show.value = false + emit('saved') + }, + onError(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 1f2611c5..eac012d5 100644 --- a/frontend/src/components/Settings/Coupons.vue +++ b/frontend/src/components/Settings/Coupons.vue @@ -27,36 +27,49 @@ {{ __('Expires On') }} {{ __('Usage') }} {{ __('Active') }} - + - + {{ row.code }} {{ row.discount_type }} - {{ row.percent_off }}% + {{ row.percent_off }}% {{ row.amount_off }} {{ row.expires_on || '-' }} - {{ row.times_redeemed }}/{{ row.usage_limit || '∞' }} + + {{ row.times_redeemed }}/{{ row.usage_limit || '∞' }} + {{ __('Enabled') }} {{ __('Disabled') }} - - + - + - diff --git a/frontend/src/components/Settings/TransactionDetails.vue b/frontend/src/components/Settings/TransactionDetails.vue index 07d97e90..d9aa5e5e 100644 --- a/frontend/src/components/Settings/TransactionDetails.vue +++ b/frontend/src/components/Settings/TransactionDetails.vue @@ -88,7 +88,10 @@ :disabled="true" />
- {{ getCurrencySymbol(row['currency']) }} {{ row['total_amount'] }} + {{ getCurrencySymbol(row['currency']) }} + {{ row['total_amount'] }}
{{ row[column.key] }} diff --git a/frontend/src/pages/Billing.vue b/frontend/src/pages/Billing.vue index bf7ff3cd..8e6c8320 100644 --- a/frontend/src/pages/Billing.vue +++ b/frontend/src/pages/Billing.vue @@ -35,7 +35,10 @@ {{ orderSummary.data.original_amount_formatted }}
-
+
{{ __('Discount') }}
-{{ orderSummary.data.discount_amount_formatted }}
@@ -61,11 +64,32 @@
-
- {{ __('Coupon') }} - - - +
+ {{ + __('Coupon') + }} + + +
@@ -268,7 +292,7 @@ const setBillingDetails = (data) => { const paymentLink = createResource({ url: 'lms.lms.payments.get_payment_link', makeParams(values) { - let data={ + let data = { doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course', docname: props.name, title: orderSummary.data.title, diff --git a/lms/lms/doctype/lms_coupon/lms_coupon.py b/lms/lms/doctype/lms_coupon/lms_coupon.py index abb0ee55..d5328d84 100644 --- a/lms/lms/doctype/lms_coupon/lms_coupon.py +++ b/lms/lms/doctype/lms_coupon/lms_coupon.py @@ -2,75 +2,75 @@ # For license information, please see license.txt import frappe -from frappe.model.document import Document from frappe import _ +from frappe.model.document import Document from frappe.utils import nowdate class LMSCoupon(Document): - def validate(self): - if self.code: - self.code = self.code.strip().upper() + def validate(self): + if self.code: + self.code = self.code.strip().upper() - if not self.code: - frappe.throw(_("Coupon code is required")) + if not self.code: + frappe.throw(_("Coupon code is required")) - if len(self.code) < 6: - frappe.throw(_("Coupon code must be atleast 6 characters")) + if len(self.code) < 6: + frappe.throw(_("Coupon code must be atleast 6 characters")) - if self.name: - existing = frappe.db.exists( - "LMS Coupon", - { - "code": self.code, - "name": ["!=", self.name], - }, - ) - else: - existing = frappe.db.exists("LMS Coupon", {"code": self.code}) - - if existing: - frappe.throw(_("Coupon code is already taken. Use a different one")) + if self.name: + existing = frappe.db.exists( + "LMS Coupon", + { + "code": self.code, + "name": ["!=", self.name], + }, + ) + else: + existing = frappe.db.exists("LMS Coupon", {"code": self.code}) - if not self.discount_type: - frappe.throw(_("Discount type is required")) + if existing: + frappe.throw(_("Coupon code is already taken. Use a different one")) - if self.discount_type == "Percent": - 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 not self.discount_type: + frappe.throw(_("Discount type is required")) - if self.discount_type == "Amount": - 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.discount_type == "Percent": + 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.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.discount_type == "Amount": + 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.expires_on and str(self.expires_on) < nowdate(): - frappe.throw(_("Expiry date cannot be in the past")) + 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 not self.get("applicable_items") or len(self.get("applicable_items")) == 0: - frappe.throw(_("Please select atleast one course or batch")) + if self.expires_on and str(self.expires_on) < nowdate(): + frappe.throw(_("Expiry date cannot be in the past")) - for item in self.get("applicable_items"): - if not item.get("reference_name"): - frappe.throw(_("Please select a valid course or batch")) + 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_coupon/test_lms_coupon.py b/lms/lms/doctype/lms_coupon/test_lms_coupon.py index 11cdf628..58e26892 100644 --- a/lms/lms/doctype/lms_coupon/test_lms_coupon.py +++ b/lms/lms/doctype/lms_coupon/test_lms_coupon.py @@ -4,7 +4,6 @@ # import frappe from frappe.tests import IntegrationTestCase - # On IntegrationTestCase, the doctype test records and all # link-field test record dependencies are recursively loaded # Use these module variables to add/remove to/from that list @@ -12,7 +11,6 @@ EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - class IntegrationTestLMSCoupon(IntegrationTestCase): """ Integration tests for LMSCoupon. diff --git a/lms/lms/doctype/lms_payment/lms_payment.py b/lms/lms/doctype/lms_payment/lms_payment.py index 710cc304..49e1c3d1 100644 --- a/lms/lms/doctype/lms_payment/lms_payment.py +++ b/lms/lms/doctype/lms_payment/lms_payment.py @@ -5,7 +5,7 @@ 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, flt +from frappe.utils import add_days, flt, nowdate class LMSPayment(Document): @@ -15,6 +15,7 @@ class LMSPayment(Document): 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( "Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name" diff --git a/lms/lms/payments.py b/lms/lms/payments.py index f69137aa..cfe75347 100644 --- a/lms/lms/payments.py +++ b/lms/lms/payments.py @@ -1,5 +1,6 @@ import frappe + def get_payment_gateway(): return frappe.db.get_single_value("LMS Settings", "payment_gateway") @@ -18,17 +19,17 @@ def validate_currency(payment_gateway, currency): @frappe.whitelist() def get_payment_link( - doctype, - docname, - title, - amount, + doctype, + docname, + title, + amount, discount_amount, - gst_amount, - currency, - address, - redirect_to, - payment_for_certificate, - coupon_code=None, + gst_amount, + currency, + address, + redirect_to, + payment_for_certificate, + coupon_code=None, ): payment_gateway = get_payment_gateway() address = frappe._dict(address) @@ -37,21 +38,22 @@ def get_payment_link( 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 payment = record_payment( - address, - doctype, - docname, - amount, - currency, - discount_amount, - gst_amount, - payment_for_certificate, - coupon_context, - ) + address, + doctype, + docname, + amount, + currency, + discount_amount, + gst_amount, + payment_for_certificate, + coupon_context, + ) controller = get_controller(payment_gateway) payment_details = { @@ -79,15 +81,15 @@ def get_payment_link( def record_payment( - address, - doctype, - docname, - amount, - currency, - discount_amount=0, - gst_amount=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) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 50f54cfd..7ed54954 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -953,74 +953,74 @@ def get_current_exchange_rate(source, target="USD"): @frappe.whitelist() def apply_coupon(doctype, docname, code, country=None): - # Validate doctype - if doctype not in ["LMS Course", "LMS Batch"]: - frappe.throw(_("Invalid doctype for coupon application.")) + # Validate doctype + if doctype not in ["LMS Course", "LMS Batch"]: + frappe.throw(_("Invalid doctype for coupon application.")) - if not code: - frappe.throw(_("Coupon code is required.")) + if not code: + frappe.throw(_("Coupon code is required.")) - summary = get_order_summary(doctype, docname, country) + summary = get_order_summary(doctype, docname, country) - base_amount = summary.original_amount - currency = summary.currency + base_amount = summary.original_amount + currency = summary.currency - # Fetch coupon case-insensitively - coupon_name = frappe.db.get_value("LMS Coupon", {"code": code.strip().upper(), "active": 1}, "name") - if not coupon_name: - frappe.throw(_("Invalid or inactive coupon code.")) + # Fetch coupon case-insensitively + coupon_name = frappe.db.get_value("LMS Coupon", {"code": code.strip().upper(), "active": 1}, "name") + if not coupon_name: + frappe.throw(_("Invalid or inactive coupon code.")) - coupon = frappe.get_doc("LMS Coupon", coupon_name) + coupon = frappe.get_doc("LMS Coupon", coupon_name) - # Expiry - if coupon.expires_on and getdate(coupon.expires_on) < getdate(): - frappe.throw(_("This coupon has expired.")) + # Expiry + if coupon.expires_on and getdate(coupon.expires_on) < getdate(): + frappe.throw(_("This coupon has expired.")) - # Usage limit - if coupon.usage_limit and cint(coupon.times_redeemed) >= cint(coupon.usage_limit): - frappe.throw(_("This coupon has reached its usage limit.")) + # Usage limit + if coupon.usage_limit and cint(coupon.times_redeemed) >= cint(coupon.usage_limit): + frappe.throw(_("This coupon has reached its usage limit.")) - # Applicability (if rows exist, must match; if none, applies to all) - applicable = True - if len(coupon.applicable_items): - applicable = any( - (row.reference_doctype == doctype and row.reference_name == docname) - for row in coupon.applicable_items - ) - if not applicable: - frappe.throw(_("This coupon is not applicable to this item.")) + # Applicability (if rows exist, must match; if none, applies to all) + applicable = True + if len(coupon.applicable_items): + applicable = any( + (row.reference_doctype == doctype and row.reference_name == docname) + for row in coupon.applicable_items + ) + if not applicable: + frappe.throw(_("This coupon is not applicable to this item.")) - # Compute discount before tax - discount_amount = 0 - if coupon.discount_type == "Percent": - discount_amount = cint(flt(base_amount) * flt(coupon.percent_off) / 100) - else: - discount_amount = min(flt(coupon.amount_off), flt(base_amount)) + # Compute discount before tax + discount_amount = 0 + if coupon.discount_type == "Percent": + discount_amount = cint(flt(base_amount) * flt(coupon.percent_off) / 100) + else: + discount_amount = min(flt(coupon.amount_off), flt(base_amount)) - subtotal = max(flt(base_amount) - flt(discount_amount), 0) + subtotal = max(flt(base_amount) - flt(discount_amount), 0) - gst_applied = 0 - final_amount = subtotal - if currency == "INR": - final_amount, gst_applied = apply_gst(subtotal, country) + gst_applied = 0 + final_amount = subtotal + if currency == "INR": + final_amount, gst_applied = apply_gst(subtotal, country) - return { - "title": summary.title, - "name": summary.name, - "currency": currency, - "original_amount": base_amount, - "original_amount_formatted": fmt_money(base_amount, 0, currency), - "discount_amount": discount_amount, - "discount_amount_formatted": fmt_money(discount_amount, 0, currency), - "amount": final_amount, - "gst_applied": gst_applied, - "gst_amount_formatted": fmt_money(gst_applied, 0, currency) if gst_applied else None, - "total_amount_formatted": fmt_money(final_amount, 0, currency), - "coupon": coupon.name, - "coupon_code": coupon.code, - "discount_type": coupon.discount_type, - "discount_percent": coupon.percent_off if coupon.discount_type == "Percent" else None, - } + return { + "title": summary.title, + "name": summary.name, + "currency": currency, + "original_amount": base_amount, + "original_amount_formatted": fmt_money(base_amount, 0, currency), + "discount_amount": discount_amount, + "discount_amount_formatted": fmt_money(discount_amount, 0, currency), + "amount": final_amount, + "gst_applied": gst_applied, + "gst_amount_formatted": fmt_money(gst_applied, 0, currency) if gst_applied else None, + "total_amount_formatted": fmt_money(final_amount, 0, currency), + "coupon": coupon.name, + "coupon_code": coupon.code, + "discount_type": coupon.discount_type, + "discount_percent": coupon.percent_off if coupon.discount_type == "Percent" else None, + } @frappe.whitelist()