2
0
mirror of https://github.com/frappe/books.git synced 2024-11-14 01:14:03 +00:00

Merge pull request #969 from AbleKSaju/feat-pos-coupon-code

feat: Coupon Code functionality in POS
This commit is contained in:
Akshay 2024-10-22 16:05:26 +05:30 committed by GitHub
commit 20ecdf8dc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 626 additions and 149 deletions

View File

@ -1,11 +1,7 @@
import { DocValue } from 'fyo/core/types'; import { DocValue } from 'fyo/core/types';
import { ValidationMap } from 'fyo/model/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 { InvoiceItem } from '../InvoiceItem/InvoiceItem';
import { getApplicableCouponCodesName } from 'models/helpers'; import { validateCouponCode } from 'models/helpers';
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
export class AppliedCouponCodes extends InvoiceItem { export class AppliedCouponCodes extends InvoiceItem {
coupons?: string; coupons?: string;
@ -16,92 +12,7 @@ export class AppliedCouponCodes extends InvoiceItem {
return; return;
} }
const coupon = await this.fyo.db.getAll(ModelNameEnum.CouponCode, { await validateCouponCode(this as AppliedCouponCodes, value as string);
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.`
);
}
}, },
}; };
} }

View File

@ -1,4 +1,4 @@
import { Fyo, t } from 'fyo'; import { Fyo } from 'fyo';
import { DocValueMap } from 'fyo/core/types'; import { DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { import {
@ -1355,14 +1355,6 @@ export abstract class Invoice extends Transactional {
); );
if (duplicatePricingRule && duplicatePricingRule?.length >= 2) { 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; continue;
} }

View File

@ -30,6 +30,8 @@ import { isPesa } from 'fyo/utils';
import { Party } from './baseModels/Party/Party'; import { Party } from './baseModels/Party/Party';
import { CouponCode } from './baseModels/CouponCode/CouponCode'; import { CouponCode } from './baseModels/CouponCode/CouponCode';
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice'; import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
import { AppliedCouponCodes } from './baseModels/AppliedCouponCodes/AppliedCouponCodes';
import { ValidationError } from 'fyo/utils/errors';
export function getQuoteActions( export function getQuoteActions(
fyo: Fyo, fyo: Fyo,
@ -794,8 +796,59 @@ export async function removeLoyaltyPoint(doc: Doc) {
await party.updateLoyaltyPoints(); 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( export async function getPricingRule(
doc: Invoice doc: Invoice,
couponName?: string
): Promise<ApplicablePricingRules[] | undefined> { ): Promise<ApplicablePricingRules[] | undefined> {
if ( if (
!doc.fyo.singles.AccountingSettings?.enablePricingRule || !doc.fyo.singles.AccountingSettings?.enablePricingRule ||
@ -822,19 +875,41 @@ export async function getPricingRule(
}) })
).map((doc) => doc.parent) as string[]; ).map((doc) => doc.parent) as string[];
const pricingRuleDocsForItem = (await doc.fyo.db.getAll( let pricingRuleDocsForItem;
const pricingRuleDocs = (await doc.fyo.db.getAll(
ModelNameEnum.PricingRule, ModelNameEnum.PricingRule,
{ {
fields: ['*'], fields: ['*'],
filters: { filters: {
name: ['in', pricingRuleDocNames], name: ['in', pricingRuleDocNames],
isEnabled: true, isEnabled: true,
isCouponCodeBased: false,
}, },
orderBy: 'priority', orderBy: 'priority',
order: 'desc', order: 'desc',
} }
)) as PricingRule[]; )) 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( const filtered = filterPricingRules(
pricingRuleDocsForItem, pricingRuleDocsForItem,
doc.date as Date, doc.date as Date,
@ -857,6 +932,7 @@ export async function getPricingRule(
pricingRule: filtered[0], pricingRule: filtered[0],
}); });
} }
return pricingRules; return pricingRules;
} }
@ -986,11 +1062,11 @@ export async function getApplicableCouponCodesName(
} }
)) as CouponCode[]; )) as CouponCode[];
if (!couponCodeDatas || couponCodeDatas.length === 0) { if (!couponCodeDatas || !couponCodeDatas.length) {
return []; return [];
} }
const applicablePricingRules = await getPricingRule(sinvDoc); const applicablePricingRules = await getPricingRule(sinvDoc, couponName);
if (!applicablePricingRules?.length) { if (!applicablePricingRules?.length) {
return []; 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( export function getPricingRulesConflicts(
pricingRules: PricingRule[] pricingRules: PricingRule[]
): undefined | boolean { ): undefined | boolean {

View File

@ -377,15 +377,15 @@ export default defineComponent({
const pricingRule = const pricingRule =
(await this.row.parentdoc?.getPricingRule()) as ApplicablePricingRules[]; (await this.row.parentdoc?.getPricingRule()) as ApplicablePricingRules[];
let appliedPricingRuleCount: number; let appliedPricingRuleCount =
setTimeout(async () => { this.row.parentdoc?.pricingRuleDetail?.length;
if (appliedPricingRuleCount !== pricingRule?.length) {
appliedPricingRuleCount = pricingRule?.length;
await this.row.parentdoc?.appendPricingRuleDetail(pricingRule);
await this.row.parentdoc?.applyProductDiscount(); if (appliedPricingRuleCount !== pricingRule?.length) {
} appliedPricingRuleCount = pricingRule?.length;
}, 1);
await this.row.parentdoc?.appendPricingRuleDetail(pricingRule);
await this.row.parentdoc?.applyProductDiscount();
}
}, },
}, },
}); });

View File

@ -1,21 +1,28 @@
import { Money } from "pesa"; import { Money } from 'pesa';
export type ItemQtyMap = { 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 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 { export interface POSItem {
image?:string, image?: string;
name: string, name: string;
rate: Money, rate: Money;
availableQty: number, availableQty: number;
unit: string, unit: string;
hasBatch: boolean, hasBatch: boolean;
hasSerialNumber: boolean, hasSerialNumber: boolean;
} }

View 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>

View File

@ -36,6 +36,12 @@
@toggle-modal="toggleModal" @toggle-modal="toggleModal"
/> />
<CouponCodeModal
:open-modal="openCouponCodeModal"
@set-coupons-count="setCouponsCount"
@toggle-modal="toggleModal"
/>
<PaymentModal <PaymentModal
:open-modal="openPaymentModal" :open-modal="openPaymentModal"
@create-transaction="createTransaction" @create-transaction="createTransaction"
@ -43,6 +49,7 @@
@set-cash-amount="setCashAmount" @set-cash-amount="setCashAmount"
@set-transfer-amount="setTransferAmount" @set-transfer-amount="setTransferAmount"
@set-transfer-ref-no="setTransferRefNo" @set-transfer-ref-no="setTransferRefNo"
@set-coupons-count="setCouponsCount"
@set-transfer-clearance-date="setTransferClearanceDate" @set-transfer-clearance-date="setTransferClearanceDate"
/> />
@ -105,6 +112,7 @@
:item-qty-map="itemQtyMap" :item-qty-map="itemQtyMap"
@add-item="addItem" @add-item="addItem"
/> />
<ItemsGrid <ItemsGrid
v-else v-else
:items="items" :items="items"
@ -197,6 +205,7 @@
Loyalty Program Loyalty Program
</span> </span>
</div> </div>
<div class="relative group"> <div class="relative group">
<div class="p-1 rounded-md bg-gray-100" @click="routeToSinvList"> <div class="p-1 rounded-md bg-gray-100" @click="routeToSinvList">
<svg <svg
@ -236,6 +245,122 @@
Sales Invoice List Sales Invoice List
</span> </span>
</div> </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> </div>
</div> </div>
@ -430,10 +555,16 @@ import {
validateSinv, validateSinv,
} from 'src/utils/pos'; } from 'src/utils/pos';
import Barcode from 'src/components/Controls/Barcode.vue'; 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 LoyaltyProgramModal from './LoyaltyprogramModal.vue';
import AlertModal from './AlertModal.vue'; import AlertModal from './AlertModal.vue';
import SavedInvoiceModal from './SavedInvoiceModal.vue'; import SavedInvoiceModal from './SavedInvoiceModal.vue';
import CouponCodeModal from './CouponCodeModal.vue';
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
export default defineComponent({ export default defineComponent({
name: 'POS', name: 'POS',
@ -451,6 +582,7 @@ export default defineComponent({
PaymentModal, PaymentModal,
LoyaltyProgramModal, LoyaltyProgramModal,
SavedInvoiceModal, SavedInvoiceModal,
CouponCodeModal,
SelectedItemTable, SelectedItemTable,
Barcode, Barcode,
}, },
@ -463,6 +595,8 @@ export default defineComponent({
itemQtyMap: computed(() => this.itemQtyMap), itemQtyMap: computed(() => this.itemQtyMap),
itemSerialNumbers: computed(() => this.itemSerialNumbers), itemSerialNumbers: computed(() => this.itemSerialNumbers),
sinvDoc: computed(() => this.sinvDoc), sinvDoc: computed(() => this.sinvDoc),
appliedCoupons: computed(() => this.sinvDoc.coupons),
coupons: computed(() => this.coupons),
totalTaxedAmount: computed(() => this.totalTaxedAmount), totalTaxedAmount: computed(() => this.totalTaxedAmount),
transferAmount: computed(() => this.transferAmount), transferAmount: computed(() => this.transferAmount),
transferClearanceDate: computed(() => this.transferClearanceDate), transferClearanceDate: computed(() => this.transferClearanceDate),
@ -479,6 +613,8 @@ export default defineComponent({
openPaymentModal: false, openPaymentModal: false,
openLoyaltyProgramModal: false, openLoyaltyProgramModal: false,
openSavedInvoiceModal: false, openSavedInvoiceModal: false,
openCouponCodeModal: false,
openAppliedCouponsModal: false,
openShiftCloseModal: false, openShiftCloseModal: false,
openShiftOpenModal: false, openShiftOpenModal: false,
openRouteToInvoiceListModal: false, openRouteToInvoiceListModal: false,
@ -495,6 +631,9 @@ export default defineComponent({
appliedLoyaltyPoints: 0, appliedLoyaltyPoints: 0,
loyaltyProgram: '' as string, loyaltyProgram: '' as string,
appliedCoupons: [] as AppliedCouponCodes[],
appliedCouponsCount: 0,
defaultCustomer: undefined as string | undefined, defaultCustomer: undefined as string | undefined,
itemSearchTerm: '', itemSearchTerm: '',
transferRefNo: undefined as string | undefined, transferRefNo: undefined as string | undefined,
@ -505,6 +644,7 @@ export default defineComponent({
itemSerialNumbers: {} as ItemSerialNumbers, itemSerialNumbers: {} as ItemSerialNumbers,
paymentDoc: {} as Payment, paymentDoc: {} as Payment,
sinvDoc: {} as SalesInvoice, sinvDoc: {} as SalesInvoice,
coupons: {} as AppliedCouponCodes,
}; };
}, },
computed: { computed: {
@ -543,12 +683,14 @@ export default defineComponent({
deep: true, deep: true,
}, },
}, },
async mounted() { async mounted() {
await this.setItems(); await this.setItems();
}, },
async activated() { async activated() {
toggleSidebar(false); toggleSidebar(false);
validateIsPosSettingsSet(fyo); validateIsPosSettingsSet(fyo);
this.setCouponCodeDoc();
this.setSinvDoc(); this.setSinvDoc();
this.setDefaultCustomer(); this.setDefaultCustomer();
await this.setItemQtyMap(); await this.setItemQtyMap();
@ -648,6 +790,14 @@ export default defineComponent({
isPOS: true, isPOS: true,
}) as SalesInvoice; }) as SalesInvoice;
}, },
setCouponCodeDoc() {
this.coupons = this.fyo.doc.getNewDoc(
ModelNameEnum.AppliedCouponCodes
) as AppliedCouponCodes;
},
setAppliedCoupons() {
this.appliedCoupons = this.sinvDoc.coupons as AppliedCouponCodes[];
},
setTotalQuantity() { setTotalQuantity() {
this.totalQuantity = getTotalQuantity( this.totalQuantity = getTotalQuantity(
this.sinvDoc.items as SalesInvoiceItem[] this.sinvDoc.items as SalesInvoiceItem[]
@ -656,6 +806,9 @@ export default defineComponent({
setTotalTaxedAmount() { setTotalTaxedAmount() {
this.totalTaxedAmount = getTotalTaxedAmount(this.sinvDoc as SalesInvoice); this.totalTaxedAmount = getTotalTaxedAmount(this.sinvDoc as SalesInvoice);
}, },
setCouponsCount(value: number) {
this.appliedCouponsCount = value;
},
async setLoyaltyPoints(value: number) { async setLoyaltyPoints(value: number) {
this.appliedLoyaltyPoints = value; this.appliedLoyaltyPoints = value;
@ -691,23 +844,6 @@ export default defineComponent({
setTransferRefNo(ref: string) { setTransferRefNo(ref: string) {
this.transferRefNo = ref; 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) { async addItem(item: POSItem | Item | undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // 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) { async submitSinvDoc(shouldPrint: boolean) {
this.sinvDoc.once('afterSubmit', async () => { this.sinvDoc.once('afterSubmit', async () => {
showToast({ showToast({
@ -940,15 +1081,15 @@ export default defineComponent({
if (!hasPricingRules || !hasPricingRules.length) { if (!hasPricingRules || !hasPricingRules.length) {
this.sinvDoc.pricingRuleDetail = undefined; this.sinvDoc.pricingRuleDetail = undefined;
this.sinvDoc.isPricingRuleApplied = false; this.sinvDoc.isPricingRuleApplied = false;
this.removeFreeItems(); removeFreeItems(this.sinvDoc as SalesInvoice);
return; return;
} }
setTimeout(async () => { const appliedPricingRuleCount = this.sinvDoc?.items?.filter(
const appliedPricingRuleCount = this.sinvDoc?.items?.filter( (val) => val.isFreeItem
(val) => val.isFreeItem ).length;
).length;
setTimeout(async () => {
if (appliedPricingRuleCount !== hasPricingRules?.length) { if (appliedPricingRuleCount !== hasPricingRules?.length) {
await this.sinvDoc.appendPricingRuleDetail(hasPricingRules); await this.sinvDoc.appendPricingRuleDetail(hasPricingRules);
await this.sinvDoc.applyProductDiscount(); await this.sinvDoc.applyProductDiscount();

View File

@ -169,7 +169,7 @@
class="w-full bg-blue-500" class="w-full bg-blue-500"
style="padding: 1.35rem" style="padding: 1.35rem"
:disabled="disableSubmitButton" :disabled="disableSubmitButton"
@click="$emit('createTransaction')" @click="submitTransaction()"
> >
<slot> <slot>
<p class="uppercase text-lg text-white font-semibold"> <p class="uppercase text-lg text-white font-semibold">
@ -224,6 +224,7 @@ export default defineComponent({
'setTransferClearanceDate', 'setTransferClearanceDate',
'setTransferRefNo', 'setTransferRefNo',
'toggleModal', 'toggleModal',
'setCouponsCount',
], ],
setup() { setup() {
return { return {
@ -320,6 +321,10 @@ export default defineComponent({
this.$emit('setTransferAmount', fyo.pesa(0)); this.$emit('setTransferAmount', fyo.pesa(0));
this.$emit('setCashAmount', this.sinvDoc?.grandTotal); this.$emit('setCashAmount', this.sinvDoc?.grandTotal);
}, },
submitTransaction() {
this.$emit('createTransaction');
this.$emit('setCouponsCount', 0);
},
}, },
}); });
</script> </script>