feat: implement coupon code manage
ment with billing and transaction integration; bug fixes
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user