mirror of
https://github.com/frappe/books.git
synced 2024-11-09 15:20:56 +00:00
feat: price list
This commit is contained in:
parent
2a82b021fb
commit
3862fe1006
@ -14,6 +14,7 @@ import { getCountryInfo } from 'utils/misc';
|
||||
export class AccountingSettings extends Doc {
|
||||
enableDiscounting?: boolean;
|
||||
enableInventory?: boolean;
|
||||
enablePriceList?: boolean;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
writeOffAccount: () => ({
|
||||
|
@ -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 = {
|
||||
|
@ -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',
|
||||
|
60
models/baseModels/ItemPrice/ItemPrice.ts
Normal file
60
models/baseModels/ItemPrice/ItemPrice.ts
Normal file
@ -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}`
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
18
models/baseModels/PriceList/PriceList.ts
Normal file
18
models/baseModels/PriceList/PriceList.ts
Normal file
@ -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()],
|
||||
};
|
||||
}
|
||||
}
|
@ -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: `<Badge class="text-xs" color="gray">${status}</Badge>`,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getItemPrice(
|
||||
doc: InvoiceItem | ItemPrice,
|
||||
validFrom?: Date,
|
||||
validUpto?: Date
|
||||
): Promise<string | undefined> {
|
||||
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<Money | undefined> {
|
||||
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,
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -245,6 +245,13 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
|
||||
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(),
|
||||
|
Loading…
Reference in New Issue
Block a user