mirror of
https://github.com/frappe/books.git
synced 2024-11-09 15:20:56 +00:00
Merge pull request #829 from akshayitzme/pricing-rule
feat: Pricing Rule
This commit is contained in:
commit
2119c301d7
@ -18,6 +18,7 @@ export class AccountingSettings extends Doc {
|
||||
enableLead?: boolean;
|
||||
enableFormCustomization?: boolean;
|
||||
enableInvoiceReturns?: boolean;
|
||||
enablePricingRule?: boolean;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
writeOffAccount: () => ({
|
||||
@ -60,6 +61,8 @@ export class AccountingSettings extends Doc {
|
||||
override hidden: HiddenMap = {
|
||||
discountAccount: () => !this.enableDiscounting,
|
||||
gstin: () => this.fyo.singles.SystemSettings?.countryCode !== 'in',
|
||||
enablePricingRule: () =>
|
||||
!this.fyo.singles.AccountingSettings?.enableDiscounting,
|
||||
};
|
||||
|
||||
async change(ch: ChangeArg) {
|
||||
|
@ -11,7 +11,15 @@ import {
|
||||
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { Transactional } from 'models/Transactional/Transactional';
|
||||
import { addItem, getExchangeRate, getNumberSeries } from 'models/helpers';
|
||||
import {
|
||||
addItem,
|
||||
canApplyPricingRule,
|
||||
filterPricingRules,
|
||||
getExchangeRate,
|
||||
getNumberSeries,
|
||||
getPricingRulesConflicts,
|
||||
roundFreeItemQty,
|
||||
} from 'models/helpers';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
import { validateBatch } from 'models/inventory/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
@ -27,6 +35,9 @@ import { Tax } from '../Tax/Tax';
|
||||
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 { PricingRuleDetail } from '../PricingRuleDetail/PricingRuleDetail';
|
||||
|
||||
export type TaxDetail = {
|
||||
account: string;
|
||||
@ -70,6 +81,8 @@ export abstract class Invoice extends Transactional {
|
||||
isReturned?: boolean;
|
||||
returnAgainst?: string;
|
||||
|
||||
pricingRuleDetail?: PricingRuleDetail[];
|
||||
|
||||
get isSales() {
|
||||
return (
|
||||
this.schemaName === 'SalesInvoice' || this.schemaName == 'SalesQuote'
|
||||
@ -160,6 +173,7 @@ export abstract class Invoice extends Transactional {
|
||||
throw new ValidationError(this.fyo.t`Discount Account is not set.`);
|
||||
}
|
||||
await validateBatch(this);
|
||||
await this._validatePricingRule();
|
||||
}
|
||||
|
||||
async afterSubmit() {
|
||||
@ -625,6 +639,22 @@ export abstract class Invoice extends Transactional {
|
||||
!!this.autoStockTransferLocation,
|
||||
dependsOn: [],
|
||||
},
|
||||
isPricingRuleApplied: {
|
||||
formula: async () => {
|
||||
if (!this.fyo.singles.AccountingSettings?.enablePricingRule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pricingRule = await this.getPricingRule();
|
||||
if (!pricingRule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.appendPricingRuleDetail(pricingRule);
|
||||
return !!pricingRule;
|
||||
},
|
||||
dependsOn: ['items'],
|
||||
},
|
||||
};
|
||||
|
||||
getStockTransferred() {
|
||||
@ -701,6 +731,9 @@ export abstract class Invoice extends Transactional {
|
||||
(!this.canEdit && !this.priceList),
|
||||
returnAgainst: () =>
|
||||
(this.isSubmitted || this.isCancelled) && !this.returnAgainst,
|
||||
pricingRuleDetail: () =>
|
||||
!this.fyo.singles.AccountingSettings?.enablePricingRule ||
|
||||
!this.pricingRuleDetail?.length,
|
||||
};
|
||||
|
||||
static defaults: DefaultMap = {
|
||||
@ -917,6 +950,16 @@ export abstract class Invoice extends Transactional {
|
||||
return transfer;
|
||||
}
|
||||
|
||||
async beforeSync(): Promise<void> {
|
||||
await super.beforeSync();
|
||||
|
||||
if (this.pricingRuleDetail?.length) {
|
||||
await this.applyProductDiscount();
|
||||
} else {
|
||||
this.clearFreeItems();
|
||||
}
|
||||
}
|
||||
|
||||
async beforeCancel(): Promise<void> {
|
||||
await super.beforeCancel();
|
||||
await this._validateStockTransferCancelled();
|
||||
@ -1045,4 +1088,174 @@ export abstract class Invoice extends Transactional {
|
||||
async addItem(name: string) {
|
||||
return await addItem(name, this);
|
||||
}
|
||||
|
||||
async appendPricingRuleDetail(
|
||||
applicablePricingRule: ApplicablePricingRules[]
|
||||
) {
|
||||
await this.set('pricingRuleDetail', null);
|
||||
|
||||
for (const doc of applicablePricingRule) {
|
||||
await this.append('pricingRuleDetail', {
|
||||
referenceName: doc.pricingRule.name,
|
||||
referenceItem: doc.applyOnItem,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearFreeItems() {
|
||||
if (this.pricingRuleDetail?.length || !this.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of this.items) {
|
||||
if (item.isFreeItem) {
|
||||
this.items = this.items?.filter(
|
||||
(invoiceItem) => invoiceItem.name !== item.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async applyProductDiscount() {
|
||||
if (!this.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pricingRuleDetail?.length || !this.pricingRuleDetail.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.items = this.items.filter((item) => !item.isFreeItem);
|
||||
|
||||
for (const item of this.items) {
|
||||
const pricingRuleDetailForItem = this.pricingRuleDetail.filter(
|
||||
(doc) => doc.referenceItem === item.item
|
||||
);
|
||||
|
||||
if (!pricingRuleDetailForItem.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pricingRuleDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
pricingRuleDetailForItem[0].referenceName
|
||||
)) as PricingRule;
|
||||
|
||||
if (pricingRuleDoc.discountType === 'Price Discount') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const appliedItems = pricingRuleDoc.appliedItems?.map((doc) => doc.item);
|
||||
if (!appliedItems?.includes(item.item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const canApplyPRLOnItem = canApplyPricingRule(
|
||||
pricingRuleDoc,
|
||||
this.date as Date,
|
||||
item.quantity as number,
|
||||
item.amount as Money
|
||||
);
|
||||
|
||||
if (!canApplyPRLOnItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let freeItemQty = pricingRuleDoc.freeItemQuantity as number;
|
||||
|
||||
if (pricingRuleDoc.isRecursive) {
|
||||
freeItemQty =
|
||||
(item.quantity as number) / (pricingRuleDoc.recurseEvery as number);
|
||||
}
|
||||
|
||||
if (pricingRuleDoc.roundFreeItemQty) {
|
||||
freeItemQty = roundFreeItemQty(
|
||||
freeItemQty,
|
||||
pricingRuleDoc.roundingMethod as 'round' | 'floor' | 'ceil'
|
||||
);
|
||||
}
|
||||
|
||||
await this.append('items', {
|
||||
item: pricingRuleDoc.freeItem as string,
|
||||
quantity: freeItemQty,
|
||||
isFreeItem: true,
|
||||
rate: pricingRuleDoc.freeItemRate,
|
||||
unit: pricingRuleDoc.freeItemUnit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getPricingRule(): Promise<ApplicablePricingRules[] | undefined> {
|
||||
if (!this.isSales || !this.items) {
|
||||
return;
|
||||
}
|
||||
const pricingRules: ApplicablePricingRules[] = [];
|
||||
|
||||
for (const item of this.items) {
|
||||
if (item.isFreeItem) {
|
||||
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 pricingRuleDocsForItem = (await this.fyo.db.getAll(
|
||||
ModelNameEnum.PricingRule,
|
||||
{
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
name: ['in', pricingRuleDocNames],
|
||||
isEnabled: true,
|
||||
},
|
||||
orderBy: 'priority',
|
||||
order: 'desc',
|
||||
}
|
||||
)) as PricingRule[];
|
||||
|
||||
const filtered = filterPricingRules(
|
||||
pricingRuleDocsForItem,
|
||||
this.date as Date,
|
||||
item.quantity as number,
|
||||
item.amount as Money
|
||||
);
|
||||
|
||||
if (!filtered.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isPricingRuleHasConflicts = getPricingRulesConflicts(
|
||||
filtered,
|
||||
item.item as string
|
||||
);
|
||||
|
||||
if (isPricingRuleHasConflicts) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pricingRules.push({
|
||||
applyOnItem: item.item as string,
|
||||
pricingRule: filtered[0],
|
||||
});
|
||||
}
|
||||
|
||||
return pricingRules;
|
||||
}
|
||||
|
||||
async _validatePricingRule() {
|
||||
if (!this.fyo.singles.AccountingSettings?.enablePricingRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.items) {
|
||||
return;
|
||||
}
|
||||
await this.getPricingRule();
|
||||
}
|
||||
}
|
||||
|
6
models/baseModels/Invoice/types.ts
Normal file
6
models/baseModels/Invoice/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { PricingRule } from '../PricingRule/PricingRule';
|
||||
|
||||
export interface ApplicablePricingRules {
|
||||
applyOnItem: string;
|
||||
pricingRule: PricingRule;
|
||||
}
|
@ -19,6 +19,7 @@ import { Item } from '../Item/Item';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
import { PriceList } from '../PriceList/PriceList';
|
||||
import { isPesa } from 'fyo/utils';
|
||||
import { PricingRule } from '../PricingRule/PricingRule';
|
||||
|
||||
export abstract class InvoiceItem extends Doc {
|
||||
item?: string;
|
||||
@ -46,6 +47,8 @@ export abstract class InvoiceItem extends Doc {
|
||||
itemDiscountedTotal?: Money;
|
||||
itemTaxedTotal?: Money;
|
||||
|
||||
isFreeItem?: boolean;
|
||||
|
||||
get isSales() {
|
||||
return (
|
||||
this.schemaName === 'SalesInvoiceItem' ||
|
||||
@ -93,6 +96,10 @@ export abstract class InvoiceItem extends Doc {
|
||||
return !!this.parentdoc?.isReturn;
|
||||
}
|
||||
|
||||
get pricingRuleDetail() {
|
||||
return this.parentdoc?.pricingRuleDetail;
|
||||
}
|
||||
|
||||
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
|
||||
super(schema, data, fyo);
|
||||
this._setGetCurrencies();
|
||||
@ -169,6 +176,7 @@ export abstract class InvoiceItem extends Doc {
|
||||
'itemTaxedTotal',
|
||||
'itemDiscountedTotal',
|
||||
'setItemDiscountAmount',
|
||||
'pricingRuleDetail',
|
||||
],
|
||||
},
|
||||
unit: {
|
||||
@ -404,6 +412,80 @@ export abstract class InvoiceItem extends Doc {
|
||||
},
|
||||
dependsOn: ['item', 'quantity'],
|
||||
},
|
||||
setItemDiscountAmount: {
|
||||
formula: async () => {
|
||||
if (
|
||||
!this.fyo.singles.AccountingSettings?.enablePricingRule ||
|
||||
!this.parentdoc?.pricingRuleDetail
|
||||
) {
|
||||
return this.setItemDiscountAmount;
|
||||
}
|
||||
|
||||
const pricingRule = this.parentdoc?.pricingRuleDetail?.filter(
|
||||
(prDetail) => prDetail.referenceItem === this.item
|
||||
);
|
||||
|
||||
if (!pricingRule) {
|
||||
return this.setItemDiscountAmount;
|
||||
}
|
||||
|
||||
const pricingRuleDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
pricingRule[0].referenceName
|
||||
)) as PricingRule;
|
||||
|
||||
if (pricingRuleDoc.discountType === 'Product Discount') {
|
||||
return this.setItemDiscountAmount;
|
||||
}
|
||||
|
||||
if (pricingRuleDoc.priceDiscountType === 'amount') {
|
||||
const discountAmount = pricingRuleDoc.discountAmount?.mul(
|
||||
this.quantity as number
|
||||
);
|
||||
await this.set('itemDiscountAmount', discountAmount);
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.setItemDiscountAmount;
|
||||
},
|
||||
dependsOn: ['pricingRuleDetail'],
|
||||
},
|
||||
itemDiscountPercent: {
|
||||
formula: async () => {
|
||||
if (
|
||||
!this.fyo.singles.AccountingSettings?.enablePricingRule ||
|
||||
!this.parentdoc?.pricingRuleDetail
|
||||
) {
|
||||
return this.itemDiscountPercent;
|
||||
}
|
||||
|
||||
const pricingRule = this.parentdoc?.pricingRuleDetail?.filter(
|
||||
(prDetail) => prDetail.referenceItem === this.item
|
||||
);
|
||||
|
||||
if (!pricingRule) {
|
||||
return this.itemDiscountPercent;
|
||||
}
|
||||
|
||||
const pricingRuleDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
pricingRule[0].referenceName
|
||||
)) as PricingRule;
|
||||
|
||||
if (pricingRuleDoc.discountType === 'Product Discount') {
|
||||
return this.itemDiscountPercent;
|
||||
}
|
||||
|
||||
if (pricingRuleDoc.priceDiscountType === 'percentage') {
|
||||
await this.set('setItemDiscountAmount', false);
|
||||
|
||||
return pricingRuleDoc.discountPercentage;
|
||||
}
|
||||
|
||||
return this.itemDiscountPercent;
|
||||
},
|
||||
dependsOn: ['pricingRuleDetail'],
|
||||
},
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
@ -531,7 +613,21 @@ export abstract class InvoiceItem extends Doc {
|
||||
}
|
||||
|
||||
async function getItemRate(doc: InvoiceItem): Promise<Money | undefined> {
|
||||
if (doc.isFreeItem) {
|
||||
return doc.rate;
|
||||
}
|
||||
|
||||
let pricingRuleRate: Money | undefined;
|
||||
if (doc.fyo.singles.AccountingSettings?.enablePricingRule) {
|
||||
pricingRuleRate = await getItemRateFromPricingRule(doc);
|
||||
}
|
||||
|
||||
if (pricingRuleRate) {
|
||||
return pricingRuleRate;
|
||||
}
|
||||
|
||||
let priceListRate: Money | undefined;
|
||||
|
||||
if (doc.fyo.singles.AccountingSettings?.enablePriceList) {
|
||||
priceListRate = await getItemRateFromPriceList(doc);
|
||||
}
|
||||
@ -552,6 +648,33 @@ async function getItemRate(doc: InvoiceItem): Promise<Money | undefined> {
|
||||
return;
|
||||
}
|
||||
|
||||
async function getItemRateFromPricingRule(
|
||||
doc: InvoiceItem
|
||||
): Promise<Money | undefined> {
|
||||
const pricingRule = doc.parentdoc?.pricingRuleDetail?.filter(
|
||||
(prDetail) => prDetail.referenceItem === doc.item
|
||||
);
|
||||
|
||||
if (!pricingRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pricingRuleDoc = (await doc.fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
pricingRule[0].referenceName
|
||||
)) as PricingRule;
|
||||
|
||||
if (pricingRuleDoc.discountType !== 'Price Discount') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pricingRuleDoc.priceDiscountType !== 'rate') {
|
||||
return;
|
||||
}
|
||||
|
||||
return pricingRuleDoc.discountRate;
|
||||
}
|
||||
|
||||
async function getItemRateFromPriceList(
|
||||
doc: InvoiceItem
|
||||
): Promise<Money | undefined> {
|
||||
|
@ -2,7 +2,7 @@ import { Doc } from 'fyo/model/doc';
|
||||
import { ListViewSettings } from 'fyo/model/types';
|
||||
import { PriceListItem } from './PriceListItem';
|
||||
import {
|
||||
getPriceListEnabledColumn,
|
||||
getIsDocEnabledColumn,
|
||||
getPriceListStatusColumn,
|
||||
} from 'models/helpers';
|
||||
|
||||
@ -14,11 +14,7 @@ export class PriceList extends Doc {
|
||||
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: [
|
||||
'name',
|
||||
getPriceListEnabledColumn(),
|
||||
getPriceListStatusColumn(),
|
||||
],
|
||||
columns: ['name', getIsDocEnabledColumn(), getPriceListStatusColumn()],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
166
models/baseModels/PricingRule/PricingRule.ts
Normal file
166
models/baseModels/PricingRule/PricingRule.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
import { PricingRuleItem } from '../PricingRuleItem/PricingRuleItem';
|
||||
import { getIsDocEnabledColumn } from 'models/helpers';
|
||||
import {
|
||||
HiddenMap,
|
||||
ListViewSettings,
|
||||
RequiredMap,
|
||||
ValidationMap,
|
||||
} from 'fyo/model/types';
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { t } from 'fyo';
|
||||
|
||||
export class PricingRule extends Doc {
|
||||
isEnabled?: boolean;
|
||||
title?: string;
|
||||
appliedItems?: PricingRuleItem[];
|
||||
discountType?: 'Price Discount' | 'Product Discount';
|
||||
|
||||
priceDiscountType?: 'rate' | 'percentage' | 'amount';
|
||||
discountRate?: Money;
|
||||
discountPercentage?: number;
|
||||
discountAmount?: Money;
|
||||
|
||||
forPriceList?: string;
|
||||
|
||||
freeItem?: string;
|
||||
freeItemQuantity?: number;
|
||||
freeItemUnit?: string;
|
||||
freeItemRate?: Money;
|
||||
roundFreeItemQty?: number;
|
||||
roundingMethod?: string;
|
||||
|
||||
isRecursive?: boolean;
|
||||
recurseEvery?: number;
|
||||
recurseOver?: number;
|
||||
|
||||
minQuantity?: number;
|
||||
maxQuantity?: number;
|
||||
|
||||
minAmount?: Money;
|
||||
maxAmount?: Money;
|
||||
|
||||
validFrom?: Date;
|
||||
validTo?: Date;
|
||||
|
||||
thresholdForSuggestion?: number;
|
||||
priority?: number;
|
||||
|
||||
get isDiscountTypeIsPriceDiscount() {
|
||||
return this.discountType === 'Price Discount';
|
||||
}
|
||||
|
||||
validations: ValidationMap = {
|
||||
minQuantity: (value: DocValue) => {
|
||||
if (!value || !this.maxQuantity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value as number) > this.maxQuantity) {
|
||||
throw new ValidationError(
|
||||
t`Minimum Quantity should be less than the Maximum Quantity.`
|
||||
);
|
||||
}
|
||||
},
|
||||
maxQuantity: (value: DocValue) => {
|
||||
if (!this.minQuantity || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value as number) < this.minQuantity) {
|
||||
throw new ValidationError(
|
||||
t`Maximum Quantity should be greater than the Minimum Quantity.`
|
||||
);
|
||||
}
|
||||
},
|
||||
minAmount: (value: DocValue) => {
|
||||
if (!value || !this.maxAmount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value as Money).isZero() && this.maxAmount.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value as Money).gte(this.maxAmount)) {
|
||||
throw new ValidationError(
|
||||
t`Minimum Amount should be less than the Maximum Amount.`
|
||||
);
|
||||
}
|
||||
},
|
||||
maxAmount: (value: DocValue) => {
|
||||
if (!this.minAmount || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.minAmount.isZero() && (value as Money).isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value as Money).lte(this.minAmount)) {
|
||||
throw new ValidationError(
|
||||
t`Maximum Amount should be greater than the Minimum Amount.`
|
||||
);
|
||||
}
|
||||
},
|
||||
validFrom: (value: DocValue) => {
|
||||
if (!value || !this.validTo) {
|
||||
return;
|
||||
}
|
||||
if ((value as Date).toISOString() > this.validTo.toISOString()) {
|
||||
throw new ValidationError(
|
||||
t`Valid From Date should be less than Valid To Date.`
|
||||
);
|
||||
}
|
||||
},
|
||||
validTo: (value: DocValue) => {
|
||||
if (!this.validFrom || !value) {
|
||||
return;
|
||||
}
|
||||
if ((value as Date).toISOString() < this.validFrom.toISOString()) {
|
||||
throw new ValidationError(
|
||||
t`Valid To Date should be greater than Valid From Date.`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
required: RequiredMap = {
|
||||
priceDiscountType: () => this.isDiscountTypeIsPriceDiscount,
|
||||
};
|
||||
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: ['name', 'title', getIsDocEnabledColumn(), 'discountType'],
|
||||
};
|
||||
}
|
||||
|
||||
hidden: HiddenMap = {
|
||||
location: () => !this.fyo.singles.AccountingSettings?.enableInventory,
|
||||
|
||||
priceDiscountType: () => !this.isDiscountTypeIsPriceDiscount,
|
||||
discountRate: () =>
|
||||
!this.isDiscountTypeIsPriceDiscount || this.priceDiscountType !== 'rate',
|
||||
discountPercentage: () =>
|
||||
!this.isDiscountTypeIsPriceDiscount ||
|
||||
this.priceDiscountType !== 'percentage',
|
||||
discountAmount: () =>
|
||||
!this.isDiscountTypeIsPriceDiscount ||
|
||||
this.priceDiscountType !== 'amount',
|
||||
forPriceList: () =>
|
||||
!this.isDiscountTypeIsPriceDiscount || this.priceDiscountType === 'rate',
|
||||
|
||||
freeItem: () => this.isDiscountTypeIsPriceDiscount,
|
||||
freeItemQuantity: () => this.isDiscountTypeIsPriceDiscount,
|
||||
freeItemUnit: () => this.isDiscountTypeIsPriceDiscount,
|
||||
freeItemRate: () => this.isDiscountTypeIsPriceDiscount,
|
||||
roundFreeItemQty: () => this.isDiscountTypeIsPriceDiscount,
|
||||
roundingMethod: () =>
|
||||
this.isDiscountTypeIsPriceDiscount || !this.roundFreeItemQty,
|
||||
isRecursive: () => this.isDiscountTypeIsPriceDiscount,
|
||||
recurseEvery: () => this.isDiscountTypeIsPriceDiscount || !this.isRecursive,
|
||||
recurseOver: () => this.isDiscountTypeIsPriceDiscount || !this.isRecursive,
|
||||
};
|
||||
}
|
6
models/baseModels/PricingRuleDetail/PricingRuleDetail.ts
Normal file
6
models/baseModels/PricingRuleDetail/PricingRuleDetail.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
|
||||
export class PricingRuleDetail extends Doc {
|
||||
referenceName?: string;
|
||||
referenceItem?: string;
|
||||
}
|
19
models/baseModels/PricingRuleItem/PricingRuleItem.ts
Normal file
19
models/baseModels/PricingRuleItem/PricingRuleItem.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FormulaMap } from 'fyo/model/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
|
||||
export class PricingRuleItem extends Doc {
|
||||
item?: string;
|
||||
unit?: string;
|
||||
|
||||
formulas: FormulaMap = {
|
||||
unit: {
|
||||
formula: () => {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
return this.fyo.getValue(ModelNameEnum.Item, this.item, 'unit');
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
556
models/baseModels/tests/testPricingRule.spec.ts
Normal file
556
models/baseModels/tests/testPricingRule.spec.ts
Normal file
@ -0,0 +1,556 @@
|
||||
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';
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
const pricingRuleMap = [
|
||||
{
|
||||
name: 'PRLE-1001',
|
||||
isEnabled: false,
|
||||
title: 'JKT PDR Offer',
|
||||
appliedItems: [{ item: itemMap.Jacket.name }],
|
||||
discountType: 'Price Discount',
|
||||
priceDiscountType: 'rate',
|
||||
discountRate: 800,
|
||||
minQuantity: 4,
|
||||
maxQuantity: 6,
|
||||
minAmount: fyo.pesa(4000),
|
||||
maxAmount: fyo.pesa(6000),
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
test('Pricing Rule: create dummy item, party, pricing rules', 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),
|
||||
`Price List: ${pricingRule.name} exists`
|
||||
);
|
||||
}
|
||||
|
||||
await fyo.singles.AccountingSettings?.set('enablePricingRule', true);
|
||||
t.ok(fyo.singles.AccountingSettings?.enablePricingRule);
|
||||
});
|
||||
|
||||
test('disabled pricing rule is not applied', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: new Date(),
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', { item: itemMap.Jacket.name, quantity: 5 });
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.pricingRuleDetail?.length, undefined);
|
||||
});
|
||||
|
||||
test('pricing rule is applied when filtered by min and max qty', async (t) => {
|
||||
const pruleDoc = (await fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
pricingRuleMap[0].name
|
||||
)) as PricingRule;
|
||||
|
||||
await pruleDoc.set('isEnabled', true);
|
||||
await pruleDoc.sync();
|
||||
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: new Date(),
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail![0].referenceName,
|
||||
pricingRuleMap[0].name,
|
||||
'Pricing Rule is added to Pricing Rule Detail'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
sinv.items![0].rate!.float,
|
||||
pricingRuleMap[0].discountRate,
|
||||
'item rate fetched from Pricing Rule'
|
||||
);
|
||||
});
|
||||
|
||||
test('pricing rule is not applied when item qty is < min qty', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: new Date(),
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', { item: itemMap.Jacket.name, quantity: 3 });
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.pricingRuleDetail?.length, undefined);
|
||||
});
|
||||
|
||||
test('pricing rule is not applied when item qty is > max qty', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: new Date(),
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', { item: itemMap.Jacket.name, quantity: 10 });
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.pricingRuleDetail?.length, undefined);
|
||||
});
|
||||
|
||||
test('pricing rule is applied when filtered by min and max amount', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: new Date(),
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail![0].referenceName,
|
||||
pricingRuleMap[0].name,
|
||||
'Pricing Rule is added to Pricing Rule Detail'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
sinv.items![0].rate!.float,
|
||||
pricingRuleMap[0].discountRate,
|
||||
'item rate fetched from Pricing Rule'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is not applied when item amount is < min amount', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: new Date(),
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 2,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail?.length,
|
||||
undefined,
|
||||
'Pricing Rule is not applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is not applied when item amount is > max amount', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: new Date(),
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 7,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail?.length,
|
||||
undefined,
|
||||
'Pricing Rule is not applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is not applied when sinvDate < validFrom date', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-01-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail?.length,
|
||||
undefined,
|
||||
'Pricing Rule is not applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is not applied when sinvDate > validFrom date', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-03-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail?.length,
|
||||
undefined,
|
||||
'Pricing Rule is not applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is applied when filtered by qty, amount and dates', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail![0].referenceName,
|
||||
pricingRuleMap[1].name,
|
||||
'Pricing Rule is applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is applied when filtered by qty, amount and dates', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail![0].referenceName,
|
||||
pricingRuleMap[1].name,
|
||||
'Pricing Rule is applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is not applied when qty condition is false, rest is true', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 7,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail?.length,
|
||||
undefined,
|
||||
'Pricing Rule is not applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is not applied when amount condition is false, rest is true', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: fyo.pesa(250),
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail?.length,
|
||||
undefined,
|
||||
'Pricing Rule is not applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is not applied when validity condition is false, rest is true', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-03-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail?.length,
|
||||
undefined,
|
||||
'Pricing Rule is not applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('create two pricing rules, Highest priority pricing rule is applied', async (t) => {
|
||||
const newPricingRuleDoc = fyo.doc.getNewDoc(ModelNameEnum.PricingRule, {
|
||||
...pricingRuleMap[1],
|
||||
priority: '2',
|
||||
appliedItems: [{ item: itemMap.Cap.name }],
|
||||
});
|
||||
|
||||
await newPricingRuleDoc.runFormulas();
|
||||
await newPricingRuleDoc.sync();
|
||||
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(
|
||||
sinv.pricingRuleDetail![0].referenceName,
|
||||
'PRLE-1003',
|
||||
'Pricing Rule with highest priority is applied'
|
||||
);
|
||||
});
|
||||
|
||||
test('Pricing Rule is not applied due to two docs having same priority', async (t) => {
|
||||
const pricingRuleDoc = await fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
'PRLE-1003'
|
||||
);
|
||||
|
||||
await pricingRuleDoc.set('priority', '1');
|
||||
await pricingRuleDoc.sync();
|
||||
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(!!sinv.pricingRuleDetail?.length, false);
|
||||
});
|
||||
|
||||
test('create a price discount of type rate, discounted rate should apply', async (t) => {
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.items![0].rate?.float, pricingRuleMap[0].discountRate);
|
||||
});
|
||||
|
||||
test('create a price discount of type percent, discount percent should apply', async (t) => {
|
||||
const pricingRuleDoc = await fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
pricingRuleMap[0].name
|
||||
);
|
||||
|
||||
await pricingRuleDoc.setMultiple({
|
||||
priceDiscountType: 'percentage',
|
||||
discountPercentage: 69,
|
||||
});
|
||||
|
||||
await pricingRuleDoc.sync();
|
||||
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.items![0].itemDiscountPercent, 69);
|
||||
});
|
||||
|
||||
test('create a price discount of type amount, discount amount should apply', async (t) => {
|
||||
const pricingRuleDoc = await fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
pricingRuleMap[0].name
|
||||
);
|
||||
|
||||
await pricingRuleDoc.setMultiple({
|
||||
priceDiscountType: 'amount',
|
||||
discountAmount: 500,
|
||||
});
|
||||
|
||||
await pricingRuleDoc.sync();
|
||||
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Jacket.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Jacket.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
|
||||
t.equal(sinv.items![0].itemDiscountAmount!.float, 2500);
|
||||
});
|
||||
|
||||
test('create a product discount giving 1 free item', async (t) => {
|
||||
const pricingRuleDoc = await fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
'PRLE-1003'
|
||||
);
|
||||
|
||||
await pricingRuleDoc.set('isEnabled', false);
|
||||
await pricingRuleDoc.sync();
|
||||
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
account: 'Debtors',
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
await sinv.sync();
|
||||
|
||||
t.equal(!!sinv.items![1].isFreeItem, true);
|
||||
t.equal(sinv.items![1].rate!.float, pricingRuleMap[1].freeItemRate);
|
||||
t.equal(sinv.items![1].quantity, pricingRuleMap[1].freeItemQuantity);
|
||||
});
|
||||
|
||||
test('create a product discount, recurse 2', async (t) => {
|
||||
const pricingRuleDoc = await fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
'PRLE-1003'
|
||||
);
|
||||
|
||||
await pricingRuleDoc.set('isRecursive', true);
|
||||
await pricingRuleDoc.set('recurseEvery', 2);
|
||||
await pricingRuleDoc.sync();
|
||||
|
||||
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
account: 'Debtors',
|
||||
date: '2024-02-01',
|
||||
party: partyMap.partyOne.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinv.append('items', {
|
||||
item: itemMap.Cap.name,
|
||||
quantity: 5,
|
||||
rate: itemMap.Cap.rate,
|
||||
});
|
||||
await sinv.runFormulas();
|
||||
await sinv.sync();
|
||||
|
||||
t.equal(!!sinv.items![1].isFreeItem, true);
|
||||
t.equal(sinv.items![1].rate!.float, pricingRuleMap[1].freeItemRate);
|
||||
t.equal(sinv.items![1].quantity, pricingRuleMap[1].freeItemQuantity);
|
||||
});
|
||||
|
||||
closeTestFyo(fyo, __filename);
|
@ -22,6 +22,9 @@ import { StockMovement } from './inventory/StockMovement';
|
||||
import { StockTransfer } from './inventory/StockTransfer';
|
||||
import { InvoiceStatus, ModelNameEnum } from './types';
|
||||
import { Lead } from './baseModels/Lead/Lead';
|
||||
import { PricingRule } from './baseModels/PricingRule/PricingRule';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { ApplicablePricingRules } from './baseModels/Invoice/types';
|
||||
|
||||
export function getQuoteActions(
|
||||
fyo: Fyo,
|
||||
@ -517,7 +520,7 @@ export function getPriceListStatusColumn(): ColumnConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export function getPriceListEnabledColumn(): ColumnConfig {
|
||||
export function getIsDocEnabledColumn(): ColumnConfig {
|
||||
return {
|
||||
label: t`Enabled`,
|
||||
fieldname: 'enabled',
|
||||
@ -657,3 +660,183 @@ export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
|
||||
|
||||
await item.set('item', name);
|
||||
}
|
||||
|
||||
export async function getPricingRule(
|
||||
doc: Invoice
|
||||
): Promise<ApplicablePricingRules[] | null> {
|
||||
if (
|
||||
!doc.fyo.singles.AccountingSettings?.enablePricingRule ||
|
||||
!doc.isSales ||
|
||||
!doc.items
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pricingRules: ApplicablePricingRules[] = [];
|
||||
|
||||
for (const item of doc.items) {
|
||||
if (item.isFreeItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pricingRuleDocNames = (
|
||||
await doc.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 pricingRuleDocsForItem = (await doc.fyo.db.getAll(
|
||||
ModelNameEnum.PricingRule,
|
||||
{
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
name: ['in', pricingRuleDocNames],
|
||||
isEnabled: true,
|
||||
},
|
||||
orderBy: 'priority',
|
||||
order: 'desc',
|
||||
}
|
||||
)) as PricingRule[];
|
||||
|
||||
const filtered = filterPricingRules(
|
||||
pricingRuleDocsForItem,
|
||||
doc.date as Date,
|
||||
item.quantity as number,
|
||||
item.amount as Money
|
||||
);
|
||||
|
||||
if (!filtered.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isPricingRuleHasConflicts = getPricingRulesConflicts(
|
||||
filtered,
|
||||
item.item as string
|
||||
);
|
||||
|
||||
if (isPricingRuleHasConflicts) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pricingRules.push({
|
||||
applyOnItem: item.item as string,
|
||||
pricingRule: filtered[0],
|
||||
});
|
||||
}
|
||||
|
||||
return pricingRules;
|
||||
}
|
||||
|
||||
export function filterPricingRules(
|
||||
pricingRuleDocsForItem: PricingRule[],
|
||||
sinvDate: Date,
|
||||
quantity: number,
|
||||
amount: Money
|
||||
): PricingRule[] | [] {
|
||||
const filteredPricingRules: PricingRule[] | undefined = [];
|
||||
|
||||
for (const pricingRuleDoc of pricingRuleDocsForItem) {
|
||||
if (canApplyPricingRule(pricingRuleDoc, sinvDate, quantity, amount)) {
|
||||
filteredPricingRules.push(pricingRuleDoc);
|
||||
}
|
||||
}
|
||||
return filteredPricingRules;
|
||||
}
|
||||
|
||||
export function canApplyPricingRule(
|
||||
pricingRuleDoc: PricingRule,
|
||||
sinvDate: Date,
|
||||
quantity: number,
|
||||
amount: Money
|
||||
): boolean {
|
||||
// Filter by Quantity
|
||||
if (
|
||||
(pricingRuleDoc.minQuantity as number) > 0 &&
|
||||
quantity < (pricingRuleDoc.minQuantity as number)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(pricingRuleDoc.maxQuantity as number) > 0 &&
|
||||
quantity > (pricingRuleDoc.maxQuantity as number)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by Amount
|
||||
if (
|
||||
!pricingRuleDoc.minAmount?.isZero() &&
|
||||
amount.lte(pricingRuleDoc.minAmount as Money)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!pricingRuleDoc.maxAmount?.isZero() &&
|
||||
amount.gte(pricingRuleDoc.maxAmount as Money)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by Validity
|
||||
if (
|
||||
pricingRuleDoc.validFrom &&
|
||||
new Date(sinvDate.setHours(0, 0, 0, 0)).toISOString() <
|
||||
pricingRuleDoc.validFrom.toISOString()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
pricingRuleDoc.validTo &&
|
||||
new Date(sinvDate.setHours(0, 0, 0, 0)).toISOString() >
|
||||
pricingRuleDoc.validTo.toISOString()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getPricingRulesConflicts(
|
||||
pricingRules: PricingRule[],
|
||||
item: string
|
||||
): string[] | undefined {
|
||||
const pricingRuleDocs = Array.from(pricingRules);
|
||||
|
||||
const firstPricingRule = pricingRuleDocs.shift();
|
||||
if (!firstPricingRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conflictingPricingRuleNames: string[] = [];
|
||||
for (const pricingRuleDoc of pricingRuleDocs.slice(0)) {
|
||||
if (pricingRuleDoc.priority !== firstPricingRule?.priority) {
|
||||
continue;
|
||||
}
|
||||
|
||||
conflictingPricingRuleNames.push(pricingRuleDoc.name as string);
|
||||
}
|
||||
|
||||
if (!conflictingPricingRuleNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ValidationError(
|
||||
t`Pricing Rules ${
|
||||
firstPricingRule.name as string
|
||||
}, ${conflictingPricingRuleNames.join(
|
||||
', '
|
||||
)} has the same Priority for the Item ${item}.`
|
||||
);
|
||||
}
|
||||
|
||||
export function roundFreeItemQty(
|
||||
quantity: number,
|
||||
roundingMethod: 'round' | 'floor' | 'ceil'
|
||||
): number {
|
||||
return Math[roundingMethod](quantity);
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ import { Payment } from './baseModels/Payment/Payment';
|
||||
import { PaymentFor } from './baseModels/PaymentFor/PaymentFor';
|
||||
import { PriceList } from './baseModels/PriceList/PriceList';
|
||||
import { PriceListItem } from './baseModels/PriceList/PriceListItem';
|
||||
import { PricingRule } from './baseModels/PricingRule/PricingRule';
|
||||
import { PricingRuleItem } from './baseModels/PricingRuleItem/PricingRuleItem';
|
||||
import { PrintSettings } from './baseModels/PrintSettings/PrintSettings';
|
||||
import { PrintTemplate } from './baseModels/PrintTemplate';
|
||||
import { PurchaseInvoice } from './baseModels/PurchaseInvoice/PurchaseInvoice';
|
||||
@ -61,6 +63,8 @@ export const models = {
|
||||
PrintSettings,
|
||||
PriceList,
|
||||
PriceListItem,
|
||||
PricingRule,
|
||||
PricingRuleItem,
|
||||
PurchaseInvoice,
|
||||
PurchaseInvoiceItem,
|
||||
SalesInvoice,
|
||||
|
@ -22,6 +22,9 @@ export enum ModelNameEnum {
|
||||
Payment = 'Payment',
|
||||
PaymentFor = 'PaymentFor',
|
||||
PriceList = 'PriceList',
|
||||
PricingRule = 'PricingRule',
|
||||
PricingRuleItem = 'PricingRuleItem',
|
||||
PricingRuleDetail = 'PricingRuleDetail',
|
||||
PrintSettings = 'PrintSettings',
|
||||
PrintTemplate = 'PrintTemplate',
|
||||
PurchaseInvoice = 'PurchaseInvoice',
|
||||
|
@ -107,6 +107,13 @@
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "enablePricingRule",
|
||||
"label": "Enable Pricing Rule",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscalYearStart",
|
||||
"label": "Fiscal Year Start Date",
|
||||
|
@ -62,6 +62,10 @@
|
||||
{
|
||||
"value": "PurchaseReceipt",
|
||||
"label": "Purchase Receipt"
|
||||
},
|
||||
{
|
||||
"value": "PricingRule",
|
||||
"label": "Pricing Rule"
|
||||
}
|
||||
],
|
||||
"default": "-",
|
||||
|
230
schemas/app/PricingRule.json
Normal file
230
schemas/app/PricingRule.json
Normal file
@ -0,0 +1,230 @@
|
||||
{
|
||||
"name": "PricingRule",
|
||||
"label": "Pricing Rule",
|
||||
"naming": "numberSeries",
|
||||
"isSubmittable": false,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "numberSeries",
|
||||
"label": "Number Series",
|
||||
"fieldtype": "Link",
|
||||
"target": "NumberSeries",
|
||||
"create": true,
|
||||
"required": true,
|
||||
"default": "PRLE-",
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "isEnabled",
|
||||
"label": "Is Pricing Rule Enabled",
|
||||
"fieldtype": "Check",
|
||||
"default": true,
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"label": "Title",
|
||||
"fieldtype": "Data",
|
||||
"required": true,
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "appliedItems",
|
||||
"label": "Applied Items",
|
||||
"fieldtype": "Table",
|
||||
"target": "PricingRuleItem",
|
||||
"required": true,
|
||||
"edit": true,
|
||||
"section": "Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "discountType",
|
||||
"label": "Discount Type",
|
||||
"fieldtype": "Select",
|
||||
"required": true,
|
||||
"section": "Default",
|
||||
"options": [
|
||||
{
|
||||
"value": "Price Discount",
|
||||
"label": "Price Discount"
|
||||
},
|
||||
{
|
||||
"value": "Product Discount",
|
||||
"label": "Product Discount"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fieldname": "priceDiscountType",
|
||||
"label": "Price Discount Type",
|
||||
"fieldtype": "Select",
|
||||
"section": "Price Discount Scheme",
|
||||
"options": [
|
||||
{
|
||||
"value": "rate",
|
||||
"label": "Rate"
|
||||
},
|
||||
{
|
||||
"value": "percentage",
|
||||
"label": "Discount Percentage"
|
||||
},
|
||||
{
|
||||
"value": "amount",
|
||||
"label": "Discount Amount"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fieldname": "discountRate",
|
||||
"label": "Rate",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Price Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "discountPercentage",
|
||||
"label": "Discount Percentage",
|
||||
"fieldtype": "Float",
|
||||
"section": "Price Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "discountAmount",
|
||||
"label": "Discount Amount",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Price Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "freeItem",
|
||||
"label": "Free Item",
|
||||
"fieldtype": "Link",
|
||||
"target": "Item",
|
||||
"section": "Product Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "freeItemQuantity",
|
||||
"label": "Quantity",
|
||||
"fieldtype": "Float",
|
||||
"section": "Product Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "freeItemUnit",
|
||||
"label": "UOM",
|
||||
"fieldtype": "Link",
|
||||
"target": "UOM",
|
||||
"section": "Product Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "freeItemRate",
|
||||
"label": "Rate",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Product Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "roundFreeItemQty",
|
||||
"label": "Round Free Item Quantity",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Product Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "roundingMethod",
|
||||
"label": "Rounding Method",
|
||||
"fieldtype": "Select",
|
||||
"required": true,
|
||||
"default": "round",
|
||||
"section": "Product Discount Scheme",
|
||||
"options": [
|
||||
{
|
||||
"value": "floor",
|
||||
"label": "Floor"
|
||||
},
|
||||
{
|
||||
"value": "round",
|
||||
"label": "Round"
|
||||
},
|
||||
{
|
||||
"value": "ceil",
|
||||
"label": "Ceil"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fieldname": "isRecursive",
|
||||
"label": "Is Recursive",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Product Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "recurseEvery",
|
||||
"label": "Recurse Every (As Per Transaction Unit)",
|
||||
"fieldtype": "Float",
|
||||
"section": "Product Discount Scheme"
|
||||
},
|
||||
{
|
||||
"fieldname": "minQuantity",
|
||||
"label": "Min Qty (As Per Stock Unit)",
|
||||
"fieldtype": "Float",
|
||||
"section": "Quantity and Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "maxQuantity",
|
||||
"label": "Max Qty (As Per Stock Unit)",
|
||||
"fieldtype": "Float",
|
||||
"section": "Quantity and Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "minAmount",
|
||||
"label": "Min Amount",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Quantity and Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "maxAmount",
|
||||
"label": "Max Amount",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Quantity and Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "validFrom",
|
||||
"label": "Valid From",
|
||||
"fieldtype": "Date",
|
||||
"section": "Validity"
|
||||
},
|
||||
{
|
||||
"fieldname": "validTo",
|
||||
"label": "Valid To",
|
||||
"fieldtype": "Date",
|
||||
"section": "Validity"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority",
|
||||
"label": "Priority",
|
||||
"fieldtype": "Select",
|
||||
"section": "Priority",
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "value": "1", "label": 1 },
|
||||
{ "value": "2", "label": 2 },
|
||||
{ "value": "3", "label": 3 },
|
||||
{ "value": "4", "label": 4 },
|
||||
{ "value": "5", "label": 5 },
|
||||
{ "value": "6", "label": 6 },
|
||||
{ "value": "7", "label": 7 },
|
||||
{ "value": "8", "label": 8 },
|
||||
{ "value": "9", "label": 9 },
|
||||
{ "value": "10", "label": 10 },
|
||||
{ "value": "11", "label": 11 },
|
||||
{ "value": "12", "label": 12 },
|
||||
{ "value": "13", "label": 13 },
|
||||
{ "value": "14", "label": 14 },
|
||||
{ "value": "15", "label": 15 },
|
||||
{ "value": "16", "label": 16 },
|
||||
{ "value": "17", "label": 17 },
|
||||
{ "value": "18", "label": 18 },
|
||||
{ "value": "19", "label": 19 },
|
||||
{ "value": "20", "label": 20 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"keywordFields": ["name"]
|
||||
}
|
23
schemas/app/PricingRuleDetail.json
Normal file
23
schemas/app/PricingRuleDetail.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "PricingRuleDetail",
|
||||
"label": "Pricing Rule Detail",
|
||||
"isSingle": false,
|
||||
"isChild": true,
|
||||
"fields": [
|
||||
{
|
||||
"label": "Pricing Rule",
|
||||
"fieldname": "referenceName",
|
||||
"fieldtype": "Link",
|
||||
"target": "PricingRule",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"label": "Item",
|
||||
"fieldname": "referenceItem",
|
||||
"fieldtype": "Link",
|
||||
"target": "Item",
|
||||
"readOnly": true
|
||||
}
|
||||
],
|
||||
"tableFields": ["referenceName", "referenceItem"]
|
||||
}
|
25
schemas/app/PricingRuleItem.json
Normal file
25
schemas/app/PricingRuleItem.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "PricingRuleItem",
|
||||
"label": "Pricing Rule Item",
|
||||
"isChild": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "item",
|
||||
"label": "Item",
|
||||
"fieldtype": "Link",
|
||||
"target": "Item",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "unit",
|
||||
"label": "Unit Type",
|
||||
"placeholder": "Unit Type",
|
||||
"fieldtype": "Link",
|
||||
"target": "UOM",
|
||||
"create": true,
|
||||
"section": "Default"
|
||||
}
|
||||
],
|
||||
"tableFields": ["item", "unit"],
|
||||
"quickEditFields": ["item", "unit"]
|
||||
}
|
@ -75,6 +75,21 @@
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"fieldname": "isPricingRuleApplied",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"fieldname": "pricingRuleDetail",
|
||||
"fieldtype": "Table",
|
||||
"label": "Pricing Rule Detail",
|
||||
"target": "PricingRuleDetail",
|
||||
"edit": false,
|
||||
"readOnly": true,
|
||||
"section": "References"
|
||||
}
|
||||
],
|
||||
"keywordFields": ["name", "party"]
|
||||
|
@ -1,5 +1,13 @@
|
||||
{
|
||||
"name": "SalesInvoiceItem",
|
||||
"label": "Sales Invoice Item",
|
||||
"extends": "InvoiceItem"
|
||||
"extends": "InvoiceItem",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "isFreeItem",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ import Payment from './app/Payment.json';
|
||||
import PaymentFor from './app/PaymentFor.json';
|
||||
import PriceList from './app/PriceList.json';
|
||||
import PriceListItem from './app/PriceListItem.json';
|
||||
import PricingRule from './app/PricingRule.json';
|
||||
import PricingRuleItem from './app/PricingRuleItem.json';
|
||||
import PricingRuleDetail from './app/PricingRuleDetail.json';
|
||||
import PrintSettings from './app/PrintSettings.json';
|
||||
import PrintTemplate from './app/PrintTemplate.json';
|
||||
import PurchaseInvoice from './app/PurchaseInvoice.json';
|
||||
@ -122,6 +125,10 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
||||
PriceList as Schema,
|
||||
PriceListItem as SchemaStub,
|
||||
|
||||
PricingRule as Schema,
|
||||
PricingRuleItem as SchemaStub,
|
||||
PricingRuleDetail as SchemaStub,
|
||||
|
||||
Tax as Schema,
|
||||
TaxDetail as Schema,
|
||||
TaxSummary as Schema,
|
||||
|
@ -217,6 +217,7 @@ import {
|
||||
validateSinv,
|
||||
} from 'src/utils/pos';
|
||||
import Barcode from 'src/components/Controls/Barcode.vue';
|
||||
import { getPricingRule } from 'models/helpers';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'POS',
|
||||
@ -361,10 +362,27 @@ 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
|
||||
this.sinvDoc.runFormulas();
|
||||
await this.sinvDoc.runFormulas();
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
@ -384,17 +402,21 @@ export default defineComponent({
|
||||
|
||||
const existingItems =
|
||||
this.sinvDoc.items?.filter(
|
||||
(invoiceItem) => invoiceItem.item === item.name
|
||||
(invoiceItem) =>
|
||||
invoiceItem.item === item.name && !invoiceItem.isFreeItem
|
||||
) ?? [];
|
||||
|
||||
if (item.hasBatch) {
|
||||
for (const item of existingItems) {
|
||||
const itemQty = item.quantity ?? 0;
|
||||
for (const invItem of existingItems) {
|
||||
const itemQty = invItem.quantity ?? 0;
|
||||
const qtyInBatch =
|
||||
this.itemQtyMap[item.item as string][item.batch as string] ?? 0;
|
||||
this.itemQtyMap[invItem.item as string][invItem.batch as string] ??
|
||||
0;
|
||||
|
||||
if (itemQty < qtyInBatch) {
|
||||
item.quantity = (item.quantity as number) + 1;
|
||||
invItem.quantity = (invItem.quantity as number) + 1;
|
||||
invItem.rate = item.rate as Money;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -414,7 +436,10 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (existingItems.length) {
|
||||
existingItems[0].rate = item.rate as Money;
|
||||
existingItems[0].quantity = (existingItems[0].quantity as number) + 1;
|
||||
await this.applyPricingRule();
|
||||
await this.sinvDoc.runFormulas();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -562,6 +587,21 @@ export default defineComponent({
|
||||
validateSinv(this.sinvDoc as SalesInvoice, this.itemQtyMap);
|
||||
await validateShipment(this.itemSerialNumbers);
|
||||
},
|
||||
async applyPricingRule() {
|
||||
const hasPricingRules = await getPricingRule(
|
||||
this.sinvDoc as SalesInvoice
|
||||
);
|
||||
|
||||
if (!hasPricingRules || !hasPricingRules.length) {
|
||||
this.sinvDoc.pricingRuleDetail = undefined;
|
||||
this.sinvDoc.isPricingRuleApplied = false;
|
||||
this.removeFreeItems();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sinvDoc.appendPricingRuleDetail(hasPricingRules);
|
||||
await this.sinvDoc.applyProductDiscount();
|
||||
},
|
||||
|
||||
getItem,
|
||||
},
|
||||
|
@ -21,7 +21,8 @@ import { showToast } from './interactive';
|
||||
export async function getItemQtyMap(): Promise<ItemQtyMap> {
|
||||
const itemQtyMap: ItemQtyMap = {};
|
||||
const valuationMethod =
|
||||
fyo.singles.InventorySettings?.valuationMethod ?? ValuationMethod.FIFO;
|
||||
(fyo.singles.InventorySettings?.valuationMethod as ValuationMethod) ??
|
||||
ValuationMethod.FIFO;
|
||||
|
||||
const rawSLEs = await getRawStockLedgerEntries(fyo);
|
||||
const rawData = getStockLedgerEntries(rawSLEs, valuationMethod);
|
||||
|
@ -279,6 +279,13 @@ function getCompleteSidebar(): SidebarConfig {
|
||||
schemaName: 'PriceList',
|
||||
hidden: () => !fyo.singles.AccountingSettings?.enablePriceList,
|
||||
},
|
||||
{
|
||||
label: t`Pricing Rule`,
|
||||
name: 'pricing-rule',
|
||||
route: '/list/PricingRule',
|
||||
schemaName: 'PricingRule',
|
||||
hidden: () => !fyo.singles.AccountingSettings?.enablePricingRule,
|
||||
},
|
||||
] as SidebarItem[],
|
||||
},
|
||||
getReportSidebar(),
|
||||
|
Loading…
Reference in New Issue
Block a user