diff --git a/frontend/src/components/CourseCard.vue b/frontend/src/components/CourseCard.vue index 0b5da205..fae4c511 100644 --- a/frontend/src/components/CourseCard.vue +++ b/frontend/src/components/CourseCard.vue @@ -100,9 +100,15 @@ -
+
{{ course.price }}
+
+ {{ __('Paid Certificate') }} +
diff --git a/frontend/src/components/CourseCardOverlay.vue b/frontend/src/components/CourseCardOverlay.vue index b5c84811..d34b3b54 100644 --- a/frontend/src/components/CourseCardOverlay.vue +++ b/frontend/src/components/CourseCardOverlay.vue @@ -6,7 +6,7 @@ class="rounded-t-md min-h-56 w-full" />
-
+
{{ course.data.price }}
+
+ + + {{ __('Paid Certificate') }} + +
diff --git a/frontend/src/pages/CourseCertification.vue b/frontend/src/pages/CourseCertification.vue new file mode 100644 index 00000000..37a3b764 --- /dev/null +++ b/frontend/src/pages/CourseCertification.vue @@ -0,0 +1,2 @@ + + diff --git a/frontend/src/pages/CourseForm.vue b/frontend/src/pages/CourseForm.vue index 47de0416..6d62275a 100644 --- a/frontend/src/pages/CourseForm.vue +++ b/frontend/src/pages/CourseForm.vue @@ -160,7 +160,7 @@
{{ __('Settings') }}
-
+
-
-
-
- {{ __('Pricing') }} + {{ __('Pricing and Certification') }}
-
+
+ +
+
+ + + + + + +
import('@/pages/Lesson.vue'), props: true, }, + { + path: '/courses/:courseName/certification', + name: 'CourseCertification', + component: () => import('@/pages/CourseCertification.vue'), + props: true, + }, { path: '/courses/:courseName/learn/:chapterName', name: 'SCORMChapter', diff --git a/lms/lms/api.py b/lms/lms/api.py index 74daf2e1..9d4c930c 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -190,24 +190,24 @@ def get_translations(): @frappe.whitelist() -def validate_billing_access(type, name): +def validate_billing_access(billing_type, name): access = True message = "" - doctype = "LMS Course" if type == "course" else "LMS Batch" + doctype = "LMS Batch" if billing_type == "batch" else "LMS Course" if frappe.session.user == "Guest": access = False message = _("Please login to continue with payment.") - if type not in ["course", "batch"]: + if access and billing_type not in ["course", "batch", "certificate"]: access = False message = _("Module is incorrect.") - if not frappe.db.exists(doctype, name): + if access and not frappe.db.exists(doctype, name): access = False message = _("Module Name is incorrect or does not exist.") - if type == "course": + if access and billing_type == "course": membership = frappe.db.exists( "LMS Enrollment", {"member": frappe.session.user, "course": name} ) @@ -215,7 +215,7 @@ def validate_billing_access(type, name): access = False message = _("You are already enrolled for this course.") - else: + elif access and billing_type == "batch": membership = frappe.db.exists( "LMS Batch Enrollment", {"member": frappe.session.user, "batch": name} ) @@ -223,6 +223,19 @@ def validate_billing_access(type, name): access = False message = _("You are already enrolled for this batch.") + elif access and billing_type == "certificate": + purchased_certificate = frappe.db.exists( + "LMS Enrollment", + { + "course": name, + "member": frappe.session.user, + "purchased_certificate": 1, + }, + ) + if purchased_certificate: + access = False + message = _("You have already purchased the certificate for this course.") + address = frappe.db.get_value( "Address", {"email_id": frappe.session.user}, @@ -370,7 +383,7 @@ def get_evaluator_details(evaluator): @frappe.whitelist(allow_guest=True) -def get_certified_participants(filters=None, start=0, page_length=30, search=None): +def get_certified_participants(filters=None, start=0, page_length=30): or_filters = {} if not filters: filters = {} diff --git a/lms/lms/doctype/lms_batch/lms_batch.py b/lms/lms/doctype/lms_batch/lms_batch.py index dfb7f4fe..e293c794 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -10,7 +10,6 @@ from datetime import timedelta from frappe.model.document import Document from frappe.utils import cint, format_datetime, get_time, add_days, nowdate from lms.lms.utils import ( - get_lessons, get_lesson_index, get_lesson_url, get_quiz_details, @@ -258,17 +257,6 @@ def create_batch( return doc -@frappe.whitelist() -def fetch_lessons(courses): - lessons = [] - courses = json.loads(courses) - - for course in courses: - lessons.extend(get_lessons(course.get("course"))) - - return lessons - - @frappe.whitelist() def add_course(course, parent, name=None, evaluator=None): frappe.only_for("Moderator") diff --git a/lms/lms/doctype/lms_course/lms_course.json b/lms/lms/doctype/lms_course/lms_course.json index beae4cd0..9369456c 100644 --- a/lms/lms/doctype/lms_course/lms_course.json +++ b/lms/lms/doctype/lms_course/lms_course.json @@ -40,15 +40,12 @@ "pricing_tab", "pricing_section", "paid_course", + "enable_certification", + "paid_certificate", "column_break_acoj", "course_price", "currency", "amount_usd", - "certification_tab", - "certification_section", - "enable_certification", - "column_break_rxww", - "expiry", "tab_4_tab", "statistics_section", "enrollments", @@ -134,22 +131,11 @@ "fieldtype": "Section Break", "label": "Course Settings" }, - { - "fieldname": "certification_section", - "fieldtype": "Section Break" - }, { "default": "0", "fieldname": "enable_certification", "fieldtype": "Check", - "label": "Enable Certification" - }, - { - "default": "0", - "depends_on": "enable_certification", - "fieldname": "expiry", - "fieldtype": "Int", - "label": "Certification Expires After (Years)" + "label": "Completion Certificate" }, { "fieldname": "related_courses", @@ -181,7 +167,6 @@ "fieldtype": "Section Break" }, { - "depends_on": "paid_course", "fieldname": "currency", "fieldtype": "Link", "label": "Currency", @@ -195,22 +180,16 @@ "label": "Paid Course" }, { - "depends_on": "paid_course", "fieldname": "course_price", "fieldtype": "Currency", - "label": "Course Price", + "label": "Amount", "mandatory_depends_on": "paid_course" }, - { - "fieldname": "column_break_rxww", - "fieldtype": "Column Break" - }, { "fieldname": "column_break_acoj", "fieldtype": "Column Break" }, { - "depends_on": "paid_course", "description": "If you set an amount here, then the USD equivalent setting will not get applied.", "fieldname": "amount_usd", "fieldtype": "Currency", @@ -238,12 +217,7 @@ { "fieldname": "pricing_tab", "fieldtype": "Tab Break", - "label": "Pricing" - }, - { - "fieldname": "certification_tab", - "fieldtype": "Tab Break", - "label": "Certification" + "label": "Pricing and Certification" }, { "fieldname": "column_break_htgn", @@ -284,6 +258,12 @@ "fieldtype": "Data", "label": "Rating", "read_only": 1 + }, + { + "default": "0", + "fieldname": "paid_certificate", + "fieldtype": "Check", + "label": "Paid Certificate" } ], "is_published_field": "published", @@ -310,7 +290,7 @@ } ], "make_attachments_public": 1, - "modified": "2024-10-30 23:08:31.842860", + "modified": "2025-02-20 16:44:38.891383", "modified_by": "Administrator", "module": "LMS", "name": "LMS Course", diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index 1870130a..54625d49 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -5,9 +5,8 @@ import json import random import frappe from frappe.model.document import Document -from frappe.utils import cint, today -from frappe.utils.telemetry import capture -from lms.lms.utils import get_chapters, can_create_courses +from frappe.utils import today, cint +from lms.lms.utils import get_chapters from ...utils import generate_slug, validate_image, update_payment_record from frappe import _ @@ -53,9 +52,12 @@ class LMSCourse(Document): frappe.throw(_("Please install the Payments app to create a paid courses.")) def validate_amount_and_currency(self): - if self.paid_course and (not self.course_price and not self.currency): + if self.paid_course and (cint(self.course_price) < 0 or not self.currency): frappe.throw(_("Amount and currency are required for paid courses.")) + if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency): + frappe.throw(_("Amount and currency are required for paid certificates.")) + def on_update(self): if not self.upcoming and self.has_value_changed("upcoming"): self.send_email_to_interested_users() diff --git a/lms/lms/doctype/lms_enrollment/lms_enrollment.json b/lms/lms/doctype/lms_enrollment/lms_enrollment.json index 0e9b258a..5a811ccf 100644 --- a/lms/lms/doctype/lms_enrollment/lms_enrollment.json +++ b/lms/lms/doctype/lms_enrollment/lms_enrollment.json @@ -14,6 +14,9 @@ "member", "member_name", "member_username", + "certification_section", + "purchased_certificate", + "certificate", "section_break_8", "cohort", "subgroup", @@ -123,11 +126,28 @@ "fieldtype": "Link", "label": "Payment", "options": "LMS Payment" + }, + { + "fieldname": "certification_section", + "fieldtype": "Section Break", + "label": "Certification" + }, + { + "default": "0", + "fieldname": "purchased_certificate", + "fieldtype": "Check", + "label": "Purchased Certificate" + }, + { + "fieldname": "certificate", + "fieldtype": "Link", + "label": "Certificate", + "options": "LMS Certificate" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-10-30 12:44:16.103598", + "modified": "2025-02-21 17:11:37.986157", "modified_by": "Administrator", "module": "LMS", "name": "LMS Enrollment", diff --git a/lms/lms/doctype/lms_payment/lms_payment.json b/lms/lms/doctype/lms_payment/lms_payment.json index 67720a46..cca8ae66 100644 --- a/lms/lms/doctype/lms_payment/lms_payment.json +++ b/lms/lms/doctype/lms_payment/lms_payment.json @@ -14,6 +14,7 @@ "payment_for_document_type", "payment_for_document", "payment_received", + "payment_for_certificate", "payment_details_section", "currency", "amount", @@ -136,6 +137,12 @@ "fieldtype": "Link", "label": "Source", "options": "LMS Source" + }, + { + "default": "0", + "fieldname": "payment_for_certificate", + "fieldtype": "Check", + "label": "Payment for Certificate" } ], "index_web_pages_for_search": 1, @@ -149,7 +156,7 @@ "link_fieldname": "payment" } ], - "modified": "2025-02-18 15:54:25.383353", + "modified": "2025-02-21 18:29:55.436611", "modified_by": "Administrator", "module": "LMS", "name": "LMS Payment", diff --git a/lms/lms/payments.py b/lms/lms/payments.py index 8f8cc082..9f4e31fe 100644 --- a/lms/lms/payments.py +++ b/lms/lms/payments.py @@ -18,19 +18,26 @@ def validate_currency(payment_gateway, currency): @frappe.whitelist() -def get_payment_link(doctype, docname, title, amount, total_amount, currency, address): +def get_payment_link( + doctype, + docname, + title, + amount, + total_amount, + currency, + address, + redirect_to, + payment_for_certificate, +): payment_gateway = get_payment_gateway() address = frappe._dict(address) amount_with_gst = total_amount if total_amount != amount else 0 - payment = record_payment(address, doctype, docname, amount, currency, amount_with_gst) + payment = record_payment( + address, doctype, docname, amount, currency, amount_with_gst, payment_for_certificate + ) controller = get_controller(payment_gateway) - if doctype == "LMS Course": - redirect_to = f"/lms/courses/{docname}/learn/1-1" - elif doctype == "LMS Batch": - redirect_to = f"/lms/batches/{docname}" - payment_details = { "amount": total_amount, "title": f"Payment for {doctype} {title} {docname}", @@ -53,7 +60,15 @@ def get_payment_link(doctype, docname, title, amount, total_amount, currency, ad return url -def record_payment(address, doctype, docname, amount, currency, amount_with_gst=0): +def record_payment( + address, + doctype, + docname, + amount, + currency, + amount_with_gst=0, + payment_for_certificate=0, +): address = frappe._dict(address) address_name = save_address(address) @@ -71,6 +86,7 @@ def record_payment(address, doctype, docname, amount, currency, amount_with_gst= "source": address.source, "payment_for_document_type": doctype, "payment_for_document": docname, + "payment_for_certificate": payment_for_certificate, } ) payment_doc.save(ignore_permissions=True) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 5a6147b9..9405fd50 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -68,27 +68,26 @@ def generate_slug(title, doctype): return slugify(title, used_slugs=slugs) -def get_membership(course, member=None, batch=None): +def get_membership(course, member=None): if not member: member = frappe.session.user filters = {"member": member, "course": course} - if batch: - filters["batch_old"] = batch - is_member = frappe.db.exists("LMS Enrollment", filters) - if is_member: + if frappe.db.exists("LMS Enrollment", filters): membership = frappe.db.get_value( "LMS Enrollment", filters, - ["name", "batch_old", "current_lesson", "member_type", "progress", "member"], + [ + "name", + "current_lesson", + "progress", + "member", + "purchased_certificate", + "certificate", + ], as_dict=True, ) - - if membership and membership.batch_old: - membership.batch_title = frappe.db.get_value( - "LMS Batch Old", membership.batch_old, "title" - ) return membership return False @@ -1009,6 +1008,7 @@ def get_course_details(course): "category", "status", "paid_course", + "paid_certificate", "course_price", "currency", "amount_usd", @@ -1023,7 +1023,7 @@ def get_course_details(course): course_details.instructors = get_instructors(course_details.name) # course_details.is_instructor = is_instructor(course_details.name) - if course_details.paid_course: + if course_details.paid_course or course_details.paid_certificate: """course_details.course_price, course_details.currency = check_multicurrency( course_details.course_price, course_details.currency, None, course_details.amount_usd )""" @@ -1136,14 +1136,21 @@ def get_lesson(course, chapter, lesson): return {} membership = get_membership(course) - course_title = frappe.db.get_value("LMS Course", course, "title") + course_info = frappe.db.get_value( + "LMS Course", course, ["title", "paid_certificate"], as_dict=1 + ) + if ( not lesson_details.include_in_preview and not membership and not has_course_moderator_role() and not is_instructor(course) ): - return {"no_preview": 1, "title": lesson_details.title, "course_title": course_title} + return { + "no_preview": 1, + "title": lesson_details.title, + "course_title": course_info.title, + } lesson_details = frappe.db.get_value( "Course Lesson", @@ -1178,7 +1185,8 @@ def get_lesson(course, chapter, lesson): lesson_details.prev = neighbours["prev"] lesson_details.membership = membership lesson_details.instructors = get_instructors(course) - lesson_details.course_title = course_title + lesson_details.course_title = course_info.title + lesson_details.paid_certificate = course_info.paid_certificate return lesson_details @@ -1612,11 +1620,19 @@ def get_order_summary(doctype, docname, country=None): details = frappe.db.get_value( "LMS Course", docname, - ["title", "name", "paid_course", "course_price as amount", "currency", "amount_usd"], + [ + "title", + "name", + "paid_course", + "paid_certificate", + "course_price as amount", + "currency", + "amount_usd", + ], as_dict=True, ) - if not details.paid_course: + if not details.paid_course and not details.paid_certificate: raise frappe.throw(_("This course is free.")) else: @@ -1730,9 +1746,14 @@ def update_payment_record(doctype, docname): "order_id": data.get("order_id"), }, ) + payment_for_certificate = frappe.db.get_value( + "LMS Payment", data.payment, "payment_for_certificate" + ) try: - if doctype == "LMS Course": + if payment_for_certificate: + update_certificate_purchase(docname) + elif doctype == "LMS Course": enroll_in_course(data.payment, docname) else: enroll_in_batch(docname, data.payment) @@ -1792,6 +1813,15 @@ def enroll_in_batch(batch, payment_name=None): new_student.save() +def update_certificate_purchase(course): + frappe.db.set_value( + "LMS Enrollment", + {"member": frappe.session.user, "course": course}, + "purchased_certificate", + 1, + ) + + @frappe.whitelist() def get_programs(): if ( diff --git a/lms/lms/widgets/CourseOutline.html b/lms/lms/widgets/CourseOutline.html deleted file mode 100644 index b95fbdba..00000000 --- a/lms/lms/widgets/CourseOutline.html +++ /dev/null @@ -1,110 +0,0 @@ -{% set chapters = get_chapters(course.name) %} -{% set is_instructor = is_instructor(course.name) %} - -{% if chapters | length %} -
- - {% if not lesson_page %} -
- {{ _("Course Content") }} -
- - - {% endif %} - - {% if chapters | length %} -
- {% for chapter in chapters %} - {% set lessons = get_lessons(course.name, chapter) %} - -
- - - - - -
- {% endfor %} -
- - {% endif %} - -
-{% endif %} - -{% if chapters | length %} - -{{ widgets.NoPreviewModal(course=course, membership=membership) }} - -{% endif %}