mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
feat: model changes for pricing rule
This commit is contained in:
parent
3346a733f3
commit
5efdd9fbc6
@ -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,14 @@ 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,
|
||||
} from 'models/helpers';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
import { validateBatch } from 'models/inventory/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
@ -27,6 +34,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 +80,8 @@ export abstract class Invoice extends Transactional {
|
||||
isReturned?: boolean;
|
||||
returnAgainst?: string;
|
||||
|
||||
pricingRuleDetail?: PricingRuleDetail[];
|
||||
|
||||
get isSales() {
|
||||
return (
|
||||
this.schemaName === 'SalesInvoice' || this.schemaName == 'SalesQuote'
|
||||
@ -625,6 +637,17 @@ export abstract class Invoice extends Transactional {
|
||||
!!this.autoStockTransferLocation,
|
||||
dependsOn: [],
|
||||
},
|
||||
isPricingRuleApplied: {
|
||||
formula: async () => {
|
||||
const pricingRule = await this.getPricingRule();
|
||||
if (pricingRule) {
|
||||
await this.appendPricingRuleDetail(pricingRule);
|
||||
}
|
||||
|
||||
return !!pricingRule?.length;
|
||||
},
|
||||
dependsOn: ['items'],
|
||||
},
|
||||
};
|
||||
|
||||
getStockTransferred() {
|
||||
@ -917,6 +940,14 @@ export abstract class Invoice extends Transactional {
|
||||
return transfer;
|
||||
}
|
||||
|
||||
async beforeSync(): Promise<void> {
|
||||
await super.beforeSync();
|
||||
|
||||
if (this.pricingRuleDetail?.length) {
|
||||
await this.applyProductDiscount();
|
||||
}
|
||||
}
|
||||
|
||||
async beforeCancel(): Promise<void> {
|
||||
await super.beforeCancel();
|
||||
await this._validateStockTransferCancelled();
|
||||
@ -1045,4 +1076,171 @@ export abstract class Invoice extends Transactional {
|
||||
async addItem(name: string) {
|
||||
return await addItem(name, this);
|
||||
}
|
||||
|
||||
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 appendPricingRuleDetail(
|
||||
applicablePricingRule: ApplicablePricingRules[]
|
||||
) {
|
||||
await this.set('pricingRuleDetail', null);
|
||||
|
||||
for (const doc of applicablePricingRule) {
|
||||
await this.append('pricingRuleDetail', {
|
||||
referenceName: doc.pricingRule.name,
|
||||
referenceItem: doc.applyOnItem,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async applyPriceDiscount() {
|
||||
if (!this.pricingRuleDetail || !this.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const doc of this.pricingRuleDetail) {
|
||||
const pricingRuleDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
doc.referenceName
|
||||
)) as PricingRule;
|
||||
|
||||
if (pricingRuleDoc.discountType === 'Product Discount') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const appliedItems = pricingRuleDoc.appliedItems?.map(
|
||||
(itemDoc) => itemDoc.item
|
||||
);
|
||||
|
||||
for (const item of this.items) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async applyProductDiscount() {
|
||||
if (!this.pricingRuleDetail || !this.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const doc of this.pricingRuleDetail) {
|
||||
const pricingRuleDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
doc.referenceName
|
||||
)) as PricingRule;
|
||||
|
||||
if (pricingRuleDoc.discountType === 'Price Discount') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const appliedItems = pricingRuleDoc.appliedItems?.map(
|
||||
(itemDoc) => itemDoc.item
|
||||
);
|
||||
|
||||
for (const item of this.items) {
|
||||
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);
|
||||
}
|
||||
|
||||
await this.append('items', {
|
||||
item: pricingRuleDoc.freeItem as string,
|
||||
quantity: freeItemQty,
|
||||
isFreeItem: true,
|
||||
rate: pricingRuleDoc.freeItemRate,
|
||||
unit: pricingRuleDoc.freeItemUnit,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,9 @@ import { Item } from '../Item/Item';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
import { PriceList } from '../PriceList/PriceList';
|
||||
import { isPesa } from 'fyo/utils';
|
||||
import { canApplyPricingRule } from 'models/helpers';
|
||||
import { PricingRule } from '../PricingRule/PricingRule';
|
||||
import { SalesInvoiceItem } from '../SalesInvoiceItem/SalesInvoiceItem';
|
||||
|
||||
export abstract class InvoiceItem extends Doc {
|
||||
item?: string;
|
||||
@ -404,6 +407,42 @@ export abstract class InvoiceItem extends Doc {
|
||||
},
|
||||
dependsOn: ['item', 'quantity'],
|
||||
},
|
||||
isPricingRuleAppliedItem: {
|
||||
formula: async () => {
|
||||
if (!this.parentdoc?.pricingRuleDetail) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const prleDoc of this.parentdoc.pricingRuleDetail) {
|
||||
const pricingRuleDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.PricingRule,
|
||||
prleDoc.referenceName
|
||||
)) as PricingRule;
|
||||
|
||||
if (
|
||||
!canApplyPricingRule(
|
||||
pricingRuleDoc,
|
||||
this.parentdoc.date as Date,
|
||||
this.quantity as number,
|
||||
this.amount as Money
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const appliedItems = pricingRuleDoc.appliedItems?.map(
|
||||
(item) => item.item
|
||||
);
|
||||
|
||||
if (!appliedItems?.includes(this.item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await applyPricingRuleOnItem(this, pricingRuleDoc);
|
||||
}
|
||||
},
|
||||
dependsOn: ['item', 'quantity'],
|
||||
},
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
@ -734,3 +773,30 @@ function getRate(
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function applyPricingRuleOnItem(
|
||||
sinvItemDoc: SalesInvoiceItem,
|
||||
pricingRuleDoc: PricingRule
|
||||
) {
|
||||
switch (pricingRuleDoc.priceDiscountType) {
|
||||
case 'rate':
|
||||
await sinvItemDoc.set('rate', pricingRuleDoc.discountRate);
|
||||
return;
|
||||
|
||||
case 'amount':
|
||||
await sinvItemDoc.set('setItemDiscountAmount', true);
|
||||
const discountAmount = pricingRuleDoc.discountAmount?.mul(
|
||||
sinvItemDoc.quantity as number
|
||||
);
|
||||
|
||||
await sinvItemDoc.set('itemDiscountAmount', discountAmount);
|
||||
return;
|
||||
|
||||
case 'percentage':
|
||||
await sinvItemDoc.set(
|
||||
'itemDiscountPercent',
|
||||
pricingRuleDoc.discountPercentage
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -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()],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ 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 { showToast } from 'src/utils/interactive';
|
||||
|
||||
export function getQuoteActions(
|
||||
fyo: Fyo,
|
||||
@ -517,7 +519,7 @@ export function getPriceListStatusColumn(): ColumnConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export function getPriceListEnabledColumn(): ColumnConfig {
|
||||
export function getIsDocEnabledColumn(): ColumnConfig {
|
||||
return {
|
||||
label: t`Enabled`,
|
||||
fieldname: 'enabled',
|
||||
@ -657,3 +659,105 @@ export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
|
||||
|
||||
await item.set('item', name);
|
||||
}
|
||||
|
||||
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 &&
|
||||
sinvDate.toISOString() < pricingRuleDoc.validFrom.toISOString()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
pricingRuleDoc.validTo &&
|
||||
sinvDate.toISOString() > pricingRuleDoc.validTo.toISOString()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getPricingRulesConflicts(
|
||||
pricingRules: PricingRule[],
|
||||
item: string
|
||||
) {
|
||||
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) {
|
||||
conflictingPricingRuleNames.push(pricingRuleDoc.name as string);
|
||||
}
|
||||
}
|
||||
|
||||
if (!conflictingPricingRuleNames.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`Pricing Rules ${
|
||||
firstPricingRule.name as string
|
||||
}, ${conflictingPricingRuleNames.join(
|
||||
', '
|
||||
)} has the same Priority for the Item ${item}.`,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
@ -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,8 @@ export enum ModelNameEnum {
|
||||
Payment = 'Payment',
|
||||
PaymentFor = 'PaymentFor',
|
||||
PriceList = 'PriceList',
|
||||
PricingRule = 'PricingRule',
|
||||
PricingRuleItem = 'PricingRuleItem',
|
||||
PrintSettings = 'PrintSettings',
|
||||
PrintTemplate = 'PrintTemplate',
|
||||
PurchaseInvoice = 'PurchaseInvoice',
|
||||
|
Loading…
Reference in New Issue
Block a user