mirror of
https://github.com/frappe/books.git
synced 2025-01-03 15:17:30 +00:00
incr: add StockQueue manager class
This commit is contained in:
parent
e1123ecc3a
commit
a277c748fb
88
models/inventory/stockQueue.ts
Normal file
88
models/inventory/stockQueue.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
export class StockQueue {
|
||||||
|
quantity: number;
|
||||||
|
value: number;
|
||||||
|
queue: { rate: number; quantity: number }[];
|
||||||
|
movingAverage: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.value = 0;
|
||||||
|
this.quantity = 0;
|
||||||
|
this.movingAverage = 0;
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get fifo() {
|
||||||
|
/**
|
||||||
|
* Stock value maintained is based on the stock queue
|
||||||
|
* ∴ FIFO by default. This returns FIFO valuation rate.
|
||||||
|
*/
|
||||||
|
const valuation = this.value / this.quantity;
|
||||||
|
if (Number.isNaN(valuation)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
inward(rate: number, quantity: number): null | number {
|
||||||
|
if (quantity <= 0 || rate < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inwardValue = rate * quantity;
|
||||||
|
/**
|
||||||
|
* Update Moving Average valuation
|
||||||
|
*/
|
||||||
|
this.movingAverage =
|
||||||
|
(this.movingAverage * this.quantity + inwardValue) /
|
||||||
|
(this.quantity + quantity);
|
||||||
|
|
||||||
|
this.quantity += quantity;
|
||||||
|
this.value += inwardValue;
|
||||||
|
|
||||||
|
const last = this.queue.at(-1);
|
||||||
|
if (last?.rate !== rate) {
|
||||||
|
this.queue.push({ rate, quantity });
|
||||||
|
} else {
|
||||||
|
last.quantity += quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
outward(quantity: number): null | number {
|
||||||
|
if (this.quantity < quantity || quantity <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let incomingRate: number = 0;
|
||||||
|
this.quantity -= quantity;
|
||||||
|
let remaining = quantity;
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
const last = this.queue.shift();
|
||||||
|
if (last === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedQuantity = last.quantity;
|
||||||
|
let quantityRemoved = remaining;
|
||||||
|
|
||||||
|
const quantityLeft = storedQuantity - remaining;
|
||||||
|
remaining = remaining - storedQuantity;
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
quantityRemoved = storedQuantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantityLeft > 0) {
|
||||||
|
this.queue.unshift({ rate: last.rate, quantity: quantityLeft });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value -= last.rate * quantityRemoved;
|
||||||
|
incomingRate += quantityRemoved * last.rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return incomingRate / quantity;
|
||||||
|
}
|
||||||
|
}
|
116
models/inventory/tests/testStockQueue.spec.ts
Normal file
116
models/inventory/tests/testStockQueue.spec.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import test from 'tape';
|
||||||
|
import { StockQueue } from '../stockQueue';
|
||||||
|
|
||||||
|
test('stockQueue:initialization', (t) => {
|
||||||
|
const q = new StockQueue();
|
||||||
|
|
||||||
|
t.equal(q.quantity, 0);
|
||||||
|
t.equal(q.value, 0);
|
||||||
|
t.equal(q.queue.length, 0);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stockQueue:operations', (t) => {
|
||||||
|
const q = new StockQueue();
|
||||||
|
|
||||||
|
t.equal(q.inward(100, 4), 100);
|
||||||
|
t.equal(q.fifo, 100);
|
||||||
|
t.equal(q.movingAverage, 100);
|
||||||
|
t.equal(q.queue.length, 1);
|
||||||
|
t.equal(q.quantity, 4);
|
||||||
|
t.equal(q.value, 400);
|
||||||
|
|
||||||
|
t.equal(q.inward(200, 8), 200);
|
||||||
|
t.equal(q.fifo, (400 + 1600) / 12);
|
||||||
|
t.equal(q.movingAverage, (400 + 1600) / 12);
|
||||||
|
t.equal(q.queue.length, 2);
|
||||||
|
t.equal(q.quantity, 4 + 8);
|
||||||
|
t.equal(q.value, 400 + 1600);
|
||||||
|
|
||||||
|
t.equal(q.inward(300, 3), 300);
|
||||||
|
t.equal(q.fifo, (400 + 1600 + 900) / 15);
|
||||||
|
t.equal(q.movingAverage, (400 + 1600 + 900) / 15);
|
||||||
|
t.equal(q.queue.length, 3);
|
||||||
|
t.equal(q.quantity, 4 + 8 + 3);
|
||||||
|
t.equal(q.value, 400 + 1600 + 900);
|
||||||
|
|
||||||
|
t.equal(q.outward(3), 100);
|
||||||
|
t.equal(q.fifo, (100 + 1600 + 900) / 12);
|
||||||
|
t.equal(q.movingAverage, (400 + 1600 + 900) / 15);
|
||||||
|
t.equal(q.queue.length, 3);
|
||||||
|
t.equal(q.quantity, 1 + 8 + 3);
|
||||||
|
t.equal(q.value, 100 + 1600 + 900);
|
||||||
|
|
||||||
|
t.equal(q.outward(5), (100 + 800) / 5);
|
||||||
|
t.equal(q.fifo, (800 + 900) / 7);
|
||||||
|
t.equal(q.movingAverage, (400 + 1600 + 900) / 15);
|
||||||
|
t.equal(q.queue.length, 2);
|
||||||
|
t.equal(q.quantity, 4 + 3);
|
||||||
|
t.equal(q.value, 800 + 900);
|
||||||
|
|
||||||
|
t.equal(q.outward(4), 200);
|
||||||
|
t.equal(q.fifo, 900 / 3);
|
||||||
|
t.equal(q.movingAverage, (400 + 1600 + 900) / 15);
|
||||||
|
t.equal(q.queue.length, 1);
|
||||||
|
t.equal(q.quantity, 3);
|
||||||
|
t.equal(q.value, 900);
|
||||||
|
|
||||||
|
t.equal(q.outward(3), 300);
|
||||||
|
t.equal(q.fifo, 0);
|
||||||
|
t.equal(q.movingAverage, (400 + 1600 + 900) / 15);
|
||||||
|
t.equal(q.queue.length, 0);
|
||||||
|
t.equal(q.quantity, 0);
|
||||||
|
t.equal(q.value, 0);
|
||||||
|
|
||||||
|
t.equal(q.inward(100, 1), 100);
|
||||||
|
t.equal(q.fifo, 100);
|
||||||
|
t.equal(q.movingAverage, 100);
|
||||||
|
t.equal(q.queue.length, 1);
|
||||||
|
t.equal(q.quantity, 1);
|
||||||
|
t.equal(q.value, 100);
|
||||||
|
|
||||||
|
t.equal(q.inward(150, 1), 150);
|
||||||
|
t.equal(q.fifo, (100 + 150) / 2);
|
||||||
|
t.equal(q.movingAverage, (100 + 150) / 2);
|
||||||
|
t.equal(q.queue.length, 2);
|
||||||
|
t.equal(q.quantity, 2);
|
||||||
|
t.equal(q.value, 100 + 150);
|
||||||
|
|
||||||
|
t.equal(q.inward(100, 1), 100);
|
||||||
|
t.equal(q.fifo, (100 + 150 + 100) / 3);
|
||||||
|
t.equal(q.movingAverage, (100 + 150 + 100) / 3);
|
||||||
|
t.equal(q.queue.length, 3);
|
||||||
|
t.equal(q.quantity, 3);
|
||||||
|
t.equal(q.value, 100 + 150 + 100);
|
||||||
|
|
||||||
|
t.equal(q.outward(1), 100);
|
||||||
|
t.equal(q.fifo, (150 + 100) / 2);
|
||||||
|
t.equal(q.movingAverage, (100 + 150 + 100) / 3);
|
||||||
|
t.equal(q.queue.length, 2);
|
||||||
|
t.equal(q.quantity, 2);
|
||||||
|
t.equal(q.value, 150 + 100);
|
||||||
|
|
||||||
|
t.equal(q.outward(2), (150 + 100) / 2);
|
||||||
|
t.equal(q.fifo, 0);
|
||||||
|
t.equal(q.movingAverage, (100 + 150 + 100) / 3);
|
||||||
|
t.equal(q.queue.length, 0);
|
||||||
|
t.equal(q.quantity, 0);
|
||||||
|
t.equal(q.value, 0);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stockQueue:invalidOperations', (t) => {
|
||||||
|
const q = new StockQueue();
|
||||||
|
|
||||||
|
t.equal(q.outward(1), null);
|
||||||
|
t.equal(q.outward(0), null);
|
||||||
|
t.equal(q.outward(-5), null);
|
||||||
|
|
||||||
|
t.equal(q.inward(1000, -1), null);
|
||||||
|
t.equal(q.inward(0, 0), null);
|
||||||
|
t.equal(q.inward(-0.1, 5), null);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user