2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 23:30:56 +00:00

feat: model changes for pricing rule

This commit is contained in:
akshayitzme 2024-01-30 18:25:50 +05:30
parent 3346a733f3
commit 5efdd9fbc6
7 changed files with 381 additions and 8 deletions

View File

@ -18,6 +18,7 @@ export class AccountingSettings extends Doc {
enableLead?: boolean; enableLead?: boolean;
enableFormCustomization?: boolean; enableFormCustomization?: boolean;
enableInvoiceReturns?: boolean; enableInvoiceReturns?: boolean;
enablePricingRule?: boolean;
static filters: FiltersMap = { static filters: FiltersMap = {
writeOffAccount: () => ({ writeOffAccount: () => ({
@ -60,6 +61,8 @@ export class AccountingSettings extends Doc {
override hidden: HiddenMap = { override hidden: HiddenMap = {
discountAccount: () => !this.enableDiscounting, discountAccount: () => !this.enableDiscounting,
gstin: () => this.fyo.singles.SystemSettings?.countryCode !== 'in', gstin: () => this.fyo.singles.SystemSettings?.countryCode !== 'in',
enablePricingRule: () =>
!this.fyo.singles.AccountingSettings?.enableDiscounting,
}; };
async change(ch: ChangeArg) { async change(ch: ChangeArg) {

View File

@ -11,7 +11,14 @@ import {
import { DEFAULT_CURRENCY } from 'fyo/utils/consts'; import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
import { ValidationError } from 'fyo/utils/errors'; import { ValidationError } from 'fyo/utils/errors';
import { Transactional } from 'models/Transactional/Transactional'; 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 { StockTransfer } from 'models/inventory/StockTransfer';
import { validateBatch } from 'models/inventory/helpers'; import { validateBatch } from 'models/inventory/helpers';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
@ -27,6 +34,9 @@ import { Tax } from '../Tax/Tax';
import { TaxSummary } from '../TaxSummary/TaxSummary'; import { TaxSummary } from '../TaxSummary/TaxSummary';
import { ReturnDocItem } from 'models/inventory/types'; import { ReturnDocItem } from 'models/inventory/types';
import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/types'; import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/types';
import { PricingRule } from '../PricingRule/PricingRule';
import { ApplicablePricingRules } from './types';
import { PricingRuleDetail } from '../PricingRuleDetail/PricingRuleDetail';
export type TaxDetail = { export type TaxDetail = {
account: string; account: string;
@ -70,6 +80,8 @@ export abstract class Invoice extends Transactional {
isReturned?: boolean; isReturned?: boolean;
returnAgainst?: string; returnAgainst?: string;
pricingRuleDetail?: PricingRuleDetail[];
get isSales() { get isSales() {
return ( return (
this.schemaName === 'SalesInvoice' || this.schemaName == 'SalesQuote' this.schemaName === 'SalesInvoice' || this.schemaName == 'SalesQuote'
@ -625,6 +637,17 @@ export abstract class Invoice extends Transactional {
!!this.autoStockTransferLocation, !!this.autoStockTransferLocation,
dependsOn: [], dependsOn: [],
}, },
isPricingRuleApplied: {
formula: async () => {
const pricingRule = await this.getPricingRule();
if (pricingRule) {
await this.appendPricingRuleDetail(pricingRule);
}
return !!pricingRule?.length;
},
dependsOn: ['items'],
},
}; };
getStockTransferred() { getStockTransferred() {
@ -917,6 +940,14 @@ export abstract class Invoice extends Transactional {
return transfer; return transfer;
} }
async beforeSync(): Promise<void> {
await super.beforeSync();
if (this.pricingRuleDetail?.length) {
await this.applyProductDiscount();
}
}
async beforeCancel(): Promise<void> { async beforeCancel(): Promise<void> {
await super.beforeCancel(); await super.beforeCancel();
await this._validateStockTransferCancelled(); await this._validateStockTransferCancelled();
@ -1045,4 +1076,171 @@ export abstract class Invoice extends Transactional {
async addItem(name: string) { async addItem(name: string) {
return await addItem(name, this); 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,
});
}
}
}
} }

View File

@ -19,6 +19,9 @@ import { Item } from '../Item/Item';
import { StockTransfer } from 'models/inventory/StockTransfer'; import { StockTransfer } from 'models/inventory/StockTransfer';
import { PriceList } from '../PriceList/PriceList'; import { PriceList } from '../PriceList/PriceList';
import { isPesa } from 'fyo/utils'; 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 { export abstract class InvoiceItem extends Doc {
item?: string; item?: string;
@ -404,6 +407,42 @@ export abstract class InvoiceItem extends Doc {
}, },
dependsOn: ['item', 'quantity'], 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 = { validations: ValidationMap = {
@ -734,3 +773,30 @@ function getRate(
return null; 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;
}
}

View File

@ -2,7 +2,7 @@ import { Doc } from 'fyo/model/doc';
import { ListViewSettings } from 'fyo/model/types'; import { ListViewSettings } from 'fyo/model/types';
import { PriceListItem } from './PriceListItem'; import { PriceListItem } from './PriceListItem';
import { import {
getPriceListEnabledColumn, getIsDocEnabledColumn,
getPriceListStatusColumn, getPriceListStatusColumn,
} from 'models/helpers'; } from 'models/helpers';
@ -14,11 +14,7 @@ export class PriceList extends Doc {
static getListViewSettings(): ListViewSettings { static getListViewSettings(): ListViewSettings {
return { return {
columns: [ columns: ['name', getIsDocEnabledColumn(), getPriceListStatusColumn()],
'name',
getPriceListEnabledColumn(),
getPriceListStatusColumn(),
],
}; };
} }
} }

View File

@ -22,6 +22,8 @@ import { StockMovement } from './inventory/StockMovement';
import { StockTransfer } from './inventory/StockTransfer'; import { StockTransfer } from './inventory/StockTransfer';
import { InvoiceStatus, ModelNameEnum } from './types'; import { InvoiceStatus, ModelNameEnum } from './types';
import { Lead } from './baseModels/Lead/Lead'; import { Lead } from './baseModels/Lead/Lead';
import { PricingRule } from './baseModels/PricingRule/PricingRule';
import { showToast } from 'src/utils/interactive';
export function getQuoteActions( export function getQuoteActions(
fyo: Fyo, fyo: Fyo,
@ -517,7 +519,7 @@ export function getPriceListStatusColumn(): ColumnConfig {
}; };
} }
export function getPriceListEnabledColumn(): ColumnConfig { export function getIsDocEnabledColumn(): ColumnConfig {
return { return {
label: t`Enabled`, label: t`Enabled`,
fieldname: 'enabled', fieldname: 'enabled',
@ -657,3 +659,105 @@ export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
await item.set('item', name); 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;
}

View File

@ -14,6 +14,8 @@ import { Payment } from './baseModels/Payment/Payment';
import { PaymentFor } from './baseModels/PaymentFor/PaymentFor'; import { PaymentFor } from './baseModels/PaymentFor/PaymentFor';
import { PriceList } from './baseModels/PriceList/PriceList'; import { PriceList } from './baseModels/PriceList/PriceList';
import { PriceListItem } from './baseModels/PriceList/PriceListItem'; 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 { PrintSettings } from './baseModels/PrintSettings/PrintSettings';
import { PrintTemplate } from './baseModels/PrintTemplate'; import { PrintTemplate } from './baseModels/PrintTemplate';
import { PurchaseInvoice } from './baseModels/PurchaseInvoice/PurchaseInvoice'; import { PurchaseInvoice } from './baseModels/PurchaseInvoice/PurchaseInvoice';
@ -61,6 +63,8 @@ export const models = {
PrintSettings, PrintSettings,
PriceList, PriceList,
PriceListItem, PriceListItem,
PricingRule,
PricingRuleItem,
PurchaseInvoice, PurchaseInvoice,
PurchaseInvoiceItem, PurchaseInvoiceItem,
SalesInvoice, SalesInvoice,

View File

@ -22,6 +22,8 @@ export enum ModelNameEnum {
Payment = 'Payment', Payment = 'Payment',
PaymentFor = 'PaymentFor', PaymentFor = 'PaymentFor',
PriceList = 'PriceList', PriceList = 'PriceList',
PricingRule = 'PricingRule',
PricingRuleItem = 'PricingRuleItem',
PrintSettings = 'PrintSettings', PrintSettings = 'PrintSettings',
PrintTemplate = 'PrintTemplate', PrintTemplate = 'PrintTemplate',
PurchaseInvoice = 'PurchaseInvoice', PurchaseInvoice = 'PurchaseInvoice',