From 3862fe1006ca527629aa51df3e9b5b5b06dfc214 Mon Sep 17 00:00:00 2001 From: akshayitzme Date: Tue, 6 Jun 2023 14:29:08 +0530 Subject: [PATCH] feat: price list --- .../AccountingSettings/AccountingSettings.ts | 1 + models/baseModels/Invoice/Invoice.ts | 6 + models/baseModels/InvoiceItem/InvoiceItem.ts | 21 +++- models/baseModels/ItemPrice/ItemPrice.ts | 60 +++++++++ models/baseModels/PriceList/PriceList.ts | 18 +++ models/helpers.ts | 118 ++++++++++++++++++ models/index.ts | 4 + models/types.ts | 2 + schemas/schemas.ts | 5 + src/utils/sidebarConfig.ts | 7 ++ 10 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 models/baseModels/ItemPrice/ItemPrice.ts create mode 100644 models/baseModels/PriceList/PriceList.ts diff --git a/models/baseModels/AccountingSettings/AccountingSettings.ts b/models/baseModels/AccountingSettings/AccountingSettings.ts index 20da1baf..e1cef9da 100644 --- a/models/baseModels/AccountingSettings/AccountingSettings.ts +++ b/models/baseModels/AccountingSettings/AccountingSettings.ts @@ -14,6 +14,7 @@ import { getCountryInfo } from 'utils/misc'; export class AccountingSettings extends Doc { enableDiscounting?: boolean; enableInventory?: boolean; + enablePriceList?: boolean; static filters: FiltersMap = { writeOffAccount: () => ({ diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 865307ea..0cac0054 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -35,6 +35,7 @@ export abstract class Invoice extends Transactional { party?: string; account?: string; currency?: string; + priceList?: string; netTotal?: Money; grandTotal?: Money; baseGrandTotal?: Money; @@ -514,6 +515,7 @@ export abstract class Invoice extends Transactional { attachment: () => !(this.attachment || !(this.isSubmitted || this.isCancelled)), backReference: () => !this.backReference, + priceList: () => !this.fyo.singles.AccountingSettings?.enablePriceList, }; static defaults: DefaultMap = { @@ -544,6 +546,10 @@ export abstract class Invoice extends Transactional { accountType: doc.isSales ? 'Receivable' : 'Payable', }), numberSeries: (doc: Doc) => ({ referenceType: doc.schemaName }), + priceList: (doc: Doc) => ({ + enabled: true, + ...(doc.isSales ? { selling: true } : { buying: true }), + }), }; static createFilters: FiltersMap = { diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index 1b48534d..0bbff395 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -17,6 +17,7 @@ import { safeParseFloat } from 'utils/index'; import { Invoice } from '../Invoice/Invoice'; import { Item } from '../Item/Item'; import { StockTransfer } from 'models/inventory/StockTransfer'; +import { getPriceListRate } from 'models/helpers'; export abstract class InvoiceItem extends Doc { item?: string; @@ -48,6 +49,18 @@ export abstract class InvoiceItem extends Doc { return this.schemaName === 'SalesInvoiceItem'; } + get date() { + return this.parentdoc?.date ?? undefined; + } + + get party() { + return this.parentdoc?.party ?? undefined; + } + + get priceList() { + return this.parentdoc?.priceList ?? undefined; + } + get discountAfterTax() { return !!this?.parentdoc?.discountAfterTax; } @@ -101,12 +114,15 @@ export abstract class InvoiceItem extends Doc { }, rate: { formula: async (fieldname) => { - const rate = (await this.fyo.getValue( + const priceListRate = await getPriceListRate(this); + const itemRate = (await this.fyo.getValue( 'Item', this.item as string, 'rate' )) as undefined | Money; + const rate = priceListRate instanceof Money ? priceListRate : itemRate; + if (!rate?.float && this.rate?.float) { return this.rate; } @@ -144,6 +160,9 @@ export abstract class InvoiceItem extends Doc { return rateFromTotals ?? rate ?? this.fyo.pesa(0); }, dependsOn: [ + 'date', + 'priceList', + 'batch', 'party', 'exchangeRate', 'item', diff --git a/models/baseModels/ItemPrice/ItemPrice.ts b/models/baseModels/ItemPrice/ItemPrice.ts new file mode 100644 index 00000000..b692e364 --- /dev/null +++ b/models/baseModels/ItemPrice/ItemPrice.ts @@ -0,0 +1,60 @@ +import { t } from 'fyo'; +import { DocValue } from 'fyo/core/types'; +import { Doc } from 'fyo/model/doc'; +import { ValidationMap } from 'fyo/model/types'; +import { ValidationError } from 'fyo/utils/errors'; +import { getItemPrice } from 'models/helpers'; +import { ModelNameEnum } from 'models/types'; +import { Money } from 'pesa'; + +export class ItemPrice extends Doc { + item?: string; + rate?: Money; + validFrom?: Date; + validUpto?: Date; + + get isBuying() { + return !!this.parentdoc?.buying; + } + + get isSelling() { + return !!this.parentdoc?.selling; + } + + get priceList() { + return this.parentdoc?.name; + } + + validations: ValidationMap = { + validUpto: async (value: DocValue) => { + if (!value || !this.validFrom) { + return; + } + if (value < this.validFrom) { + throw new ValidationError( + t`Valid From date can not be greater than Valid To date.` + ); + } + + const itemPrice = await getItemPrice( + this, + this.validFrom, + this.validUpto + ); + + if (!itemPrice) { + return; + } + + const priceList = (await this.fyo.getValue( + ModelNameEnum.ItemPrice, + itemPrice, + 'parent' + )) as string; + + throw new ValidationError( + t`an Item Price already exists for the given date in Price List ${priceList}` + ); + }, + }; +} diff --git a/models/baseModels/PriceList/PriceList.ts b/models/baseModels/PriceList/PriceList.ts new file mode 100644 index 00000000..7eb2b88f --- /dev/null +++ b/models/baseModels/PriceList/PriceList.ts @@ -0,0 +1,18 @@ +import { Doc } from 'fyo/model/doc'; +import { ListViewSettings } from 'fyo/model/types'; +import { ItemPrice } from '../ItemPrice/ItemPrice'; +import { getPriceListStatusColumn } from 'models/helpers'; + +export class PriceList extends Doc { + enabled?: boolean; + buying?: boolean; + selling?: boolean; + isUomDependent?: boolean; + priceListItem?: ItemPrice[]; + + static getListViewSettings(): ListViewSettings { + return { + columns: ['name', getPriceListStatusColumn()], + }; + } +} diff --git a/models/helpers.ts b/models/helpers.ts index 3f75c3c1..cf6d4082 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -17,6 +17,8 @@ import { Invoice } from './baseModels/Invoice/Invoice'; import { StockMovement } from './inventory/StockMovement'; import { StockTransfer } from './inventory/StockTransfer'; import { InvoiceStatus, ModelNameEnum } from './types'; +import { InvoiceItem } from './baseModels/InvoiceItem/InvoiceItem'; +import { ItemPrice } from './baseModels/ItemPrice/ItemPrice'; export function getInvoiceActions( fyo: Fyo, @@ -325,6 +327,122 @@ export function getSerialNumberStatusText(status: string): string { } } +export function getPriceListStatusColumn(): ColumnConfig { + return { + label: t`Enabled For`, + fieldname: 'enabledFor', + fieldtype: 'Select', + render(doc) { + let status = 'None'; + + if (doc.buying && !doc.selling) { + status = 'Buying'; + } + + if (doc.selling && !doc.buying) { + status = 'Selling'; + } + + if (doc.buying && doc.selling) { + status = 'Buying & Selling'; + } + + return { + template: `${status}`, + }; + }, + }; +} + +export async function getItemPrice( + doc: InvoiceItem | ItemPrice, + validFrom?: Date, + validUpto?: Date +): Promise { + if (!doc.item || !doc.priceList) { + return; + } + + const isUomDependent = await doc.fyo.getValue( + ModelNameEnum.PriceList, + doc.priceList, + 'isUomDependent' + ); + + const itemPriceQuery = Object.values( + await doc.fyo.db.getAll(ModelNameEnum.ItemPrice, { + filters: { + enabled: true, + item: doc.item, + ...(doc.isSales ? { selling: true } : { buying: true }), + ...(doc.batch ? { batch: doc.batch as string } : { batch: null }), + }, + fields: ['name', 'unit', 'party', 'batch', 'validFrom', 'validUpto'], + }) + )[0]; + + if (!itemPriceQuery) { + return; + } + + const { name, unit, party } = itemPriceQuery; + const validFromDate = validFrom ?? itemPriceQuery.validFrom; + const validUptoDate = validFrom ?? itemPriceQuery.validUpto; + let date; + + if (doc.date) { + date = new Date((doc.date as Date).setHours(0, 0, 0)); + } + + if (isUomDependent && unit !== doc.unit) { + return; + } + + if (party && doc.party !== party) { + return; + } + + if (date instanceof Date) { + if (validFromDate && date < validFromDate) { + return; + } + + if (validUptoDate && date > validUptoDate) { + return; + } + } + + if (validFrom && validUpto) { + if (validFromDate && validFrom < validFromDate) { + return; + } + + if (validUptoDate && validFrom > validUptoDate) { + return; + } + } + + return name as string; +} + +export async function getPriceListRate( + doc: InvoiceItem +): Promise { + const itemPrice = await getItemPrice(doc); + + if (!itemPrice) { + return; + } + + const itemPriceRate = (await doc.fyo.getValue( + ModelNameEnum.ItemPrice, + itemPrice, + 'rate' + )) as Money; + + return itemPriceRate; +} + export async function getExchangeRate({ fromCurrency, toCurrency, diff --git a/models/index.ts b/models/index.ts index 68f3edab..2a3e9de6 100644 --- a/models/index.ts +++ b/models/index.ts @@ -10,6 +10,8 @@ import { JournalEntryAccount } from './baseModels/JournalEntryAccount/JournalEnt import { Party } from './baseModels/Party/Party'; import { Payment } from './baseModels/Payment/Payment'; import { PaymentFor } from './baseModels/PaymentFor/PaymentFor'; +import { PriceList } from './baseModels/PriceList/PriceList'; +import { ItemPrice } from './baseModels/ItemPrice/ItemPrice'; import { PrintSettings } from './baseModels/PrintSettings/PrintSettings'; import { PurchaseInvoice } from './baseModels/PurchaseInvoice/PurchaseInvoice'; import { PurchaseInvoiceItem } from './baseModels/PurchaseInvoiceItem/PurchaseInvoiceItem'; @@ -45,6 +47,8 @@ export const models = { Payment, PaymentFor, PrintSettings, + PriceList, + ItemPrice, PurchaseInvoice, PurchaseInvoiceItem, SalesInvoice, diff --git a/models/types.ts b/models/types.ts index d3b06a4c..65077b43 100644 --- a/models/types.ts +++ b/models/types.ts @@ -10,6 +10,7 @@ export enum ModelNameEnum { GetStarted = 'GetStarted', Defaults = 'Defaults', Item = 'Item', + ItemPrice = 'ItemPrice', UOM = 'UOM', UOMConversionItem = 'UOMConversionItem', JournalEntry = 'JournalEntry', @@ -19,6 +20,7 @@ export enum ModelNameEnum { Party = 'Party', Payment = 'Payment', PaymentFor = 'PaymentFor', + PriceList = 'PriceList', PrintSettings = 'PrintSettings', PrintTemplate = 'PrintTemplate', PurchaseInvoice = 'PurchaseInvoice', diff --git a/schemas/schemas.ts b/schemas/schemas.ts index dbfc741b..06b0e456 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -9,6 +9,8 @@ import Defaults from './app/Defaults.json'; import GetStarted from './app/GetStarted.json'; import InventorySettings from './app/inventory/InventorySettings.json'; import Location from './app/inventory/Location.json'; +import PriceList from './app/PriceList.json'; +import ItemPrice from './app/ItemPrice.json'; import PurchaseReceipt from './app/inventory/PurchaseReceipt.json'; import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.json'; import SerialNumber from './app/inventory/SerialNumber.json'; @@ -100,6 +102,9 @@ export const appSchemas: Schema[] | SchemaStub[] = [ SalesInvoiceItem as SchemaStub, PurchaseInvoiceItem as SchemaStub, + PriceList as Schema, + ItemPrice as SchemaStub, + Tax as Schema, TaxDetail as Schema, TaxSummary as Schema, diff --git a/src/utils/sidebarConfig.ts b/src/utils/sidebarConfig.ts index 4eaba42b..92f11a58 100644 --- a/src/utils/sidebarConfig.ts +++ b/src/utils/sidebarConfig.ts @@ -245,6 +245,13 @@ async function getCompleteSidebar(): Promise { schemaName: 'Item', filters: { for: 'Both' }, }, + { + label: t`Price List`, + name: 'price-list', + route: '/list/PriceList', + schemaName: 'PriceList', + hidden: () => !fyo.singles.AccountingSettings?.enablePriceList, + }, ] as SidebarItem[], }, await getReportSidebar(),