mirror of
https://github.com/frappe/books.git
synced 2025-01-22 22:58:28 +00:00
Merge pull request #930 from AbleKSaju/feat-couponCode
feat: coupon codes
This commit is contained in:
commit
5b564c2309
@ -16,6 +16,7 @@ export class AccountingSettings extends Doc {
|
||||
enableInventory?: boolean;
|
||||
enablePriceList?: boolean;
|
||||
enableLead?: boolean;
|
||||
enableCouponCode?: boolean;
|
||||
enableFormCustomization?: boolean;
|
||||
enableInvoiceReturns?: boolean;
|
||||
enableLoyaltyProgram?: boolean;
|
||||
@ -67,6 +68,8 @@ export class AccountingSettings extends Doc {
|
||||
gstin: () => this.fyo.singles.SystemSettings?.countryCode !== 'in',
|
||||
enablePricingRule: () =>
|
||||
!this.fyo.singles.AccountingSettings?.enableDiscounting,
|
||||
enableCouponCode: () =>
|
||||
!this.fyo.singles.AccountingSettings?.enablePricingRule,
|
||||
};
|
||||
|
||||
async change(ch: ChangeArg) {
|
||||
|
107
models/baseModels/AppliedCouponCodes/AppliedCouponCodes.ts
Normal file
107
models/baseModels/AppliedCouponCodes/AppliedCouponCodes.ts
Normal file
@ -0,0 +1,107 @@
|
||||
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';
|
||||
|
||||
export class AppliedCouponCodes extends InvoiceItem {
|
||||
coupons?: string;
|
||||
|
||||
validations: ValidationMap = {
|
||||
coupons: async (value: DocValue) => {
|
||||
if (!value) {
|
||||
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.`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
192
models/baseModels/CouponCode/CouponCode.ts
Normal file
192
models/baseModels/CouponCode/CouponCode.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
FiltersMap,
|
||||
FormulaMap,
|
||||
ListViewSettings,
|
||||
ValidationMap,
|
||||
} from 'fyo/model/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { t } from 'fyo';
|
||||
import { Money } from 'pesa';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
|
||||
import { ApplicableCouponCodes } from '../Invoice/types';
|
||||
|
||||
export class CouponCode extends Doc {
|
||||
name?: string;
|
||||
couponName?: string;
|
||||
pricingRule?: string;
|
||||
|
||||
validFrom?: Date;
|
||||
validTo?: Date;
|
||||
|
||||
minAmount?: Money;
|
||||
maxAmount?: Money;
|
||||
|
||||
removeUnusedCoupons(coupons: ApplicableCouponCodes[], sinvDoc: SalesInvoice) {
|
||||
if (!coupons.length) {
|
||||
sinvDoc.coupons = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sinvDoc.coupons = sinvDoc.coupons!.filter((coupon) => {
|
||||
return coupons.find((c: ApplicableCouponCodes) =>
|
||||
coupon?.coupons?.includes(c?.coupon)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
formulas: FormulaMap = {
|
||||
name: {
|
||||
formula: () => {
|
||||
return this.couponName?.replace(/\s+/g, '').toUpperCase().slice(0, 8);
|
||||
},
|
||||
dependsOn: ['couponName'],
|
||||
},
|
||||
};
|
||||
|
||||
async pricingRuleData() {
|
||||
return await this.fyo.db.getAll(ModelNameEnum.PricingRule, {
|
||||
fields: ['minAmount', 'maxAmount', 'validFrom', 'validTo'],
|
||||
filters: {
|
||||
name: this.pricingRule as string,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
validations: ValidationMap = {
|
||||
minAmount: async (value: DocValue) => {
|
||||
if (!value || !this.maxAmount || !this.pricingRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [pricingRuleData] = await this.pricingRuleData();
|
||||
|
||||
if (
|
||||
(pricingRuleData?.minAmount as Money).isZero() &&
|
||||
(pricingRuleData.maxAmount as Money).isZero()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { minAmount } = pricingRuleData;
|
||||
|
||||
if ((value as Money).isZero() && this.maxAmount.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value as Money).lt(minAmount as Money)) {
|
||||
throw new ValidationError(
|
||||
t`Minimum Amount should be greather than the Pricing Rule's Minimum Amount.`
|
||||
);
|
||||
}
|
||||
|
||||
if ((value as Money).gte(this.maxAmount)) {
|
||||
throw new ValidationError(
|
||||
t`Minimum Amount should be less than the Maximum Amount.`
|
||||
);
|
||||
}
|
||||
},
|
||||
maxAmount: async (value: DocValue) => {
|
||||
if (!this.minAmount || !value || !this.pricingRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [pricingRuleData] = await this.pricingRuleData();
|
||||
|
||||
if (
|
||||
(pricingRuleData?.minAmount as Money).isZero() &&
|
||||
(pricingRuleData.maxAmount as Money).isZero()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { maxAmount } = pricingRuleData;
|
||||
|
||||
if (this.minAmount.isZero() && (value as Money).isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value as Money).gt(maxAmount as Money)) {
|
||||
throw new ValidationError(
|
||||
t`Maximum Amount should be lesser than Pricing Rule's Maximum Amount`
|
||||
);
|
||||
}
|
||||
|
||||
if ((value as Money).lte(this.minAmount)) {
|
||||
throw new ValidationError(
|
||||
t`Maximum Amount should be greater than the Minimum Amount.`
|
||||
);
|
||||
}
|
||||
},
|
||||
validFrom: async (value: DocValue) => {
|
||||
if (!value || !this.validTo || !this.pricingRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [pricingRuleData] = await this.pricingRuleData();
|
||||
|
||||
if (!pricingRuleData?.validFrom && !pricingRuleData.validTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { validFrom } = pricingRuleData;
|
||||
if (
|
||||
validFrom &&
|
||||
(value as Date).toISOString() < (validFrom as Date).toISOString()
|
||||
) {
|
||||
throw new ValidationError(
|
||||
t`Valid From Date should be greather than Pricing Rule's Valid From Date.`
|
||||
);
|
||||
}
|
||||
|
||||
if ((value as Date).toISOString() >= this.validTo.toISOString()) {
|
||||
throw new ValidationError(
|
||||
t`Valid From Date should be less than Valid To Date.`
|
||||
);
|
||||
}
|
||||
},
|
||||
validTo: async (value: DocValue) => {
|
||||
if (!this.validFrom || !value || !this.pricingRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [pricingRuleData] = await this.pricingRuleData();
|
||||
|
||||
if (!pricingRuleData?.validFrom && !pricingRuleData.validTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { validTo } = pricingRuleData;
|
||||
|
||||
if (
|
||||
validTo &&
|
||||
(value as Date).toISOString() > (validTo as Date).toISOString()
|
||||
) {
|
||||
throw new ValidationError(
|
||||
t`Valid To Date should be lesser than Pricing Rule's Valid To Date.`
|
||||
);
|
||||
}
|
||||
|
||||
if ((value as Date).toISOString() <= this.validFrom.toISOString()) {
|
||||
throw new ValidationError(
|
||||
t`Valid To Date should be greater than Valid From Date.`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
pricingRule: () => ({
|
||||
isCouponCodeBased: true,
|
||||
}),
|
||||
};
|
||||
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: ['name', 'couponName', 'pricingRule', 'maximumUse', 'used'],
|
||||
};
|
||||
}
|
||||
}
|
@ -13,10 +13,12 @@ import { ValidationError } from 'fyo/utils/errors';
|
||||
import { Transactional } from 'models/Transactional/Transactional';
|
||||
import {
|
||||
addItem,
|
||||
canApplyCouponCode,
|
||||
canApplyPricingRule,
|
||||
createLoyaltyPointEntry,
|
||||
filterPricingRules,
|
||||
getAddedLPWithGrandTotal,
|
||||
getApplicableCouponCodesName,
|
||||
getExchangeRate,
|
||||
getNumberSeries,
|
||||
getPricingRulesConflicts,
|
||||
@ -39,9 +41,14 @@ import { TaxSummary } from '../TaxSummary/TaxSummary';
|
||||
import { ReturnDocItem } from 'models/inventory/types';
|
||||
import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/types';
|
||||
import { PricingRule } from '../PricingRule/PricingRule';
|
||||
import { ApplicablePricingRules } from './types';
|
||||
import { ApplicableCouponCodes, ApplicablePricingRules } from './types';
|
||||
import { PricingRuleDetail } from '../PricingRuleDetail/PricingRuleDetail';
|
||||
import { LoyaltyProgram } from '../LoyaltyProgram/LoyaltyProgram';
|
||||
import { AppliedCouponCodes } from '../AppliedCouponCodes/AppliedCouponCodes';
|
||||
import { CouponCode } from '../CouponCode/CouponCode';
|
||||
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
|
||||
import { SalesInvoiceItem } from '../SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { PriceListItem } from '../PriceList/PriceListItem';
|
||||
|
||||
export type TaxDetail = {
|
||||
account: string;
|
||||
@ -61,6 +68,7 @@ export abstract class Invoice extends Transactional {
|
||||
taxes?: TaxSummary[];
|
||||
|
||||
items?: InvoiceItem[];
|
||||
coupons?: AppliedCouponCodes[];
|
||||
party?: string;
|
||||
account?: string;
|
||||
currency?: string;
|
||||
@ -187,6 +195,9 @@ export abstract class Invoice extends Transactional {
|
||||
|
||||
if (this.isReturn) {
|
||||
await this._removeLoyaltyPointEntry();
|
||||
this.reduceUsedCountOfCoupons();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isQuote) {
|
||||
@ -228,6 +239,10 @@ export abstract class Invoice extends Transactional {
|
||||
|
||||
await this._updateIsItemsReturned();
|
||||
await this._createLoyaltyPointEntry();
|
||||
|
||||
if (this.schemaName === ModelNameEnum.SalesInvoice) {
|
||||
this.updateUsedCountOfCoupons();
|
||||
}
|
||||
}
|
||||
|
||||
async afterCancel() {
|
||||
@ -236,12 +251,10 @@ export abstract class Invoice extends Transactional {
|
||||
await this._updatePartyOutStanding();
|
||||
await this._updateIsItemsReturned();
|
||||
await this._removeLoyaltyPointEntry();
|
||||
this.reduceUsedCountOfCoupons();
|
||||
}
|
||||
|
||||
async _removeLoyaltyPointEntry() {
|
||||
if (!this.loyaltyProgram) {
|
||||
return;
|
||||
}
|
||||
await removeLoyaltyPoint(this);
|
||||
}
|
||||
|
||||
@ -545,6 +558,31 @@ export abstract class Invoice extends Transactional {
|
||||
return newReturnDoc;
|
||||
}
|
||||
|
||||
updateUsedCountOfCoupons() {
|
||||
this.coupons?.map(async (coupon) => {
|
||||
const couponDoc = await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
coupon.coupons
|
||||
);
|
||||
|
||||
await couponDoc.setAndSync({ used: (couponDoc.used as number) + 1 });
|
||||
});
|
||||
}
|
||||
reduceUsedCountOfCoupons() {
|
||||
if (!this.coupons?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.coupons?.map(async (coupon) => {
|
||||
const couponDoc = await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
coupon.coupons
|
||||
);
|
||||
|
||||
await couponDoc.setAndSync({ used: (couponDoc.used as number) - 1 });
|
||||
});
|
||||
}
|
||||
|
||||
async _updateIsItemsReturned() {
|
||||
if (!this.isReturn || !this.returnAgainst || this.isQuote) {
|
||||
return;
|
||||
@ -727,7 +765,7 @@ export abstract class Invoice extends Transactional {
|
||||
await this.appendPricingRuleDetail(pricingRule);
|
||||
return !!pricingRule;
|
||||
},
|
||||
dependsOn: ['items'],
|
||||
dependsOn: ['items', 'coupons'],
|
||||
},
|
||||
};
|
||||
|
||||
@ -1035,6 +1073,31 @@ export abstract class Invoice extends Transactional {
|
||||
} else {
|
||||
this.clearFreeItems();
|
||||
}
|
||||
|
||||
if (!this.coupons?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const applicableCouponCodes = await Promise.all(
|
||||
this.coupons?.map(async (coupon) => {
|
||||
return await getApplicableCouponCodesName(
|
||||
coupon.coupons as string,
|
||||
this as SalesInvoice
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const flattedApplicableCouponCodes = applicableCouponCodes?.flat();
|
||||
|
||||
const couponCodeDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
this.coupons[0].coupons
|
||||
)) as CouponCode;
|
||||
|
||||
couponCodeDoc.removeUnusedCoupons(
|
||||
flattedApplicableCouponCodes as ApplicableCouponCodes[],
|
||||
this as SalesInvoice
|
||||
);
|
||||
}
|
||||
|
||||
async beforeCancel(): Promise<void> {
|
||||
@ -1259,6 +1322,21 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
}
|
||||
|
||||
async getPricingRuleDocNames(
|
||||
item: SalesInvoiceItem,
|
||||
sinvDoc: SalesInvoice
|
||||
): Promise<string[]> {
|
||||
const docs = (await sinvDoc.fyo.db.getAll(ModelNameEnum.PricingRuleItem, {
|
||||
fields: ['parent'],
|
||||
filters: {
|
||||
item: item.item as string,
|
||||
unit: item.unit as string,
|
||||
},
|
||||
})) as PriceListItem[];
|
||||
|
||||
return docs.map((doc) => doc.parent) as string[];
|
||||
}
|
||||
|
||||
async getPricingRule(): Promise<ApplicablePricingRules[] | undefined> {
|
||||
if (!this.isSales || !this.items) {
|
||||
return;
|
||||
@ -1288,15 +1366,49 @@ export abstract class Invoice extends Transactional {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pricingRuleDocNames = (
|
||||
await this.fyo.db.getAll(ModelNameEnum.PricingRuleItem, {
|
||||
fields: ['parent'],
|
||||
filters: {
|
||||
item: item.item as string,
|
||||
unit: item.unit as string,
|
||||
},
|
||||
})
|
||||
).map((doc) => doc.parent) as string[];
|
||||
const pricingRuleDocNames = await this.getPricingRuleDocNames(
|
||||
item,
|
||||
this as SalesInvoice
|
||||
);
|
||||
|
||||
if (!pricingRuleDocNames.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.coupons?.length) {
|
||||
for (const coupon of this.coupons) {
|
||||
const couponCodeDatas = await this.fyo.db.getAll(
|
||||
ModelNameEnum.CouponCode,
|
||||
{
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
name: coupon?.coupons as string,
|
||||
isEnabled: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const couponPricingRuleDocNames = couponCodeDatas
|
||||
.map((doc) => doc.pricingRule)
|
||||
.filter((val) =>
|
||||
pricingRuleDocNames.includes(val as string)
|
||||
) as string[];
|
||||
|
||||
if (!couponPricingRuleDocNames.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filtered = canApplyCouponCode(
|
||||
couponCodeDatas[0] as CouponCode,
|
||||
this.grandTotal as Money,
|
||||
this.date as Date
|
||||
);
|
||||
|
||||
if (filtered) {
|
||||
pricingRuleDocNames.push(...couponPricingRuleDocNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pricingRuleDocsForItem = (await this.fyo.db.getAll(
|
||||
ModelNameEnum.PricingRule,
|
||||
@ -1311,6 +1423,50 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
)) as PricingRule[];
|
||||
|
||||
if (pricingRuleDocsForItem[0].isCouponCodeBased) {
|
||||
if (!this.coupons?.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await Promise.allSettled(
|
||||
this.coupons?.map(async (val) => {
|
||||
if (!val.coupons) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [pricingRule] = (
|
||||
await this.fyo.db.getAll(ModelNameEnum.CouponCode, {
|
||||
fields: ['pricingRule'],
|
||||
filters: {
|
||||
name: val?.coupons,
|
||||
},
|
||||
})
|
||||
).map((doc) => doc.pricingRule);
|
||||
|
||||
if (!pricingRule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pricingRuleDocsForItem[0].name === pricingRule) {
|
||||
return pricingRule;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
const fulfilledData = data
|
||||
.filter(
|
||||
(result): result is PromiseFulfilledResult<string | false> =>
|
||||
result.status === 'fulfilled'
|
||||
)
|
||||
.map((result) => result.value as string);
|
||||
|
||||
if (!fulfilledData[0] && !fulfilledData.filter((val) => val).length) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = filterPricingRules(
|
||||
pricingRuleDocsForItem,
|
||||
this.date as Date,
|
||||
|
@ -4,3 +4,8 @@ export interface ApplicablePricingRules {
|
||||
applyOnItem: string;
|
||||
pricingRule: PricingRule;
|
||||
}
|
||||
|
||||
export interface ApplicableCouponCodes {
|
||||
pricingRule: string;
|
||||
coupon: string;
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ export class PricingRule extends Doc {
|
||||
discountPercentage?: number;
|
||||
discountAmount?: Money;
|
||||
|
||||
isCouponCodeBased?: boolean;
|
||||
|
||||
forPriceList?: string;
|
||||
|
||||
freeItem?: string;
|
||||
|
298
models/baseModels/tests/testCouponCodes.spec.ts
Normal file
298
models/baseModels/tests/testCouponCodes.spec.ts
Normal file
@ -0,0 +1,298 @@
|
||||
import test from 'tape';
|
||||
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { getItem } from 'models/inventory/tests/helpers';
|
||||
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
|
||||
import { PricingRule } from '../PricingRule/PricingRule';
|
||||
import { assertThrows } from 'backend/database/tests/helpers';
|
||||
|
||||
const fyo = getTestFyo();
|
||||
setupTestFyo(fyo, __filename);
|
||||
|
||||
const itemMap = {
|
||||
Jacket: {
|
||||
name: 'Jacket',
|
||||
rate: 1000,
|
||||
unit: 'Unit',
|
||||
},
|
||||
Cap: {
|
||||
name: 'Cap',
|
||||
rate: 100,
|
||||
unit: 'Unit',
|
||||
},
|
||||
};
|
||||
|
||||
const partyMap = {
|
||||
partyOne: {
|
||||
name: 'Daisy',
|
||||
email: 'daisy@alien.com',
|
||||
account: 'Debtors',
|
||||
},
|
||||
};
|
||||
|
||||
const pricingRuleMap = [
|
||||
{
|
||||
name: 'PRLE-1001',
|
||||
title: 'JKT PDR Offer',
|
||||
isCouponCodeBased: true,
|
||||
appliedItems: [{ item: itemMap.Jacket.name }],
|
||||
discountType: 'Price Discount',
|
||||
priceDiscountType: 'rate',
|
||||
discountRate: 800,
|
||||
minQuantity: 4,
|
||||
maxQuantity: 6,
|
||||
minAmount: fyo.pesa(4000),
|
||||
maxAmount: fyo.pesa(6000),
|
||||
validFrom: '2024-02-01',
|
||||
validTo: '2024-02-29',
|
||||
priority: '1',
|
||||
},
|
||||
{
|
||||
name: 'PRLE-1002',
|
||||
title: 'CAP PDR Offer',
|
||||
appliedItems: [{ item: itemMap.Cap.name }],
|
||||
discountType: 'Product Discount',
|
||||
freeItem: 'Cap',
|
||||
freeItemQuantity: 1,
|
||||
freeItemUnit: 'Unit',
|
||||
freeItemRate: 0,
|
||||
minQuantity: 4,
|
||||
maxQuantity: 6,
|
||||
minAmount: 200,
|
||||
maxAmount: 1000,
|
||||
validFrom: '2024-02-01',
|
||||
validTo: '2024-02-29',
|
||||
priority: '1',
|
||||
},
|
||||
];
|
||||
const couponCodesMap = [
|
||||
{
|
||||
name: 'COUPON1',
|
||||
isEnabled: true,
|
||||
couponName: 'coupon1',
|
||||
pricingRule: pricingRuleMap[0].name,
|
||||
maximumUse: 5,
|
||||
used: 0,
|
||||
minAmount: fyo.pesa(4000),
|
||||
maxAmount: fyo.pesa(6000),
|
||||
validFrom: '2024-02-01',
|
||||
validTo: '2024-02-29',
|
||||
},
|
||||
{
|
||||
name: 'COUPON2',
|
||||
couponName: 'coupon2',
|
||||
pricingRule: pricingRuleMap[1].name,
|
||||
maximumUse: 1,
|
||||
used: 0,
|
||||
minAmount: 200,
|
||||
maxAmount: 1000,
|
||||
validFrom: '2024-02-01',
|
||||
validTo: '2024-02-29',
|
||||
},
|
||||
];
|
||||
|
||||
test(' Coupon Codes: create dummy item, party, pricing rules, coupon codes', async (t) => {
|
||||
// Create Items
|
||||
for (const { name, rate } of Object.values(itemMap)) {
|
||||
const item = getItem(name, rate, false);
|
||||
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Item, item).sync();
|
||||
|
||||
t.ok(await fyo.db.exists(ModelNameEnum.Item, name), `Item: ${name} exists`);
|
||||
}
|
||||
|
||||
// Create Party
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Party, partyMap.partyOne).sync();
|
||||
|
||||
t.ok(
|
||||
await fyo.db.exists(ModelNameEnum.Party, partyMap.partyOne.name),
|
||||
`Party: ${partyMap.partyOne.name} exists`
|
||||
);
|
||||
|
||||
// Create Pricing Rules
|
||||
for (const pricingRule of Object.values(pricingRuleMap)) {
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.PricingRule, pricingRule).sync();
|
||||
|
||||
t.ok(
|
||||
await fyo.db.exists(ModelNameEnum.PricingRule, pricingRule.name),
|
||||
`Pricing Rule: ${pricingRule.name} exists`
|
||||
);
|
||||
}
|
||||
|
||||
await fyo.singles.AccountingSettings?.set('enablePricingRule', true);
|
||||
|
||||
t.ok(fyo.singles.AccountingSettings?.enablePricingRule);
|
||||
|
||||
// Create Coupon Codes
|
||||
for (const couponCodes of Object.values(couponCodesMap)) {
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.CouponCode, couponCodes).sync();
|
||||
|
||||
t.ok(
|
||||
await fyo.db.exists(ModelNameEnum.CouponCode, couponCodes.name),
|
||||
`Coupoon Code: ${couponCodes.name} exists`
|
||||
);
|
||||
}
|
||||
|
||||
await fyo.singles.AccountingSettings?.set('enableCouponCode', true);
|
||||
|
||||
t.ok(fyo.singles.AccountingSettings?.enableCouponCode);
|
||||
});
|
||||
|
||||
test('disabled coupon codes is not applied', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-01-20T18:30:00.000Z',
|
||||
party: partyMap.partyOne.name,
|
||||
account: partyMap.partyOne.account,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
|
||||
await sinv.append('coupons', {
|
||||
coupons: couponCodesMap[0].name,
|
||||
});
|
||||
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.pricingRuleDetail?.length, undefined);
|
||||
});
|
||||
|
||||
test('Coupon code not created: coupons min amount must be lesser than coupons max.', async (t) => {
|
||||
couponCodesMap[0].minAmount = fyo.pesa(7000);
|
||||
|
||||
const ccodeDoc = fyo.doc.getNewDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
couponCodesMap[0]
|
||||
) as PricingRule;
|
||||
|
||||
await assertThrows(
|
||||
async () => await ccodeDoc.sync(),
|
||||
'Minimum Amount should be less than the Maximum Amount'
|
||||
);
|
||||
});
|
||||
|
||||
test('Coupon code not created: pricing rules max amount is lower than the coupons min.', async (t) => {
|
||||
couponCodesMap[0].minAmount = fyo.pesa(3000);
|
||||
|
||||
const ccodeDoc = fyo.doc.getNewDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
couponCodesMap[0]
|
||||
) as PricingRule;
|
||||
|
||||
await assertThrows(
|
||||
async () => await ccodeDoc.sync(),
|
||||
"Minimum Amount should be greather than the Pricing Rule's Minimum Amount"
|
||||
);
|
||||
});
|
||||
|
||||
test('coupon code not created: pricing rules max amount is lower than the coupons max.', async (t) => {
|
||||
couponCodesMap[0].maxAmount = fyo.pesa(7000);
|
||||
|
||||
const ccodeDoc = fyo.doc.getNewDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
couponCodesMap[0]
|
||||
) as PricingRule;
|
||||
|
||||
await assertThrows(
|
||||
async () => await ccodeDoc.sync(),
|
||||
"Maximum Amount should be lesser than Pricing Rule's Maximum Amount"
|
||||
);
|
||||
});
|
||||
|
||||
test("coupon code is not applied when coupon's validfrom date < coupon's validTo date", async (t) => {
|
||||
couponCodesMap[0].minAmount = fyo.pesa(4000);
|
||||
couponCodesMap[0].maxAmount = fyo.pesa(6000);
|
||||
couponCodesMap[0].validTo = '2024-01-10';
|
||||
|
||||
const ccodeDoc = fyo.doc.getNewDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
couponCodesMap[0]
|
||||
) as PricingRule;
|
||||
|
||||
await assertThrows(
|
||||
async () => await ccodeDoc.sync(),
|
||||
'Valid From Date should be less than Valid To Date'
|
||||
);
|
||||
});
|
||||
|
||||
test("coupon code is not applied when coupon's validFrom date < pricing rule's validFrom date", async (t) => {
|
||||
couponCodesMap[0].validFrom = '2024-01-01';
|
||||
|
||||
const ccodeDoc = fyo.doc.getNewDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
couponCodesMap[0]
|
||||
) as PricingRule;
|
||||
|
||||
await assertThrows(
|
||||
async () => await ccodeDoc.sync(),
|
||||
"Valid From Date should be greather than Pricing Rule's Valid From Date"
|
||||
);
|
||||
});
|
||||
|
||||
test("coupon code is not applied when coupon's validTo date > pricing rule's validTo date", async (t) => {
|
||||
couponCodesMap[0].validFrom = '2024-02-01';
|
||||
couponCodesMap[0].validTo = '2024-03-01';
|
||||
|
||||
const ccodeDoc = fyo.doc.getNewDoc(
|
||||
ModelNameEnum.CouponCode,
|
||||
couponCodesMap[0]
|
||||
) as PricingRule;
|
||||
|
||||
await assertThrows(
|
||||
async () => await ccodeDoc.sync(),
|
||||
"Valid To Date should be lesser than Pricing Rule's Valid To Date"
|
||||
);
|
||||
});
|
||||
|
||||
test('apply coupon code', async (t) => {
|
||||
couponCodesMap[0].name = 'COUPON1';
|
||||
couponCodesMap[0].validTo = '2024-02-29';
|
||||
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-10',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
|
||||
await sinv.append('coupons', { coupons: couponCodesMap[0].name });
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.pricingRuleDetail?.length, 1);
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail![0].referenceName,
|
||||
pricingRuleMap[0].name,
|
||||
'Pricing Rule is applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Coupon not applied: incorrect items added.', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-10',
|
||||
party: partyMap.partyOne.name,
|
||||
account: partyMap.partyOne.account,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
|
||||
await sinv.append('coupons', { coupons: couponCodesMap[0].name });
|
||||
|
||||
await sinv.runFormulas();
|
||||
await sinv.sync();
|
||||
|
||||
t.equal(sinv.coupons?.length, 0, 'coupon code is not applied');
|
||||
});
|
||||
|
||||
closeTestFyo(fyo, __filename);
|
@ -2,10 +2,6 @@ import test from 'tape';
|
||||
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
|
||||
import {
|
||||
assertDoesNotThrow,
|
||||
assertThrows,
|
||||
} from 'backend/database/tests/helpers';
|
||||
import { getItem } from 'models/inventory/tests/helpers';
|
||||
import { PricingRule } from '../PricingRule/PricingRule';
|
||||
|
||||
@ -104,7 +100,7 @@ test('disabled pricing rule is not applied', async (t) => {
|
||||
await sinv.append('items', { item: itemMap.Jacket.name, quantity: 5 });
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.pricingRuleDetail?.length, undefined);
|
||||
t.equal(sinv.pricingRuleDetail?.length, 0);
|
||||
});
|
||||
|
||||
test('pricing rule is applied when filtered by min and max qty', async (t) => {
|
||||
|
@ -28,6 +28,8 @@ import { LoyaltyProgram } from './baseModels/LoyaltyProgram/LoyaltyProgram';
|
||||
import { CollectionRulesItems } from './baseModels/CollectionRulesItems/CollectionRulesItems';
|
||||
import { isPesa } from 'fyo/utils';
|
||||
import { Party } from './baseModels/Party/Party';
|
||||
import { CouponCode } from './baseModels/CouponCode/CouponCode';
|
||||
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
|
||||
|
||||
export function getQuoteActions(
|
||||
fyo: Fyo,
|
||||
@ -584,8 +586,6 @@ export async function getExchangeRate({
|
||||
};
|
||||
exchangeRate = data.rates[toCurrency];
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
exchangeRate ??= 1;
|
||||
}
|
||||
|
||||
@ -762,6 +762,10 @@ export function getLoyaltyProgramTier(
|
||||
}
|
||||
|
||||
export async function removeLoyaltyPoint(doc: Doc) {
|
||||
if (!doc.loyaltyProgram) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = (await doc.fyo.db.getAll(ModelNameEnum.LoyaltyPointEntry, {
|
||||
fields: ['name', 'loyaltyPoints', 'expiryDate'],
|
||||
filters: {
|
||||
@ -916,6 +920,7 @@ export function canApplyPricingRule(
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
pricingRuleDoc.validTo &&
|
||||
new Date(sinvDate.setHours(0, 0, 0, 0)).toISOString() >
|
||||
@ -925,6 +930,82 @@ export function canApplyPricingRule(
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function canApplyCouponCode(
|
||||
couponCodeData: CouponCode,
|
||||
amount: Money,
|
||||
sinvDate: Date
|
||||
): boolean {
|
||||
// Filter by Amount
|
||||
if (
|
||||
!couponCodeData.minAmount?.isZero() &&
|
||||
amount.lte(couponCodeData.minAmount as Money)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!couponCodeData.maxAmount?.isZero() &&
|
||||
amount.gte(couponCodeData.maxAmount as Money)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by Validity
|
||||
if (
|
||||
couponCodeData.validFrom &&
|
||||
new Date(sinvDate.setHours(0, 0, 0, 0)).toISOString() <
|
||||
couponCodeData.validFrom.toISOString()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
couponCodeData.validTo &&
|
||||
new Date(sinvDate.setHours(0, 0, 0, 0)).toISOString() >
|
||||
couponCodeData.validTo.toISOString()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getApplicableCouponCodesName(
|
||||
couponName: string,
|
||||
sinvDoc: SalesInvoice
|
||||
) {
|
||||
const couponCodeDatas = (await sinvDoc.fyo.db.getAll(
|
||||
ModelNameEnum.CouponCode,
|
||||
{
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
name: couponName,
|
||||
isEnabled: true,
|
||||
},
|
||||
}
|
||||
)) as CouponCode[];
|
||||
|
||||
if (!couponCodeDatas || couponCodeDatas.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const applicablePricingRules = await getPricingRule(sinvDoc);
|
||||
|
||||
if (!applicablePricingRules?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return applicablePricingRules
|
||||
?.filter(
|
||||
(rule) => rule?.pricingRule?.name === couponCodeDatas[0].pricingRule
|
||||
)
|
||||
.map((rule) => ({
|
||||
pricingRule: rule.pricingRule.name,
|
||||
coupon: couponCodeDatas[0].name,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getPricingRulesConflicts(
|
||||
pricingRules: PricingRule[]
|
||||
): undefined | boolean {
|
||||
|
@ -13,6 +13,8 @@ import { LoyaltyProgram } from './baseModels/LoyaltyProgram/LoyaltyProgram';
|
||||
import { LoyaltyPointEntry } from './baseModels/LoyaltyPointEntry/LoyaltyPointEntry';
|
||||
import { CollectionRulesItems } from './baseModels/CollectionRulesItems/CollectionRulesItems';
|
||||
import { Lead } from './baseModels/Lead/Lead';
|
||||
import { AppliedCouponCodes } from './baseModels/AppliedCouponCodes/AppliedCouponCodes';
|
||||
import { CouponCode } from './baseModels/CouponCode/CouponCode';
|
||||
import { Payment } from './baseModels/Payment/Payment';
|
||||
import { PaymentFor } from './baseModels/PaymentFor/PaymentFor';
|
||||
import { PriceList } from './baseModels/PriceList/PriceList';
|
||||
@ -64,6 +66,7 @@ export const models = {
|
||||
LoyaltyProgram,
|
||||
LoyaltyPointEntry,
|
||||
CollectionRulesItems,
|
||||
CouponCode,
|
||||
Payment,
|
||||
PaymentFor,
|
||||
PrintSettings,
|
||||
@ -75,6 +78,7 @@ export const models = {
|
||||
PurchaseInvoiceItem,
|
||||
SalesInvoice,
|
||||
SalesInvoiceItem,
|
||||
AppliedCouponCodes,
|
||||
SalesQuote,
|
||||
SalesQuoteItem,
|
||||
SerialNumber,
|
||||
|
@ -22,6 +22,9 @@ export enum ModelNameEnum {
|
||||
LoyaltyProgram = 'LoyaltyProgram',
|
||||
LoyaltyPointEntry = 'LoyaltyPointEntry',
|
||||
CollectionRulesItems = 'CollectionRulesItems',
|
||||
CouponCode = 'CouponCode',
|
||||
|
||||
AppliedCouponCodes = 'AppliedCouponCodes',
|
||||
Payment = 'Payment',
|
||||
PaymentFor = 'PaymentFor',
|
||||
PriceList = 'PriceList',
|
||||
|
@ -121,6 +121,13 @@
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "enableCouponCode",
|
||||
"label": "Enable Coupon Code",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscalYearStart",
|
||||
"label": "Fiscal Year Start Date",
|
||||
|
15
schemas/app/AppliedCouponCodes.json
Normal file
15
schemas/app/AppliedCouponCodes.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "AppliedCouponCodes",
|
||||
"label": "Applied Coupon Codes",
|
||||
"isChild": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "coupons",
|
||||
"label": "Coupons",
|
||||
"fieldtype": "Link",
|
||||
"target": "CouponCode"
|
||||
}
|
||||
],
|
||||
"tableFields": ["coupons"],
|
||||
"quickEditFields": ["coupons"]
|
||||
}
|
89
schemas/app/CouponCode.json
Normal file
89
schemas/app/CouponCode.json
Normal file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "CouponCode",
|
||||
"label": "Coupon Code",
|
||||
"naming": "manual",
|
||||
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"label": "Coupon Code",
|
||||
"fieldtype": "Data",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "couponName",
|
||||
"label": "Name",
|
||||
"fieldtype": "Data",
|
||||
"required": true,
|
||||
"placeholder": "Coupon Name",
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "isEnabled",
|
||||
"label": "Is Enabled",
|
||||
"fieldtype": "Check",
|
||||
"default": true,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "pricingRule",
|
||||
"label": "Pricing Rule",
|
||||
"fieldtype": "Link",
|
||||
"target": "PricingRule",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "minAmount",
|
||||
"label": "Min Amount",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "maxAmount",
|
||||
"label": "Max Amount",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "validFrom",
|
||||
"label": "Valid From",
|
||||
"fieldtype": "Date",
|
||||
"required": true,
|
||||
"section": "Validity and Usage"
|
||||
},
|
||||
{
|
||||
"fieldname": "validTo",
|
||||
"label": "Valid To",
|
||||
"fieldtype": "Date",
|
||||
"required": true,
|
||||
"section": "Validity and Usage"
|
||||
},
|
||||
{
|
||||
"fieldname": "maximumUse",
|
||||
"label": "Maximum Use",
|
||||
"fieldtype": "Int",
|
||||
"default": 0,
|
||||
"required": true,
|
||||
"section": "Validity and Usage"
|
||||
},
|
||||
{
|
||||
"fieldname": "used",
|
||||
"label": "Used",
|
||||
"fieldtype": "Int",
|
||||
"default": 0,
|
||||
"required": true,
|
||||
"readOnly": true,
|
||||
"section": "Validity and Usage"
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
"name",
|
||||
"couponCode",
|
||||
"pricingRule",
|
||||
"validFrom",
|
||||
"validTo",
|
||||
"maximumUse",
|
||||
"used"
|
||||
],
|
||||
"keywordFields": ["name"]
|
||||
}
|
@ -54,6 +54,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fieldname": "isCouponCodeBased",
|
||||
"label": "Is Coupon Code Based",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "isMultiple",
|
||||
"label": "Is Multiple",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "priceDiscountType",
|
||||
"label": "Price Discount Type",
|
||||
|
@ -24,6 +24,13 @@
|
||||
"required": true,
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "coupons",
|
||||
"label": "Coupons",
|
||||
"fieldtype": "Table",
|
||||
"target": "AppliedCouponCodes",
|
||||
"section": "Coupons"
|
||||
},
|
||||
{
|
||||
"fieldname": "backReference",
|
||||
"label": "Back Reference",
|
||||
|
@ -19,6 +19,8 @@ import Lead from './app/Lead.json';
|
||||
import LoyaltyProgram from './app/LoyaltyProgram.json';
|
||||
import LoyaltyPointEntry from './app/LoyaltyPointEntry.json';
|
||||
import CollectionRulesItems from './app/CollectionRulesItems.json';
|
||||
import CouponCode from './app/CouponCode.json';
|
||||
import AppliedCouponCodes from './app/AppliedCouponCodes.json';
|
||||
import Payment from './app/Payment.json';
|
||||
import PaymentFor from './app/PaymentFor.json';
|
||||
import PriceList from './app/PriceList.json';
|
||||
@ -128,6 +130,8 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
||||
SalesInvoiceItem as SchemaStub,
|
||||
PurchaseInvoiceItem as SchemaStub,
|
||||
SalesQuoteItem as SchemaStub,
|
||||
CouponCode as Schema,
|
||||
AppliedCouponCodes as Schema,
|
||||
|
||||
PriceList as Schema,
|
||||
PriceListItem as SchemaStub,
|
||||
|
@ -216,6 +216,13 @@ function getCompleteSidebar(): SidebarConfig {
|
||||
schemaName: 'Lead',
|
||||
hidden: () => !fyo.singles.AccountingSettings?.enableLead,
|
||||
},
|
||||
{
|
||||
label: t`Coupon Code`,
|
||||
name: 'coupon-code',
|
||||
route: `/list/CouponCode`,
|
||||
schemaName: 'CouponCode',
|
||||
hidden: () => !fyo.singles.AccountingSettings?.enableCouponCode,
|
||||
},
|
||||
] as SidebarItem[],
|
||||
},
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user