refactor: transactions changes based on coupon code

This commit is contained in:
Jannat Patel
2025-11-13 11:46:35 +05:30
parent c1bdfe33f0
commit 46f5808fdb
17 changed files with 422 additions and 1882 deletions

View File

@@ -111,8 +111,9 @@ declare module 'vue' {
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
Tags: typeof import('./src/components/Tags.vue')['default']
TransactionDetails: typeof import('./src/components/Settings/TransactionDetails.vue')['default']
Transactions: typeof import('./src/components/Settings/Transactions.vue')['default']
TransactionDetails: typeof import('./src/components/Settings/Transactions/TransactionDetails.vue')['default']
TransactionList: typeof import('./src/components/Settings/Transactions/TransactionList.vue')['default']
Transactions: typeof import('./src/components/Settings/Transactions/Transactions.vue')['default']
UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default']
UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default']
Uploader: typeof import('./src/components/Controls/Uploader.vue')['default']

View File

@@ -24,7 +24,7 @@
"@editorjs/paragraph": "^2.11.3",
"@editorjs/simple-image": "^1.6.0",
"@editorjs/table": "^2.4.2",
"@vueuse/core": "^14.0.0",
"@vueuse/core": "^10.4.1",
"@vueuse/router": "^12.7.0",
"ace-builds": "^1.36.2",
"apexcharts": "^4.3.0",

View File

@@ -19,10 +19,10 @@
@click="() => togglePopover()"
:disabled="attrs.readonly"
>
<div class="flex items-center">
<div class="flex items-center w-[90%]">
<slot name="prefix" />
<span
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
class="block truncate text-base leading-5"
v-if="selectedValue"
>
{{ displayValue(selectedValue) }}

View File

@@ -1,16 +1,15 @@
<template>
<div class="flex flex-col text-base h-full overflow-y-auto">
<div class="flex items-center space-x-2 mb-5">
<Button variant="ghost" @click="emit('updateStep', 'list')">
<template #icon>
<ChevronLeft class="size-5 stroke-1 text-ink-gray-7" />
</template>
</Button>
<div class="flex flex-col text-base h-full">
<div class="flex items-center space-x-2 mb-8 -ml-1.5">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="emit('updateStep', 'list')"
/>
<div class="text-xl font-semibold text-ink-gray-9">
{{ data?.name ? __('Edit Coupon') : __('New Coupon') }}
</div>
</div>
<div class="space-y-4">
<div class="space-y-4 overflow-y-auto">
<div>
<FormControl
v-model="data.enabled"
@@ -74,35 +73,28 @@
<CouponItems ref="couponItems" :data="data" :coupons="coupons" />
</div>
</div>
<div class="mt-auto w-full space-x-2">
<Button variant="solid" class="float-right" @click="saveCoupon()">
<div class="mt-auto space-x-2 ml-auto">
<Button variant="solid" @click="saveCoupon()">
{{ __('Save') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import {
Button,
createListResource,
createResource,
FormControl,
toast,
} from 'frappe-ui'
import { ref, watch, computed } from 'vue'
import { ChevronLeft, Plus, X } from 'lucide-vue-next'
import { Button, FormControl, toast } from 'frappe-ui'
import { ref } from 'vue'
import { ChevronLeft } from 'lucide-vue-next'
import type { Coupon, Coupons } from './types'
import CouponItems from '@/components/Settings/Coupons/CouponItems.vue'
const couponItems = ref<any>(null)
const emit = defineEmits(['updateStep'])
const props = defineProps<{
coupons: Coupons
data: Coupon
}>()
const emit = defineEmits(['updateStep'])
const saveCoupon = () => {
if (props.data?.name) {
editCoupon()
@@ -129,7 +121,6 @@ const editCoupon = () => {
const createCoupon = () => {
if (couponItems.value) {
let rows = couponItems.value.saveItems()
console.log(rows)
props.data.applicable_items = rows
}
props.coupons.insert.submit(
@@ -148,98 +139,4 @@ const createCoupon = () => {
}
)
}
/*
watch(
() => show.value,
(val) => {
if (val) {
if (props.couponId && props.couponId !== 'new') {
getDoc.submit()
} else {
doc.value = {
code: '',
discount_type: 'Percent',
active: 1,
applicable_items: [],
}
}
}
}
) */
/*
function addRow() {
doc.value.applicable_items.push({
reference_doctype: 'LMS Course',
reference_name: null,
})
}
function removeRow(idx) {
doc.value.applicable_items.splice(idx, 1)
} */
/*
function getFilters(idx) {
// don't show the batch or course that has already been selected
const row = doc.value.applicable_items[idx]
if (!row.reference_doctype) return {}
const doctype = row.reference_doctype
const selectedNames = doc.value.applicable_items
.filter(
(r, i) => i !== idx && r.reference_doctype === doctype && r.reference_name
)
.map((r) => r.reference_name)
if (selectedNames.length === 0) return {}
return {
name: ['not in', selectedNames],
}
}
const saveDoc = createResource({
url: 'frappe.client.save',
makeParams(values) {
return { doc: doc.value }
},
})
const insertDoc = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return { doc: { doctype: 'LMS Coupon', ...doc.value } }
},
})
function handleCodeInput(event) {
if (event.data && !/^[A-Za-z0-9]*$/.test(event.data)) {
event.preventDefault()
}
}
function save() {
if (props.couponId && props.couponId !== 'new') {
saveDoc.submit(
{},
{
onSuccess() {
toast.success(__('Saved'))
emit('saved')
},
onError(err) {
toast.error(err.messages?.[0] || err.message || err)
},
}
)
} else {
insertDoc.submit(
{},
{
onSuccess() {
toast.success(__('Saved'))
},
onError(err) {
toast.error(err.messages?.[0] || err.message || err)
},
}
)
}
} */
</script>

View File

@@ -147,6 +147,8 @@ const columns = computed(() => {
} else {
if (field.type == 'checkbox') {
field.value = props.data[field.name] ? true : false
} else {
field.value = props.data[field.name]
}
currentColumn.push(field)
}

View File

@@ -29,7 +29,7 @@
<div
v-if="activeTab && data.doc"
:key="activeTab.label"
class="flex flex-1 flex-col px-10 py-8 bg-surface-modal"
class="flex flex-1 flex-col p-8 bg-surface-modal"
>
<component
v-if="activeTab.template"
@@ -79,7 +79,7 @@ import EmailTemplates from '@/components/Settings/EmailTemplates.vue'
import BrandSettings from '@/components/Settings/BrandSettings.vue'
import PaymentGateways from '@/components/Settings/PaymentGateways.vue'
import Coupons from '@/components/Settings/Coupons/Coupons.vue'
import Transactions from '@/components/Settings/Transactions.vue'
import Transactions from '@/components/Settings/Transactions/Transactions.vue'
import ZoomSettings from '@/components/Settings/ZoomSettings.vue'
import Badges from '@/components/Settings/Badges.vue'

View File

@@ -1,199 +0,0 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Transaction Details'),
size: '3xl',
}"
>
<template #body-content>
<div v-if="transactionData" class="text-base">
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl
:label="__('Payment Received')"
type="checkbox"
v-model="transactionData.payment_received"
/>
<FormControl
:label="__('Payment For Certificate')"
type="checkbox"
v-model="transactionData.payment_for_certificate"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Member')"
doctype="User"
v-model="transactionData.member"
/>
<FormControl
:label="__('Billing Name')"
v-model="transactionData.billing_name"
/>
<Link
:label="__('Source')"
v-model="transactionData.source"
doctype="LMS Source"
/>
</div>
<div class="font-semibold mt-10">
{{ __('Payment Details') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Payment For Document Type')"
v-model="transactionData.payment_for_document_type"
doctype="DocType"
/>
<Link
:label="__('Payment For Document')"
v-model="transactionData.payment_for_document"
:doctype="transactionData.payment_for_document_type"
/>
<Link
:label="__('Address')"
v-model="transactionData.address"
doctype="Address"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Currency')"
v-model="transactionData.currency"
doctype="Currency"
/>
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
<FormControl
v-if="transactionData.coupon"
:label="__('Coupon Code')"
v-model="transactionData.coupon_code"
:disabled="true"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl
v-if="Number(transactionData.discount_amount)"
:label="__('Discount Amount')"
v-model="transactionData.discount_amount"
:disabled="true"
/>
<FormControl
v-if="Number(transactionData.gst_amount)"
:label="__('GST Amount')"
v-model="transactionData.gst_amount"
:disabled="true"
/>
<FormControl
v-if="
Number(transactionData.discount_amount) ||
Number(transactionData.gst_amount)
"
:label="__('Total Amount')"
v-model="transactionData.total_amount"
:disabled="true"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl :label="__('GSTIN')" v-model="transactionData.gstin" />
<FormControl :label="__('PAN')" v-model="transactionData.pan" />
<FormControl
:label="__('Payment ID')"
v-model="transactionData.payment_id"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl
:label="__('Order ID')"
v-model="transactionData.order_id"
/>
</div>
</div>
</template>
<template #actions="{ close }">
<div class="space-x-2 pb-5 float-right">
<Button @click="openDetails(close)">
{{ __('Open the ') }}
{{
transaction.payment_for_document_type == 'LMS Course'
? __('Course')
: __('Batch')
}}
</Button>
<Button variant="solid" @click="saveTransaction(close)">
{{ __('Save') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Dialog, FormControl, Button, createResource } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import Link from '@/components/Controls/Link.vue'
const show = defineModel<boolean>({ required: true, default: false })
const transactions = defineModel<any>('transactions')
const router = useRouter()
const showModal = defineModel('show')
const transactionData = ref<{ [key: string]: any } | null>(null)
const props = defineProps<{
transaction: { [key: string]: any } | null
}>()
const paymentDoc = createResource({
url: 'frappe.client.get',
makeParams() {
return { doctype: 'LMS Payment', name: props.transaction?.name }
},
onSuccess(data) {
transactionData.value = data
},
})
watch(
() => props.transaction,
(newVal) => {
if (newVal?.name) {
paymentDoc.submit()
} else {
transactionData.value = null
}
},
{ immediate: true }
)
const saveTransaction = (close: () => void) => {
transactions.value.setValue
.submit({
...transactionData.value,
})
.then(() => {
close()
})
}
const openDetails = (close: Function) => {
if (props.transaction) {
const docType = props.transaction.payment_for_document_type
const docName = props.transaction.payment_for_document
if (docType && docName) {
router.push({
name: docType == 'LMS Course' ? 'CourseDetail' : 'BatchDetail',
params: {
[docType == 'LMS Course' ? 'courseName' : 'batchName']: docName,
},
})
}
}
close()
showModal.value = false
}
</script>

View File

@@ -0,0 +1,184 @@
<template>
<div class="flex flex-col h-full text-base">
<div class="flex items-center space-x-2 mb-8 -ml-1.5">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="emit('updateStep', 'list')"
/>
<div class="text-xl font-semibold text-ink-gray-9">
{{ __('Transaction Details') }}
</div>
</div>
<div v-if="transactionData" class="overflow-y-auto">
<div class="grid grid-cols-3 gap-5">
<FormControl
:label="__('Payment Received')"
type="checkbox"
v-model="transactionData.payment_received"
/>
<FormControl
:label="__('Payment For Certificate')"
type="checkbox"
v-model="transactionData.payment_for_certificate"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Member')"
doctype="User"
v-model="transactionData.member"
/>
<FormControl
:label="__('Billing Name')"
v-model="transactionData.billing_name"
/>
<Link
:label="__('Source')"
v-model="transactionData.source"
doctype="LMS Source"
/>
<Link
:label="__('Payment For Document Type')"
v-model="transactionData.payment_for_document_type"
doctype="DocType"
/>
<Link
:label="__('Payment For Document')"
v-model="transactionData.payment_for_document"
:doctype="transactionData.payment_for_document_type"
/>
</div>
<div class="font-semibold mt-10">
{{ __('Payment Details') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Currency')"
v-model="transactionData.currency"
doctype="Currency"
/>
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
<FormControl
v-if="transactionData.amount_with_gst"
:label="__('Amount with GST')"
v-model="transactionData.amount_with_gst"
/>
</div>
<div v-if="transactionData.coupon">
<div class="font-semibold mt-10">
{{ __('Coupon Details') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<FormControl
v-if="transactionData.coupon"
:label="__('Coupon Code')"
v-model="transactionData.coupon"
/>
<FormControl
v-if="transactionData.coupon"
:label="__('Coupon Code')"
v-model="transactionData.coupon_code"
/>
<FormControl
v-if="transactionData.coupon"
:label="__('Discount Amount')"
v-model="transactionData.discount_amount"
/>
<FormControl
v-if="transactionData.coupon"
:label="__('Original Amount')"
v-model="transactionData.original_amount"
/>
</div>
</div>
<div class="font-semibold mt-10">
{{ __('Billing Details') }}
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
<Link
:label="__('Address')"
v-model="transactionData.address"
doctype="Address"
/>
<FormControl :label="__('GSTIN')" v-model="transactionData.gstin" />
<FormControl :label="__('PAN')" v-model="transactionData.pan" />
<FormControl
:label="__('Payment ID')"
v-model="transactionData.payment_id"
/>
<FormControl
:label="__('Order ID')"
v-model="transactionData.order_id"
/>
</div>
</div>
<div class="space-x-2 mt-auto ml-auto">
<Button @click="openDetails()">
{{ __('Open the ') }}
{{
data.payment_for_document_type == 'LMS Course'
? __('Course')
: __('Batch')
}}
</Button>
<Button variant="solid" @click="saveTransaction()">
{{ __('Save') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { Button, FormControl } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import { ChevronLeft } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue'
const router = useRouter()
const transactionData = ref<{ [key: string]: any } | null>(null)
const emit = defineEmits(['updateStep'])
const show = defineModel('show')
const props = defineProps<{
transactions: any
data: any
}>()
watch(
() => props.data,
(newVal) => {
transactionData.value = newVal ? { ...newVal } : null
},
{ immediate: true }
)
const saveTransaction = (close: () => void) => {
props.transactions.value.setValue
.submit({
...transactionData.value,
})
.then(() => {
close()
})
}
const openDetails = () => {
if (props.data) {
const docType = props.data.payment_for_document_type
const docName = props.data.payment_for_document
if (docType && docName) {
router.push({
name: docType == 'LMS Course' ? 'CourseDetail' : 'BatchDetail',
params: {
[docType == 'LMS Course' ? 'courseName' : 'batchName']: docName,
},
})
}
show.value = false
}
}
</script>

View File

@@ -73,8 +73,7 @@
:disabled="true"
/>
<div v-else-if="column.key == 'amount'">
{{ getCurrencySymbol(row['currency']) }}
{{ row['total_amount'] }}
{{ getCurrencySymbol(row['currency']) }} {{ row[column.key] }}
</div>
<div v-else class="leading-5 text-sm">
{{ row[column.key] }}
@@ -97,17 +96,10 @@
</div>
</div>
</div>
<TransactionDetails
v-model="showForm"
:transaction="currentTransaction"
v-model:transactions="transactions"
v-model:show="show"
/>
</template>
<script setup lang="ts">
import {
Button,
createListResource,
ListView,
ListHeader,
ListHeaderItem,
@@ -119,51 +111,19 @@ import {
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { RefreshCw } from 'lucide-vue-next'
import TransactionDetails from './TransactionDetails.vue'
import Link from '@/components/Controls/Link.vue'
const showForm = ref(false)
const currentTransaction = ref<{ [key: string]: any } | null>(null)
const show = defineModel('show')
const billingName = ref(null)
const paymentReceived = ref(false)
const paymentForCertificate = ref(false)
const member = ref(null)
const emit = defineEmits(['updateStep'])
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
})
const transactions = createListResource({
doctype: 'LMS Payment',
fields: [
'name',
'member',
'billing_name',
'source',
'payment_for_document_type',
'payment_for_document',
'payment_received',
'payment_for_certificate',
'currency',
'amount',
'total_amount',
'order_id',
'payment_id',
'gstin',
'pan',
'address',
],
auto: true,
orderBy: 'modified desc',
})
const props = defineProps<{
label: string
description: string
transactions: any
}>()
watch(
[billingName, member, paymentReceived, paymentForCertificate],
@@ -173,7 +133,7 @@ watch(
newPaymentReceived,
newPaymentForCertificate,
]) => {
transactions.update({
props.transactions.update({
filters: [
newBillingName ? [['billing_name', 'like', `%${newBillingName}%`]] : [],
newMember ? [['member', '=', newMember]] : [],
@@ -185,14 +145,13 @@ watch(
: [],
].flat(),
})
transactions.reload()
props.transactions.reload()
},
{ immediate: true }
)
const openForm = (transaction: { [key: string]: any }) => {
currentTransaction.value = transaction
showForm.value = true
emit('updateStep', 'details', { ...transaction })
}
const getCurrencySymbol = (currency: string) => {

View File

@@ -0,0 +1,66 @@
<template>
<TransactionList
v-if="step === 'list'"
:label="props.label"
:description="props.description"
:transactions="transactions"
@updateStep="updateStep"
/>
<TransactionDetails
v-else-if="step == 'details'"
:transactions="transactions"
:data="data"
v-model:show="show"
@updateStep="updateStep"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { createListResource } from 'frappe-ui'
import TransactionList from '@/components/Settings/Transactions/TransactionList.vue'
import TransactionDetails from '@/components/Settings/Transactions/TransactionDetails.vue'
const step = ref('list')
const data = ref<any | null>(null)
const show = defineModel('show')
const props = defineProps<{
label: string
description: string
}>()
const updateStep = (newStep: 'list' | 'new' | 'edit', newData: any) => {
step.value = newStep
if (newData) {
data.value = newData
}
}
const transactions = createListResource({
doctype: 'LMS Payment',
fields: [
'name',
'member',
'billing_name',
'source',
'payment_for_document_type',
'payment_for_document',
'payment_received',
'payment_for_certificate',
'currency',
'amount',
'amount_with_gst',
'coupon',
'coupon_code',
'discount_amount',
'original_amount',
'order_id',
'payment_id',
'gstin',
'pan',
'address',
],
auto: true,
orderBy: 'modified desc',
})
</script>

View File

@@ -70,6 +70,7 @@
@input="appliedCoupon = $event.target.value.toUpperCase()"
@keydown.enter="applyCouponCode"
placeholder="COUPON2025"
autocomplete="off"
class="flex-1 [&_input]:bg-white"
/>
<Button
@@ -237,7 +238,6 @@ const access = createResource({
const orderSummary = createResource({
url: 'lms.lms.utils.get_order_summary',
makeParams(values) {
console.log(appliedCoupon.value)
return {
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
docname: props.name,
@@ -282,6 +282,7 @@ const paymentLink = createResource({
redirect_to: redirectTo.value,
payment_for_certificate: props.type == 'certificate',
coupon_code: appliedCoupon.value,
coupon: orderSummary.data.coupon,
}
return data
},
@@ -384,8 +385,6 @@ const validateAddress = () => {
!states.includes(billingDetails.state)
)
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
console.log('validation address')
}
const showError = (err) => {

View File

@@ -2099,15 +2099,6 @@
"@vueuse/shared" "12.8.2"
vue "^3.5.13"
"@vueuse/core@^14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-14.0.0.tgz#a3d9520935a191b167cb91e08f698545e46bf0a6"
integrity sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==
dependencies:
"@types/web-bluetooth" "^0.0.21"
"@vueuse/metadata" "14.0.0"
"@vueuse/shared" "14.0.0"
"@vueuse/metadata@10.11.1":
version "10.11.1"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
@@ -2118,11 +2109,6 @@
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3"
integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==
"@vueuse/metadata@14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-14.0.0.tgz#139231dc8503f172a7a45ce1ceaa7a415befbf3c"
integrity sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==
"@vueuse/router@^12.7.0":
version "12.8.2"
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-12.8.2.tgz#3792eab50493e50a79767592a52f6c5bb441ef33"
@@ -2145,11 +2131,6 @@
dependencies:
vue "^3.5.13"
"@vueuse/shared@14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-14.0.0.tgz#15a424285fd6d453d1a99d1caba8cc293992868d"
integrity sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==
"@yr/monotone-cubic-spline@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9"
@@ -2329,9 +2310,9 @@ base64-js@^1.3.1:
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.8.25:
version "2.8.25"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz#947dc6f81778e0fa0424a2ab9ea09a3033e71109"
integrity sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==
version "2.8.26"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.26.tgz#2c7e2f840f0ae4d83782bdfe795229a43dfc3e59"
integrity sha512-73lC1ugzwoaWCLJ1LvOgrR5xsMLTqSKIEoMHVtL9E/HNk0PXtTM76ZIm84856/SF7Nv8mPZxKoBsgpm0tR1u1Q==
binary-extensions@^2.0.0:
version "2.3.0"
@@ -4274,9 +4255,9 @@ prosemirror-trailing-node@^3.0.0:
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.3, prosemirror-transform@^1.7.3:
version "1.10.4"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz#56419eac14f9f56612c806ae46f9238648f3f02e"
integrity sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==
version "1.10.5"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz#4cf9fe5dcbdbfebd62499f24386e7cec9bc9979b"
integrity sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==
dependencies:
prosemirror-model "^1.21.0"

View File

@@ -16,21 +16,21 @@
"payment_received",
"payment_for_certificate",
"payment_details_section",
"currency",
"amount",
"coupon",
"original_amount",
"discount_amount",
"gst_amount",
"total_amount",
"amount",
"amount_with_gst",
"column_break_yxpl",
"order_id",
"payment_id",
"currency",
"coupon",
"coupon_code",
"billing_details_section",
"address",
"payment_id",
"order_id",
"column_break_monu",
"gstin",
"pan",
"coupon_code"
"pan"
],
"fields": [
{
@@ -58,6 +58,7 @@
"options": "LMS Coupon"
},
{
"depends_on": "coupon",
"fieldname": "discount_amount",
"fieldtype": "Currency",
"label": "Discount Amount",
@@ -161,24 +162,25 @@
"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
},
{
"depends_on": "coupon",
"fetch_from": "coupon.code",
"fieldname": "coupon_code",
"fieldtype": "Data",
"label": "Coupon Code"
"label": "Coupon Code",
"read_only": 1
},
{
"depends_on": "eval:doc.currency == \"INR\"",
"fieldname": "amount_with_gst",
"fieldtype": "Currency",
"label": "Amount with GST"
},
{
"depends_on": "coupon",
"fieldname": "original_amount",
"fieldtype": "Currency",
"label": "Original Amount",
"options": "currency"
}
],
"index_web_pages_for_search": 1,
@@ -192,8 +194,8 @@
"link_fieldname": "payment"
}
],
"modified": "2025-10-13 18:52:13.542556",
"modified_by": "Administrator",
"modified": "2025-11-12 12:39:52.466297",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Payment",
"owner": "Administrator",

View File

@@ -9,11 +9,7 @@ from frappe.utils import add_days, flt, nowdate
class LMSPayment(Document):
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"))
pass
def send_payment_reminder():

View File

@@ -30,36 +30,31 @@ def get_payment_link(
redirect_to,
payment_for_certificate,
coupon_code=None,
coupon=None,
):
payment_gateway = get_payment_gateway()
address = frappe._dict(address)
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
original_amount = amount
amount -= discount_amount
amount_with_gst = get_amount_with_gst(amount, gst_amount)
payment = record_payment(
address,
doctype,
docname,
amount,
original_amount,
currency,
amount_with_gst,
discount_amount,
gst_amount,
payment_for_certificate,
coupon_context,
coupon_code,
coupon,
)
controller = get_controller(payment_gateway)
payment_details = {
"amount": amount - discount_amount + gst_amount,
"discount_amount": discount_amount,
"gst_amount": gst_amount,
"amount": amount_with_gst if amount_with_gst else amount,
"title": f"Payment for {doctype} {title} {docname}",
"description": f"{address.billing_name}'s payment for {title}",
"reference_doctype": doctype,
@@ -71,25 +66,41 @@ def get_payment_link(
"redirect_to": redirect_to,
"payment": payment.name,
}
if payment_gateway == "Razorpay":
order = controller.create_order(**payment_details)
payment_details.update({"order_id": order.get("id")})
create_order(payment_gateway, payment_details, controller)
url = controller.get_payment_url(**payment_details)
return url
def create_order(payment_gateway, payment_details, controller):
if payment_gateway != "Razorpay":
return
order = controller.create_order(**payment_details)
payment_details.update({"order_id": order.get("id")})
def get_amount_with_gst(amount, gst_amount):
amount_with_gst = 0
if gst_amount:
amount_with_gst = amount + gst_amount
return amount_with_gst
def record_payment(
address,
doctype,
docname,
amount,
original_amount,
currency,
amount_with_gst=0,
discount_amount=0,
gst_amount=0,
payment_for_certificate=0,
coupon_context=None,
coupon_code=None,
coupon=None,
):
address = frappe._dict(address)
address_name = save_address(address)
@@ -103,7 +114,7 @@ def record_payment(
"amount": amount,
"currency": currency,
"discount_amount": discount_amount,
"gst_amount": gst_amount,
"amount_with_gst": amount_with_gst,
"gstin": address.gstin,
"pan": address.pan,
"source": address.source,
@@ -112,16 +123,16 @@ def record_payment(
"payment_for_certificate": payment_for_certificate,
}
)
if coupon_context:
if coupon_code:
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"),
"coupon_code": coupon_context.get("coupon_code"),
"coupon": coupon,
"coupon_code": coupon_code,
"discount_amount": discount_amount,
"original_amount": original_amount,
}
)
payment_doc.save(ignore_permissions=True)
return payment_doc

View File

@@ -1753,61 +1753,82 @@ def get_discussion_replies(topic):
@frappe.whitelist()
def get_order_summary(doctype, docname, coupon=None, country=None):
if doctype == "LMS Course":
details = frappe.db.get_value(
"LMS Course",
docname,
[
"title",
"name",
"paid_course",
"paid_certificate",
"course_price as amount",
"currency",
"amount_usd",
],
as_dict=True,
)
if not details.paid_course and not details.paid_certificate:
raise frappe.throw(_("This course is free."))
else:
details = frappe.db.get_value(
"LMS Batch",
docname,
["title", "name", "paid_batch", "amount", "currency", "amount_usd"],
as_dict=True,
)
if not details.paid_batch:
raise frappe.throw(_("To join this batch, please contact the Administrator."))
details = get_paid_course_details(docname) if doctype == "LMS Course" else get_paid_batch_details(docname)
details.amount, details.currency = check_multicurrency(
details.amount, details.currency, country, details.amount_usd
)
details.original_amount = details.amount
details.original_amount_formatted = fmt_money(details.amount, 0, details.currency)
if coupon:
discount_amount, subtotal = apply_coupon(doctype, docname, coupon, details.amount)
details.amount = subtotal
details.discount_amount = discount_amount
details.discount_amount_formatted = fmt_money(discount_amount, 0, details.currency)
if details.currency == "INR":
details.amount, details.gst_applied = apply_gst(details.amount, country)
details.gst_amount_formatted = fmt_money(details.gst_applied, 0, details.currency)
adjust_amount_for_coupon(details, coupon, doctype, docname)
get_gst_details(details, country)
details.total_amount = details.amount
details.total_amount_formatted = fmt_money(details.amount, 0, details.currency)
return details
def get_paid_course_details(docname):
details = frappe.db.get_value(
"LMS Course",
docname,
[
"title",
"name",
"paid_course",
"paid_certificate",
"course_price as amount",
"currency",
"amount_usd",
],
as_dict=True,
)
if not details.paid_course and not details.paid_certificate:
raise frappe.throw(_("This course is free."))
return details
def get_paid_batch_details(docname):
details = frappe.db.get_value(
"LMS Batch",
docname,
["title", "name", "paid_batch", "amount", "currency", "amount_usd"],
as_dict=True,
)
if not details.paid_batch:
raise frappe.throw(_("To join this batch, please contact the Administrator."))
return details
def adjust_amount_for_coupon(details, coupon, doctype, docname):
if not coupon:
return
discount_amount, subtotal, coupon_name = apply_coupon(doctype, docname, coupon, details.amount)
details.amount = subtotal
details.discount_amount = discount_amount
details.discount_amount_formatted = fmt_money(discount_amount, 0, details.currency)
details.coupon = coupon_name
def get_gst_details(details, country):
if details.currency != "INR":
return
details.amount, details.gst_applied = apply_gst(details.amount, country)
details.gst_amount_formatted = fmt_money(details.gst_applied, 0, details.currency)
def apply_coupon(doctype, docname, code, base_amount):
coupon_name = frappe.db.exists("LMS Coupon", {"code": code, "enabled": 1})
if not coupon_name:
frappe.throw(_("Invalid or inactive coupon code."))
frappe.throw(_("The coupon code '{0}' is invalid.").format(code))
coupon = frappe.db.get_value(
"LMS Coupon",
@@ -1825,22 +1846,16 @@ def apply_coupon(doctype, docname, code, base_amount):
as_dict=True,
)
validate_coupon(doctype, code, coupon)
validate_coupon(code, coupon)
validate_coupon_applicability(doctype, docname, coupon_name)
discount_amount = calculate_discount_amount(base_amount, coupon)
subtotal = max(flt(base_amount) - flt(discount_amount), 0)
return discount_amount, subtotal
return discount_amount, subtotal, coupon_name
def validate_coupon(doctype, code, coupon):
if doctype not in ["LMS Course", "LMS Batch"]:
frappe.throw(_("Invalid doctype for coupon application."))
if not code:
frappe.throw(_("Coupon code is required."))
def validate_coupon(code, coupon):
if coupon.expires_on and getdate(coupon.expires_on) < getdate():
frappe.throw(_("This coupon has expired."))

1374
yarn.lock

File diff suppressed because it is too large Load Diff