mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
Merge pull request #969 from AbleKSaju/feat-pos-coupon-code
feat: Coupon Code functionality in POS
This commit is contained in:
commit
20ecdf8dc6
@ -1,11 +1,7 @@
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { ValidationMap } from 'fyo/model/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
|
||||
import { getApplicableCouponCodesName } from 'models/helpers';
|
||||
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
|
||||
import { validateCouponCode } from 'models/helpers';
|
||||
|
||||
export class AppliedCouponCodes extends InvoiceItem {
|
||||
coupons?: string;
|
||||
@ -16,92 +12,7 @@ export class AppliedCouponCodes extends InvoiceItem {
|
||||
return;
|
||||
}
|
||||
|
||||
const coupon = await this.fyo.db.getAll(ModelNameEnum.CouponCode, {
|
||||
fields: [
|
||||
'minAmount',
|
||||
'maxAmount',
|
||||
'pricingRule',
|
||||
'validFrom',
|
||||
'validTo',
|
||||
'maximumUse',
|
||||
'used',
|
||||
'isEnabled',
|
||||
],
|
||||
filters: { name: value as string },
|
||||
});
|
||||
|
||||
if (!coupon[0].isEnabled) {
|
||||
throw new ValidationError(
|
||||
'Coupon code cannot be applied as it is not enabled'
|
||||
);
|
||||
}
|
||||
|
||||
if ((coupon[0]?.maximumUse as number) <= (coupon[0]?.used as number)) {
|
||||
throw new ValidationError(
|
||||
'Coupon code has been used maximum number of times'
|
||||
);
|
||||
}
|
||||
|
||||
const applicableCouponCodesNames = await getApplicableCouponCodesName(
|
||||
value as string,
|
||||
this.parentdoc as SalesInvoice
|
||||
);
|
||||
|
||||
if (!applicableCouponCodesNames?.length) {
|
||||
throw new ValidationError(
|
||||
this.fyo.t`Coupon ${
|
||||
value as string
|
||||
} is not applicable for applied items.`
|
||||
);
|
||||
}
|
||||
|
||||
const couponExist = this.parentdoc?.coupons?.some(
|
||||
(coupon) => coupon?.coupons === value
|
||||
);
|
||||
|
||||
if (couponExist) {
|
||||
throw new ValidationError(
|
||||
this.fyo.t`${value as string} already applied.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(coupon[0].minAmount as Money).gte(
|
||||
this.parentdoc?.grandTotal as Money
|
||||
) &&
|
||||
!(coupon[0].minAmount as Money).isZero()
|
||||
) {
|
||||
throw new ValidationError(
|
||||
this.fyo.t`The Grand Total must exceed ${
|
||||
(coupon[0].minAmount as Money).float
|
||||
} to apply the coupon ${value as string}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(coupon[0].maxAmount as Money).lte(
|
||||
this.parentdoc?.grandTotal as Money
|
||||
) &&
|
||||
!(coupon[0].maxAmount as Money).isZero()
|
||||
) {
|
||||
throw new ValidationError(
|
||||
this.fyo.t`The Grand Total must be less than ${
|
||||
(coupon[0].maxAmount as Money).float
|
||||
} to apply this coupon.`
|
||||
);
|
||||
}
|
||||
|
||||
if ((coupon[0].validFrom as Date) > (this.parentdoc?.date as Date)) {
|
||||
throw new ValidationError(
|
||||
this.fyo.t`Valid From Date should be less than Valid To Date.`
|
||||
);
|
||||
}
|
||||
|
||||
if ((coupon[0].validTo as Date) < (this.parentdoc?.date as Date)) {
|
||||
throw new ValidationError(
|
||||
this.fyo.t`Valid To Date should be greater than Valid From Date.`
|
||||
);
|
||||
}
|
||||
await validateCouponCode(this as AppliedCouponCodes, value as string);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { Fyo } from 'fyo';
|
||||
import { DocValueMap } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
@ -1355,14 +1355,6 @@ export abstract class Invoice extends Transactional {
|
||||
);
|
||||
|
||||
if (duplicatePricingRule && duplicatePricingRule?.length >= 2) {
|
||||
const { showToast } = await import('src/utils/interactive');
|
||||
const message = t`Pricing Rule '${
|
||||
duplicatePricingRule[0]?.referenceName as string
|
||||
}' is already applied to item '${
|
||||
item.item as string
|
||||
}' in another batch.`;
|
||||
showToast({ type: 'error', message });
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,8 @@ import { isPesa } from 'fyo/utils';
|
||||
import { Party } from './baseModels/Party/Party';
|
||||
import { CouponCode } from './baseModels/CouponCode/CouponCode';
|
||||
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
|
||||
import { AppliedCouponCodes } from './baseModels/AppliedCouponCodes/AppliedCouponCodes';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
|
||||
export function getQuoteActions(
|
||||
fyo: Fyo,
|
||||
@ -794,8 +796,59 @@ export async function removeLoyaltyPoint(doc: Doc) {
|
||||
await party.updateLoyaltyPoints();
|
||||
}
|
||||
|
||||
export async function getPricingRulesOfCoupons(
|
||||
doc: SalesInvoice,
|
||||
couponName?: string,
|
||||
pricingRuleDocNames?: string[]
|
||||
): Promise<PricingRule[] | undefined> {
|
||||
if (!doc?.coupons?.length && !couponName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let appliedCoupons: CouponCode[] = [];
|
||||
|
||||
const couponsToFetch = couponName
|
||||
? [couponName]
|
||||
: (doc?.coupons?.map((coupon) => coupon.coupons) as string[] | []);
|
||||
|
||||
if (couponsToFetch?.length) {
|
||||
appliedCoupons = (await doc.fyo.db.getAll(ModelNameEnum.CouponCode, {
|
||||
fields: ['*'],
|
||||
filters: { name: ['in', couponsToFetch] },
|
||||
})) as CouponCode[];
|
||||
}
|
||||
|
||||
console.log('pricingRuleDocNames', pricingRuleDocNames);
|
||||
|
||||
const filteredPricingRuleNames = appliedCoupons.filter(
|
||||
(val) => val.pricingRule === pricingRuleDocNames![0]
|
||||
);
|
||||
|
||||
if (!filteredPricingRuleNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pricingRuleDocsForItem = (await doc.fyo.db.getAll(
|
||||
ModelNameEnum.PricingRule,
|
||||
{
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
name: ['in', pricingRuleDocNames as string[]],
|
||||
isEnabled: true,
|
||||
isCouponCodeBased: true,
|
||||
},
|
||||
orderBy: 'priority',
|
||||
order: 'desc',
|
||||
}
|
||||
)) as PricingRule[];
|
||||
|
||||
console.log('pricingRuleDocsForItem', pricingRuleDocsForItem);
|
||||
return pricingRuleDocsForItem;
|
||||
}
|
||||
|
||||
export async function getPricingRule(
|
||||
doc: Invoice
|
||||
doc: Invoice,
|
||||
couponName?: string
|
||||
): Promise<ApplicablePricingRules[] | undefined> {
|
||||
if (
|
||||
!doc.fyo.singles.AccountingSettings?.enablePricingRule ||
|
||||
@ -822,19 +875,41 @@ export async function getPricingRule(
|
||||
})
|
||||
).map((doc) => doc.parent) as string[];
|
||||
|
||||
const pricingRuleDocsForItem = (await doc.fyo.db.getAll(
|
||||
let pricingRuleDocsForItem;
|
||||
|
||||
const pricingRuleDocs = (await doc.fyo.db.getAll(
|
||||
ModelNameEnum.PricingRule,
|
||||
{
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
name: ['in', pricingRuleDocNames],
|
||||
isEnabled: true,
|
||||
isCouponCodeBased: false,
|
||||
},
|
||||
orderBy: 'priority',
|
||||
order: 'desc',
|
||||
}
|
||||
)) as PricingRule[];
|
||||
|
||||
if (pricingRuleDocs.length) {
|
||||
pricingRuleDocsForItem = pricingRuleDocs;
|
||||
}
|
||||
|
||||
if (!pricingRuleDocs.length || couponName) {
|
||||
const couponPricingRules: PricingRule[] | undefined =
|
||||
await getPricingRulesOfCoupons(
|
||||
doc as SalesInvoice,
|
||||
couponName,
|
||||
pricingRuleDocNames
|
||||
);
|
||||
|
||||
pricingRuleDocsForItem = couponPricingRules as PricingRule[];
|
||||
}
|
||||
|
||||
if (!pricingRuleDocsForItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filtered = filterPricingRules(
|
||||
pricingRuleDocsForItem,
|
||||
doc.date as Date,
|
||||
@ -857,6 +932,7 @@ export async function getPricingRule(
|
||||
pricingRule: filtered[0],
|
||||
});
|
||||
}
|
||||
|
||||
return pricingRules;
|
||||
}
|
||||
|
||||
@ -986,11 +1062,11 @@ export async function getApplicableCouponCodesName(
|
||||
}
|
||||
)) as CouponCode[];
|
||||
|
||||
if (!couponCodeDatas || couponCodeDatas.length === 0) {
|
||||
if (!couponCodeDatas || !couponCodeDatas.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const applicablePricingRules = await getPricingRule(sinvDoc);
|
||||
const applicablePricingRules = await getPricingRule(sinvDoc, couponName);
|
||||
|
||||
if (!applicablePricingRules?.length) {
|
||||
return [];
|
||||
@ -1006,6 +1082,137 @@ export async function getApplicableCouponCodesName(
|
||||
}));
|
||||
}
|
||||
|
||||
export async function validateCouponCode(
|
||||
doc: AppliedCouponCodes,
|
||||
value: string,
|
||||
sinvDoc?: SalesInvoice
|
||||
) {
|
||||
const coupon = await doc.fyo.db.getAll(ModelNameEnum.CouponCode, {
|
||||
fields: [
|
||||
'minAmount',
|
||||
'maxAmount',
|
||||
'pricingRule',
|
||||
'validFrom',
|
||||
'validTo',
|
||||
'maximumUse',
|
||||
'used',
|
||||
'isEnabled',
|
||||
],
|
||||
filters: { name: value },
|
||||
});
|
||||
|
||||
if (!coupon[0]?.isEnabled) {
|
||||
throw new ValidationError(
|
||||
'Coupon code cannot be applied as it is not enabled'
|
||||
);
|
||||
}
|
||||
|
||||
if ((coupon[0]?.maximumUse as number) <= (coupon[0]?.used as number)) {
|
||||
throw new ValidationError(
|
||||
'Coupon code has been used maximum number of times'
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc.parentdoc) {
|
||||
doc.parentdoc = sinvDoc;
|
||||
}
|
||||
|
||||
const applicableCouponCodesNames = await getApplicableCouponCodesName(
|
||||
value,
|
||||
doc.parentdoc as SalesInvoice
|
||||
);
|
||||
|
||||
if (!applicableCouponCodesNames?.length) {
|
||||
throw new ValidationError(
|
||||
t`Coupon ${value} is not applicable for applied items.`
|
||||
);
|
||||
}
|
||||
|
||||
const couponExist = doc.parentdoc?.coupons?.some(
|
||||
(coupon) => coupon?.coupons === value
|
||||
);
|
||||
|
||||
if (couponExist) {
|
||||
throw new ValidationError(t`${value} already applied.`);
|
||||
}
|
||||
|
||||
if (
|
||||
(coupon[0].minAmount as Money).gte(doc.parentdoc?.grandTotal as Money) &&
|
||||
!(coupon[0].minAmount as Money).isZero()
|
||||
) {
|
||||
throw new ValidationError(
|
||||
t`The Grand Total must exceed ${
|
||||
(coupon[0].minAmount as Money).float
|
||||
} to apply the coupon ${value}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(coupon[0].maxAmount as Money).lte(doc.parentdoc?.grandTotal as Money) &&
|
||||
!(coupon[0].maxAmount as Money).isZero()
|
||||
) {
|
||||
throw new ValidationError(
|
||||
t`The Grand Total must be less than ${
|
||||
(coupon[0].maxAmount as Money).float
|
||||
} to apply this coupon.`
|
||||
);
|
||||
}
|
||||
|
||||
if ((coupon[0].validFrom as Date) > (doc.parentdoc?.date as Date)) {
|
||||
throw new ValidationError(
|
||||
t`Valid From Date should be less than Valid To Date.`
|
||||
);
|
||||
}
|
||||
|
||||
if ((coupon[0].validTo as Date) < (doc.parentdoc?.date as Date)) {
|
||||
throw new ValidationError(
|
||||
t`Valid To Date should be greater than Valid From Date.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeFreeItems(sinvDoc: SalesInvoice) {
|
||||
if (!sinvDoc || !sinvDoc.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!sinvDoc.isPricingRuleApplied) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of sinvDoc.items) {
|
||||
if (item.isFreeItem) {
|
||||
sinvDoc.items = sinvDoc.items?.filter(
|
||||
(invoiceItem) => invoiceItem.name !== item.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePricingRule(sinvDoc: SalesInvoice) {
|
||||
const applicablePricingRuleNames = await getPricingRule(sinvDoc);
|
||||
|
||||
if (!applicablePricingRuleNames || !applicablePricingRuleNames.length) {
|
||||
sinvDoc.pricingRuleDetail = undefined;
|
||||
sinvDoc.isPricingRuleApplied = false;
|
||||
removeFreeItems(sinvDoc);
|
||||
return;
|
||||
}
|
||||
|
||||
const appliedPricingRuleCount = sinvDoc?.items?.filter(
|
||||
(val) => val.isFreeItem
|
||||
).length;
|
||||
|
||||
setTimeout(() => {
|
||||
(async () => {
|
||||
if (appliedPricingRuleCount !== applicablePricingRuleNames?.length) {
|
||||
await sinvDoc.appendPricingRuleDetail(applicablePricingRuleNames);
|
||||
await sinvDoc.applyProductDiscount();
|
||||
}
|
||||
})();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
export function getPricingRulesConflicts(
|
||||
pricingRules: PricingRule[]
|
||||
): undefined | boolean {
|
||||
|
@ -377,15 +377,15 @@ export default defineComponent({
|
||||
const pricingRule =
|
||||
(await this.row.parentdoc?.getPricingRule()) as ApplicablePricingRules[];
|
||||
|
||||
let appliedPricingRuleCount: number;
|
||||
setTimeout(async () => {
|
||||
if (appliedPricingRuleCount !== pricingRule?.length) {
|
||||
appliedPricingRuleCount = pricingRule?.length;
|
||||
await this.row.parentdoc?.appendPricingRuleDetail(pricingRule);
|
||||
let appliedPricingRuleCount =
|
||||
this.row.parentdoc?.pricingRuleDetail?.length;
|
||||
|
||||
await this.row.parentdoc?.applyProductDiscount();
|
||||
}
|
||||
}, 1);
|
||||
if (appliedPricingRuleCount !== pricingRule?.length) {
|
||||
appliedPricingRuleCount = pricingRule?.length;
|
||||
|
||||
await this.row.parentdoc?.appendPricingRuleDetail(pricingRule);
|
||||
await this.row.parentdoc?.applyProductDiscount();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,21 +1,28 @@
|
||||
import { Money } from "pesa";
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export type ItemQtyMap = {
|
||||
[item: string]: { availableQty: number;[batch: string]: number };
|
||||
}
|
||||
[item: string]: { availableQty: number; [batch: string]: number };
|
||||
};
|
||||
|
||||
export type ItemSerialNumbers = { [item: string]: string };
|
||||
|
||||
export type DiscountType = "percent" | "amount";
|
||||
export type DiscountType = 'percent' | 'amount';
|
||||
|
||||
export type ModalName = 'ShiftOpen' | 'ShiftClose' | 'Payment' | 'LoyaltyProgram' | 'SavedInvoice' | 'RouteToInvoiceList'
|
||||
export type ModalName =
|
||||
| 'ShiftOpen'
|
||||
| 'ShiftClose'
|
||||
| 'Payment'
|
||||
| 'LoyaltyProgram'
|
||||
| 'SavedInvoice'
|
||||
| 'RouteToInvoiceList'
|
||||
| 'CouponCode';
|
||||
|
||||
export interface POSItem {
|
||||
image?:string,
|
||||
name: string,
|
||||
rate: Money,
|
||||
availableQty: number,
|
||||
unit: string,
|
||||
hasBatch: boolean,
|
||||
hasSerialNumber: boolean,
|
||||
}
|
||||
image?: string;
|
||||
name: string;
|
||||
rate: Money;
|
||||
availableQty: number;
|
||||
unit: string;
|
||||
hasBatch: boolean;
|
||||
hasSerialNumber: boolean;
|
||||
}
|
||||
|
214
src/pages/POS/CouponCodeModal.vue
Normal file
214
src/pages/POS/CouponCodeModal.vue
Normal file
@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<Modal class="h-auto w-96" :set-close-listener="false">
|
||||
<p class="text-center font-semibold py-3">Apply Coupon Code</p>
|
||||
<div class="px-10">
|
||||
<hr class="dark:border-gray-800" />
|
||||
<p v-if="appliedCoupons.length" class="text-xs m-2 text-gray-500">
|
||||
{{ t`Applied Coupon Codes` }}
|
||||
</p>
|
||||
<div
|
||||
v-if="appliedCoupons.length"
|
||||
class="overflow-y-auto mt-2 custom-scroll custom-scroll-thumb2"
|
||||
:style="{ height: appliedCoupons.length >= 2 ? '11vh' : '8vh' }"
|
||||
>
|
||||
<Row
|
||||
v-for="(coupon,index) in appliedCoupons as any"
|
||||
:key="index"
|
||||
:ratio="ratio"
|
||||
:border="true"
|
||||
class="
|
||||
border-b border-l border-r
|
||||
dark:border-gray-800
|
||||
relative
|
||||
group
|
||||
h-coupon-mid
|
||||
hover:bg-gray-25
|
||||
dark:bg-gray-890
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<div class="flex flex-row w-full items-center">
|
||||
<div class="flex flex-row">
|
||||
<FormControl
|
||||
v-for="df in tableFields"
|
||||
:key="df.fieldname"
|
||||
size="large"
|
||||
class="w-full"
|
||||
:df="df"
|
||||
:value="coupon[df.fieldname]"
|
||||
:read-only="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute right-3">
|
||||
<feather-icon
|
||||
name="trash"
|
||||
class="w-4 text-xl text-red-500 cursor-pointer"
|
||||
@click="removeAppliedCoupon(coupon)"
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="coupons.fieldMap"
|
||||
class="flex justify-center"
|
||||
:class="appliedCoupons.length ? 'pb-0 pt-4' : 'pt-10'"
|
||||
>
|
||||
<div class="w-80" :class="appliedCoupons.length ? 'pb-4' : 'pb-10'">
|
||||
<Link
|
||||
v-if="coupons.fieldMap"
|
||||
class="flex-shrink-0"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="couponCode"
|
||||
:df="coupons.fieldMap.coupons"
|
||||
@change="updateCouponCode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto mb-2">
|
||||
<div class="col-span-2">
|
||||
<Button
|
||||
class="w-full bg-green-500 dark:bg-green-700"
|
||||
style="padding: 1.35rem"
|
||||
:disabled="validationError"
|
||||
@click="setCouponCode()"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Save` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto mb-8">
|
||||
<div class="col-span-2">
|
||||
<Button
|
||||
class="w-full bg-red-500 dark:bg-red-700"
|
||||
style="padding: 1.35rem"
|
||||
@click="cancelApplyCouponCode()"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import { t } from 'fyo';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
|
||||
import Link from 'src/components/Controls/Link.vue';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { updatePricingRule, validateCouponCode } from 'models/helpers';
|
||||
import { Field } from 'schemas/types';
|
||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||
import Row from 'src/components/Row.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CouponCodeModal',
|
||||
components: {
|
||||
Modal,
|
||||
Button,
|
||||
Link,
|
||||
FormControl,
|
||||
Row,
|
||||
},
|
||||
emits: ['setCouponsCount', 'toggleModal'],
|
||||
|
||||
setup() {
|
||||
return {
|
||||
sinvDoc: inject('sinvDoc') as SalesInvoice,
|
||||
coupons: inject('coupons') as AppliedCouponCodes,
|
||||
appliedCoupons: inject('appliedCoupons') as AppliedCouponCodes[],
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
validationError: false,
|
||||
couponCode: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ratio() {
|
||||
return [1, 0.1, 1, 0.7];
|
||||
},
|
||||
tableFields() {
|
||||
return [
|
||||
{
|
||||
fieldname: 'coupons',
|
||||
fieldtype: 'Link',
|
||||
required: true,
|
||||
readOnly: true,
|
||||
},
|
||||
] as Field[];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateCouponCode(value: string) {
|
||||
(this.validationError = false), (this.couponCode = value);
|
||||
},
|
||||
async setCouponCode() {
|
||||
try {
|
||||
if (!this.couponCode) {
|
||||
throw new Error(t`Must be select a coupon code`);
|
||||
}
|
||||
|
||||
const appliedCouponCodes = this.fyo.doc.getNewDoc(
|
||||
ModelNameEnum.AppliedCouponCodes
|
||||
);
|
||||
|
||||
await validateCouponCode(
|
||||
appliedCouponCodes as AppliedCouponCodes,
|
||||
this.couponCode,
|
||||
this.sinvDoc
|
||||
);
|
||||
|
||||
await this.sinvDoc.append('coupons', { coupons: this.couponCode });
|
||||
|
||||
await updatePricingRule(this.sinvDoc);
|
||||
this.$emit('toggleModal', 'CouponCode');
|
||||
|
||||
this.couponCode = '';
|
||||
this.validationError = false;
|
||||
|
||||
this.$emit('setCouponsCount', this.sinvDoc.coupons?.length);
|
||||
} catch (error) {
|
||||
this.validationError = true;
|
||||
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async removeAppliedCoupon(coupon: AppliedCouponCodes) {
|
||||
this.sinvDoc.coupons = this.sinvDoc.coupons?.filter(
|
||||
(coup) => coup.coupons !== coupon?.coupons
|
||||
);
|
||||
|
||||
await updatePricingRule(this.sinvDoc);
|
||||
this.$emit('setCouponsCount', this.sinvDoc.coupons?.length);
|
||||
},
|
||||
cancelApplyCouponCode() {
|
||||
this.couponCode = '';
|
||||
this.$emit('toggleModal', 'CouponCode');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@ -36,6 +36,12 @@
|
||||
@toggle-modal="toggleModal"
|
||||
/>
|
||||
|
||||
<CouponCodeModal
|
||||
:open-modal="openCouponCodeModal"
|
||||
@set-coupons-count="setCouponsCount"
|
||||
@toggle-modal="toggleModal"
|
||||
/>
|
||||
|
||||
<PaymentModal
|
||||
:open-modal="openPaymentModal"
|
||||
@create-transaction="createTransaction"
|
||||
@ -43,6 +49,7 @@
|
||||
@set-cash-amount="setCashAmount"
|
||||
@set-transfer-amount="setTransferAmount"
|
||||
@set-transfer-ref-no="setTransferRefNo"
|
||||
@set-coupons-count="setCouponsCount"
|
||||
@set-transfer-clearance-date="setTransferClearanceDate"
|
||||
/>
|
||||
|
||||
@ -105,6 +112,7 @@
|
||||
:item-qty-map="itemQtyMap"
|
||||
@add-item="addItem"
|
||||
/>
|
||||
|
||||
<ItemsGrid
|
||||
v-else
|
||||
:items="items"
|
||||
@ -197,6 +205,7 @@
|
||||
Loyalty Program
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<div class="p-1 rounded-md bg-gray-100" @click="routeToSinvList">
|
||||
<svg
|
||||
@ -236,6 +245,122 @@
|
||||
Sales Invoice List
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="p-0.5 rounded-md bg-gray-100"
|
||||
:class="{
|
||||
hidden: !fyo.singles.AccountingSettings?.enableCouponCode,
|
||||
'bg-gray-100': loyaltyPoints,
|
||||
'dark:bg-gray-600 cursor-not-allowed':
|
||||
!sinvDoc.party || !sinvDoc.items?.length,
|
||||
}"
|
||||
@click="openCouponModal()"
|
||||
>
|
||||
<svg
|
||||
fill="#000000"
|
||||
width="28px"
|
||||
height="28px"
|
||||
viewBox="0 0 512.00 512.00"
|
||||
enable-background="new 0 0 512 512"
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
stroke="#000000"
|
||||
stroke-width="9.312000000000001"
|
||||
transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke="#CCCCCC"
|
||||
stroke-width="19.456"
|
||||
></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<g id="Layer_1"></g>
|
||||
<g id="Layer_2">
|
||||
<g>
|
||||
<path
|
||||
d="M412.7,134.4H229.6c-2,0-3.9,0.8-5.3,2.2l-27.8,27.8L169.1,137c-1.4-1.4-3.3-2.2-5.3-2.2H99.3c-4.1,0-7.5,3.4-7.5,7.5 v227.4c0,4.1,3.4,7.5,7.5,7.5h64.5c2,0,3.9-0.8,5.3-2.2l27.4-27.4l27.8,27.8c1.4,1.4,3.3,2.2,5.3,2.2h183.1c4.1,0,7.5-3.4,7.5-7.5 V141.9C420.2,137.7,416.8,134.4,412.7,134.4z M405.2,362.6H232.7l-30.9-30.9c-2.9-2.9-7.7-2.9-10.6,0l-30.5,30.5h-53.9V149.8h53.9 l30.5,30.5c2.9,2.9,7.7,2.9,10.6,0l30.9-30.9h172.5V362.6z"
|
||||
></path>
|
||||
<path
|
||||
d="M276.9,235.2c15.4,0,28-12.6,28-28s-12.6-28-28-28s-28,12.6-28,28S261.4,235.2,276.9,235.2z M276.9,194.2 c7.2,0,13,5.8,13,13s-5.8,13-13,13s-13-5.8-13-13S269.7,194.2,276.9,194.2z"
|
||||
></path>
|
||||
<path
|
||||
d="M360,262.4c-15.4,0-28,12.6-28,28s12.6,28,28,28s28-12.6,28-28S375.4,262.4,360,262.4z M360,303.4c-7.2,0-13-5.8-13-13 s5.8-13,13-13s13,5.8,13,13S367.2,303.4,360,303.4z"
|
||||
></path>
|
||||
<path
|
||||
d="M256.6,310.7c1.5,1.5,3.4,2.2,5.3,2.2s3.8-0.7,5.3-2.2l113.1-113.1c2.9-2.9,2.9-7.7,0-10.6c-2.9-2.9-7.7-2.9-10.6,0 L256.6,300.1C253.6,303,253.6,307.7,256.6,310.7z"
|
||||
></path>
|
||||
<path
|
||||
d="M196.5,202.5c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,203.3,198.4,202.5,196.5,202.5z"
|
||||
></path>
|
||||
<path
|
||||
d="M196.5,233.2c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,234,198.4,233.2,196.5,233.2z"
|
||||
></path>
|
||||
<path
|
||||
d="M196.5,263.8c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,264.6,198.4,263.8,196.5,263.8z"
|
||||
></path>
|
||||
<path
|
||||
d="M196.5,294.5c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,295.3,198.4,294.5,196.5,294.5z"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="
|
||||
absolute
|
||||
bottom-full
|
||||
left-1/2
|
||||
transform
|
||||
-translate-x-1/2
|
||||
mb-2
|
||||
bg-gray-100
|
||||
dark:bg-gray-850 dark:text-white
|
||||
text-black text-xs
|
||||
rounded-md
|
||||
p-2
|
||||
w-28
|
||||
text-center
|
||||
opacity-0
|
||||
group-hover:opacity-100
|
||||
transition-opacity
|
||||
duration-300
|
||||
"
|
||||
>
|
||||
Coupon Code
|
||||
</span>
|
||||
<div
|
||||
v-if="appliedCouponsCount !== 0"
|
||||
class="
|
||||
absolute
|
||||
top-0
|
||||
right-0
|
||||
transform
|
||||
translate-x-1/2
|
||||
-translate-y-1/2
|
||||
h-4
|
||||
w-4
|
||||
bg-green-400
|
||||
text-green-900
|
||||
rounded-full
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
text-xs
|
||||
cursor-pointer
|
||||
border-red-500
|
||||
p-2
|
||||
"
|
||||
>
|
||||
{{ appliedCouponsCount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -430,10 +555,16 @@ import {
|
||||
validateSinv,
|
||||
} from 'src/utils/pos';
|
||||
import Barcode from 'src/components/Controls/Barcode.vue';
|
||||
import { getAddedLPWithGrandTotal, getPricingRule } from 'models/helpers';
|
||||
import {
|
||||
getAddedLPWithGrandTotal,
|
||||
getPricingRule,
|
||||
removeFreeItems,
|
||||
} from 'models/helpers';
|
||||
import LoyaltyProgramModal from './LoyaltyprogramModal.vue';
|
||||
import AlertModal from './AlertModal.vue';
|
||||
import SavedInvoiceModal from './SavedInvoiceModal.vue';
|
||||
import CouponCodeModal from './CouponCodeModal.vue';
|
||||
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'POS',
|
||||
@ -451,6 +582,7 @@ export default defineComponent({
|
||||
PaymentModal,
|
||||
LoyaltyProgramModal,
|
||||
SavedInvoiceModal,
|
||||
CouponCodeModal,
|
||||
SelectedItemTable,
|
||||
Barcode,
|
||||
},
|
||||
@ -463,6 +595,8 @@ export default defineComponent({
|
||||
itemQtyMap: computed(() => this.itemQtyMap),
|
||||
itemSerialNumbers: computed(() => this.itemSerialNumbers),
|
||||
sinvDoc: computed(() => this.sinvDoc),
|
||||
appliedCoupons: computed(() => this.sinvDoc.coupons),
|
||||
coupons: computed(() => this.coupons),
|
||||
totalTaxedAmount: computed(() => this.totalTaxedAmount),
|
||||
transferAmount: computed(() => this.transferAmount),
|
||||
transferClearanceDate: computed(() => this.transferClearanceDate),
|
||||
@ -479,6 +613,8 @@ export default defineComponent({
|
||||
openPaymentModal: false,
|
||||
openLoyaltyProgramModal: false,
|
||||
openSavedInvoiceModal: false,
|
||||
openCouponCodeModal: false,
|
||||
openAppliedCouponsModal: false,
|
||||
openShiftCloseModal: false,
|
||||
openShiftOpenModal: false,
|
||||
openRouteToInvoiceListModal: false,
|
||||
@ -495,6 +631,9 @@ export default defineComponent({
|
||||
appliedLoyaltyPoints: 0,
|
||||
loyaltyProgram: '' as string,
|
||||
|
||||
appliedCoupons: [] as AppliedCouponCodes[],
|
||||
appliedCouponsCount: 0,
|
||||
|
||||
defaultCustomer: undefined as string | undefined,
|
||||
itemSearchTerm: '',
|
||||
transferRefNo: undefined as string | undefined,
|
||||
@ -505,6 +644,7 @@ export default defineComponent({
|
||||
itemSerialNumbers: {} as ItemSerialNumbers,
|
||||
paymentDoc: {} as Payment,
|
||||
sinvDoc: {} as SalesInvoice,
|
||||
coupons: {} as AppliedCouponCodes,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -543,12 +683,14 @@ export default defineComponent({
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.setItems();
|
||||
},
|
||||
async activated() {
|
||||
toggleSidebar(false);
|
||||
validateIsPosSettingsSet(fyo);
|
||||
this.setCouponCodeDoc();
|
||||
this.setSinvDoc();
|
||||
this.setDefaultCustomer();
|
||||
await this.setItemQtyMap();
|
||||
@ -648,6 +790,14 @@ export default defineComponent({
|
||||
isPOS: true,
|
||||
}) as SalesInvoice;
|
||||
},
|
||||
setCouponCodeDoc() {
|
||||
this.coupons = this.fyo.doc.getNewDoc(
|
||||
ModelNameEnum.AppliedCouponCodes
|
||||
) as AppliedCouponCodes;
|
||||
},
|
||||
setAppliedCoupons() {
|
||||
this.appliedCoupons = this.sinvDoc.coupons as AppliedCouponCodes[];
|
||||
},
|
||||
setTotalQuantity() {
|
||||
this.totalQuantity = getTotalQuantity(
|
||||
this.sinvDoc.items as SalesInvoiceItem[]
|
||||
@ -656,6 +806,9 @@ export default defineComponent({
|
||||
setTotalTaxedAmount() {
|
||||
this.totalTaxedAmount = getTotalTaxedAmount(this.sinvDoc as SalesInvoice);
|
||||
},
|
||||
setCouponsCount(value: number) {
|
||||
this.appliedCouponsCount = value;
|
||||
},
|
||||
async setLoyaltyPoints(value: number) {
|
||||
this.appliedLoyaltyPoints = value;
|
||||
|
||||
@ -691,23 +844,6 @@ export default defineComponent({
|
||||
setTransferRefNo(ref: string) {
|
||||
this.transferRefNo = ref;
|
||||
},
|
||||
removeFreeItems() {
|
||||
if (!this.sinvDoc || !this.sinvDoc.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!this.sinvDoc.isPricingRuleApplied) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of this.sinvDoc.items) {
|
||||
if (item.isFreeItem) {
|
||||
this.sinvDoc.items = this.sinvDoc.items?.filter(
|
||||
(invoiceItem) => invoiceItem.name !== item.name
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async addItem(item: POSItem | Item | undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@ -867,6 +1003,11 @@ export default defineComponent({
|
||||
});
|
||||
}
|
||||
},
|
||||
openCouponModal() {
|
||||
if (this.sinvDoc.party && this.sinvDoc.items?.length) {
|
||||
this.toggleModal('CouponCode', true);
|
||||
}
|
||||
},
|
||||
async submitSinvDoc(shouldPrint: boolean) {
|
||||
this.sinvDoc.once('afterSubmit', async () => {
|
||||
showToast({
|
||||
@ -940,15 +1081,15 @@ export default defineComponent({
|
||||
if (!hasPricingRules || !hasPricingRules.length) {
|
||||
this.sinvDoc.pricingRuleDetail = undefined;
|
||||
this.sinvDoc.isPricingRuleApplied = false;
|
||||
this.removeFreeItems();
|
||||
removeFreeItems(this.sinvDoc as SalesInvoice);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
const appliedPricingRuleCount = this.sinvDoc?.items?.filter(
|
||||
(val) => val.isFreeItem
|
||||
).length;
|
||||
const appliedPricingRuleCount = this.sinvDoc?.items?.filter(
|
||||
(val) => val.isFreeItem
|
||||
).length;
|
||||
|
||||
setTimeout(async () => {
|
||||
if (appliedPricingRuleCount !== hasPricingRules?.length) {
|
||||
await this.sinvDoc.appendPricingRuleDetail(hasPricingRules);
|
||||
await this.sinvDoc.applyProductDiscount();
|
||||
|
@ -169,7 +169,7 @@
|
||||
class="w-full bg-blue-500"
|
||||
style="padding: 1.35rem"
|
||||
:disabled="disableSubmitButton"
|
||||
@click="$emit('createTransaction')"
|
||||
@click="submitTransaction()"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
@ -224,6 +224,7 @@ export default defineComponent({
|
||||
'setTransferClearanceDate',
|
||||
'setTransferRefNo',
|
||||
'toggleModal',
|
||||
'setCouponsCount',
|
||||
],
|
||||
setup() {
|
||||
return {
|
||||
@ -320,6 +321,10 @@ export default defineComponent({
|
||||
this.$emit('setTransferAmount', fyo.pesa(0));
|
||||
this.$emit('setCashAmount', this.sinvDoc?.grandTotal);
|
||||
},
|
||||
submitTransaction() {
|
||||
this.$emit('createTransaction');
|
||||
this.$emit('setCouponsCount', 0);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user