mirror of
https://github.com/frappe/books.git
synced 2025-01-03 15:17:30 +00:00
incr: add Stock Manager
This commit is contained in:
parent
a8b5884929
commit
daaf56da48
@ -1,3 +1,15 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class StockLedgerEntry extends Doc {}
|
||||
|
||||
export class StockLedgerEntry extends Doc {
|
||||
date?: Date;
|
||||
item?: string;
|
||||
rate?: Money;
|
||||
quantity?: number;
|
||||
location?: string;
|
||||
referenceName?: string;
|
||||
referenceType?: string;
|
||||
stockValueBefore?: Money;
|
||||
stockValueAfter?: Money;
|
||||
}
|
||||
|
156
models/inventory/StockManager.ts
Normal file
156
models/inventory/StockManager.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { getStockQueue } from './helpers';
|
||||
import { StockLedgerEntry } from './StockLedgerEntry';
|
||||
import { SMDetails } from './types';
|
||||
|
||||
export class StockManager {
|
||||
/**
|
||||
* The Stock Manager is used to move stock to and from a location. It
|
||||
* updates the Stock Queue and creates Stock Ledger Entries.
|
||||
*
|
||||
* 1. Get existing stock Queue
|
||||
* 2. Get Stock Value Before from Stock Queue
|
||||
* 3. Update Stock Queue
|
||||
* 4. Get Stock Value After from Stock Queue
|
||||
* 5. Create Stock Ledger Entry
|
||||
* 6. Save Stock Queue
|
||||
* 7. Insert Stock Ledger Entry
|
||||
*/
|
||||
|
||||
date?: Date;
|
||||
item?: string;
|
||||
rate?: Money;
|
||||
quantity?: number;
|
||||
fromLocation?: string;
|
||||
toLocation?: string;
|
||||
referenceName?: string;
|
||||
referenceType?: string;
|
||||
stockValue?: string;
|
||||
stockValueDifference?: string;
|
||||
|
||||
fyo: Fyo;
|
||||
|
||||
constructor(fyo: Fyo) {
|
||||
this.fyo = fyo;
|
||||
}
|
||||
|
||||
moveStock(details: SMDetails) {
|
||||
this.date = details.date;
|
||||
this.item = details.item;
|
||||
this.quantity = details.quantity;
|
||||
this.fromLocation = details.fromLocation;
|
||||
this.toLocation = details.toLocation;
|
||||
this.referenceName = details.referenceName;
|
||||
this.referenceType = details.referenceType;
|
||||
|
||||
this.#validate();
|
||||
this.#moveStockForBothLocations();
|
||||
}
|
||||
|
||||
async #moveStockForBothLocations() {
|
||||
if (this.fromLocation) {
|
||||
await this.#moveStockForSingleLocation(this.fromLocation, true);
|
||||
}
|
||||
|
||||
if (this.toLocation) {
|
||||
await this.#moveStockForSingleLocation(this.toLocation, false);
|
||||
}
|
||||
}
|
||||
|
||||
async #moveStockForSingleLocation(location: string, isOutward: boolean) {
|
||||
let quantity = this.quantity!;
|
||||
if (isOutward) {
|
||||
quantity = -quantity;
|
||||
}
|
||||
|
||||
const { stockQueue, stockValueBefore, stockValueAfter } =
|
||||
await this.#makeStockQueueChange(location, isOutward);
|
||||
const stockLedgerEntry = this.#getStockLedgerEntry(
|
||||
location,
|
||||
quantity,
|
||||
stockValueBefore,
|
||||
stockValueAfter
|
||||
);
|
||||
|
||||
await stockQueue.sync();
|
||||
await stockLedgerEntry.sync();
|
||||
}
|
||||
|
||||
#getStockLedgerEntry(
|
||||
location: string,
|
||||
quantity: number,
|
||||
stockValueBefore: Money,
|
||||
stockValueAfter: Money
|
||||
) {
|
||||
return this.fyo.doc.getNewDoc(ModelNameEnum.StockLedgerEntry, {
|
||||
date: this.date,
|
||||
item: this.item,
|
||||
rate: this.rate,
|
||||
quantity,
|
||||
location,
|
||||
stockValueBefore,
|
||||
stockValueAfter,
|
||||
referenceName: this.referenceName,
|
||||
referenceType: this.referenceType,
|
||||
}) as StockLedgerEntry;
|
||||
}
|
||||
|
||||
async #makeStockQueueChange(location: string, isOutward: boolean) {
|
||||
const stockQueue = await getStockQueue(this.item!, location, this.fyo);
|
||||
const stockValueBefore = stockQueue.stockValue!;
|
||||
let isSuccess;
|
||||
|
||||
if (isOutward) {
|
||||
isSuccess = stockQueue.outward(-this.quantity!);
|
||||
} else {
|
||||
isSuccess = stockQueue.inward(this.rate!, this.quantity!);
|
||||
}
|
||||
|
||||
if (!isSuccess && isOutward) {
|
||||
throw new ValidationError(
|
||||
t`Stock Manager: Insufficient quantity ${
|
||||
stockQueue.quantity
|
||||
} at ${location} of ${this
|
||||
.item!} for outward transaction. Quantity required ${this.quantity!}.`
|
||||
);
|
||||
}
|
||||
|
||||
const stockValueAfter = stockQueue.stockValue!;
|
||||
|
||||
return { stockQueue, stockValueBefore, stockValueAfter };
|
||||
}
|
||||
|
||||
#validate() {
|
||||
this.#validateRate();
|
||||
this.#validateLocation();
|
||||
}
|
||||
|
||||
#validateRate() {
|
||||
if (!this.rate) {
|
||||
throw new ValidationError(t`Stock Manager: rate needs to be set`);
|
||||
}
|
||||
|
||||
if (this.rate.lte(0)) {
|
||||
throw new ValidationError(
|
||||
t`Stock Manager: rate (${this.rate.float}) has to be greater than zero`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#validateLocation() {
|
||||
if (this.fromLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.toLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ValidationError(
|
||||
t`Stock Manager: both From and To Location cannot be undefined`
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,119 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class StockQueue extends Doc {}
|
||||
type StockQueueItem = { rate: Money; quantity: number };
|
||||
|
||||
export class StockQueue extends Doc {
|
||||
item?: string;
|
||||
location?: string;
|
||||
queue?: string;
|
||||
stockValue?: Money;
|
||||
|
||||
/**
|
||||
* Stock Queue
|
||||
*
|
||||
* Used to keep track of inward rates for
|
||||
* stock valuation purposes.
|
||||
*/
|
||||
|
||||
get quantity(): number {
|
||||
return this.stockQueue.reduce((qty, sqi) => {
|
||||
return qty + sqi.quantity;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
get stockQueue(): StockQueueItem[] {
|
||||
const stringifiedRatesQueue = JSON.parse(this.queue ?? '[]') as {
|
||||
rate: string;
|
||||
quantity: number;
|
||||
}[];
|
||||
|
||||
return stringifiedRatesQueue.map(({ rate, quantity }) => ({
|
||||
rate: this.fyo.pesa(rate),
|
||||
quantity,
|
||||
}));
|
||||
}
|
||||
|
||||
set stockQueue(stockQueue: StockQueueItem[]) {
|
||||
const stringifiedRatesQueue = stockQueue.map(({ rate, quantity }) => ({
|
||||
rate: rate.store,
|
||||
quantity,
|
||||
}));
|
||||
this.queue = JSON.stringify(stringifiedRatesQueue);
|
||||
}
|
||||
|
||||
inward(rate: Money, quantity: number): boolean {
|
||||
const stockQueue = this.stockQueue;
|
||||
stockQueue.push({ rate, quantity });
|
||||
this.stockQueue = stockQueue;
|
||||
|
||||
this._updateStockValue(stockQueue);
|
||||
return true;
|
||||
}
|
||||
|
||||
outward(quantity: number): boolean {
|
||||
const stockQueue = this.stockQueue;
|
||||
const outwardQueues = getQueuesPostOutwards(stockQueue, quantity);
|
||||
if (!outwardQueues.isPossible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.stockQueue = outwardQueues.balanceQueue;
|
||||
this._updateStockValue(outwardQueues.balanceQueue);
|
||||
return true;
|
||||
}
|
||||
|
||||
_updateStockValue(stockQueue: StockQueueItem[]) {
|
||||
this.stockValue = stockQueue.reduce((acc, { rate, quantity }) => {
|
||||
return acc.add(rate.mul(quantity));
|
||||
}, this.fyo.pesa(0));
|
||||
}
|
||||
}
|
||||
|
||||
function getQueuesPostOutwards(
|
||||
stockQueue: StockQueueItem[],
|
||||
outwardQuantity: number
|
||||
) {
|
||||
const totalQuantity = stockQueue.reduce(
|
||||
(acc, { quantity }) => acc + quantity,
|
||||
0
|
||||
);
|
||||
|
||||
const isPossible = outwardQuantity <= totalQuantity;
|
||||
if (!isPossible) {
|
||||
return { isPossible };
|
||||
}
|
||||
|
||||
let outwardRemaining = outwardQuantity;
|
||||
const balanceQueue: StockQueueItem[] = [];
|
||||
const outwardQueue: StockQueueItem[] = [];
|
||||
|
||||
for (let i = stockQueue.length - 1; i >= 0; i--) {
|
||||
const { quantity, rate } = stockQueue[i];
|
||||
if (outwardRemaining === 0) {
|
||||
balanceQueue.unshift({ quantity, rate });
|
||||
}
|
||||
|
||||
const balanceRemaining = quantity - outwardRemaining;
|
||||
if (balanceRemaining === 0) {
|
||||
outwardQueue.push({ quantity, rate });
|
||||
outwardRemaining = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (balanceRemaining > 0) {
|
||||
outwardQueue.push({ quantity: outwardRemaining, rate });
|
||||
balanceQueue.unshift({ quantity: balanceRemaining, rate });
|
||||
outwardRemaining = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (balanceRemaining < 0) {
|
||||
outwardQueue.push({ quantity, rate });
|
||||
outwardRemaining = +balanceRemaining;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return { isPossible, outwardQueue, balanceQueue };
|
||||
}
|
||||
|
29
models/inventory/helpers.ts
Normal file
29
models/inventory/helpers.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { StockQueue } from './StockQueue';
|
||||
|
||||
export async function getStockQueue(
|
||||
item: string,
|
||||
location: string,
|
||||
fyo: Fyo
|
||||
): Promise<StockQueue> {
|
||||
/**
|
||||
* Create a new StockQueue if it doesn't exist.
|
||||
*/
|
||||
|
||||
const names = (await fyo.db.getAllRaw(ModelNameEnum.StockQueue, {
|
||||
filters: { item, location },
|
||||
fields: ['name'],
|
||||
limit: 1,
|
||||
})) as { name: string }[];
|
||||
const name = names?.[0]?.name;
|
||||
|
||||
if (!name) {
|
||||
return fyo.doc.getNewDoc(ModelNameEnum.StockQueue, {
|
||||
item,
|
||||
location,
|
||||
}) as StockQueue;
|
||||
}
|
||||
|
||||
return (await fyo.doc.getDoc(ModelNameEnum.StockQueue, name)) as StockQueue;
|
||||
}
|
@ -1,4 +1,17 @@
|
||||
import { Money } from "pesa";
|
||||
|
||||
export type MovementType =
|
||||
| 'MaterialIssue'
|
||||
| 'MaterialReceipt'
|
||||
| 'MaterialTransfer';
|
||||
|
||||
export interface SMDetails {
|
||||
date: Date;
|
||||
item: string;
|
||||
rate: Money;
|
||||
quantity: number;
|
||||
referenceName: string;
|
||||
referenceType: string;
|
||||
fromLocation?: string;
|
||||
toLocation?: string;
|
||||
}
|
@ -31,32 +31,12 @@
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "fromLocation",
|
||||
"label": "From Location",
|
||||
"fieldname": "location",
|
||||
"label": "Location",
|
||||
"fieldtype": "Link",
|
||||
"target": "Location",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "toLocation",
|
||||
"label": "To Location",
|
||||
"fieldtype": "Link",
|
||||
"target": "Location",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "referenceDetailName",
|
||||
"label": "Ref. Detail Name",
|
||||
"fieldtype": "DynamicLink",
|
||||
"references": "referenceDetailType",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "referenceDetailType",
|
||||
"label": "Ref. Detail Type",
|
||||
"fieldtype": "Data",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "referenceName",
|
||||
"label": "Ref. Name",
|
||||
@ -71,14 +51,14 @@
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "stockValue",
|
||||
"label": "Stock Value",
|
||||
"fieldname": "stockValueBefore",
|
||||
"label": "Stock Value Before",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "stockValueDifference",
|
||||
"label": "Stock Value Difference",
|
||||
"fieldname": "stockValueAfter",
|
||||
"label": "Stock Value After",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
}
|
||||
|
@ -3,13 +3,14 @@
|
||||
"label": "Stock Queue",
|
||||
"isSingle": false,
|
||||
"isChild": false,
|
||||
"naming": "random",
|
||||
"naming": "autoincrement",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "item",
|
||||
"label": "Item",
|
||||
"fieldtype": "Link",
|
||||
"target": "Item",
|
||||
"required": true,
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
@ -17,12 +18,14 @@
|
||||
"label": "Location",
|
||||
"fieldtype": "Link",
|
||||
"target": "Location",
|
||||
"required": true,
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "queue",
|
||||
"label": "Queue",
|
||||
"fieldtype": "Data",
|
||||
"required": true,
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
@ -30,12 +33,6 @@
|
||||
"label": "Stock Value",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "valuationRate",
|
||||
"label": "Valuation Rate",
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user