2
0
mirror of https://github.com/frappe/books.git synced 2024-11-14 01:14:03 +00:00
books/models/inventory/StockManager.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

329 lines
8.3 KiB
TypeScript
Raw Normal View History

2022-10-28 08:04:08 +00:00
import { Fyo, t } from 'fyo';
import { ValidationError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { StockLedgerEntry } from './StockLedgerEntry';
2022-10-29 07:07:52 +00:00
import { SMDetails, SMIDetails, SMTransferDetails } from './types';
2023-04-25 07:07:29 +00:00
import { getSerialNumbers } from './helpers';
2022-10-28 08:04:08 +00:00
export class StockManager {
/**
2022-10-29 07:07:52 +00:00
* The Stock Manager manages a group of Stock Manager Items
* all of which would belong to a single transaction such as a
* single Stock Movement entry.
*/
items: StockManagerItem[];
details: SMDetails;
isCancelled: boolean;
fyo: Fyo;
constructor(details: SMDetails, isCancelled: boolean, fyo: Fyo) {
this.items = [];
this.details = details;
this.isCancelled = isCancelled;
this.fyo = fyo;
}
async validateTransfers(transferDetails: SMTransferDetails[]) {
const detailsList = transferDetails.map((d) => this.#getSMIDetails(d));
for (const details of detailsList) {
await this.#validate(details);
}
}
async createTransfers(transferDetails: SMTransferDetails[]) {
2022-11-03 10:53:34 +00:00
const detailsList = transferDetails.map((d) => this.#getSMIDetails(d));
for (const details of detailsList) {
await this.#validate(details);
}
for (const details of detailsList) {
this.#createTransfer(details);
}
await this.#sync();
2022-10-29 07:07:52 +00:00
}
async cancelTransfers() {
const { referenceName, referenceType } = this.details;
await this.fyo.db.deleteAll(ModelNameEnum.StockLedgerEntry, {
referenceType,
referenceName,
});
}
async validateCancel(transferDetails: SMTransferDetails[]) {
const reverseTransferDetails = transferDetails.map(
2023-07-01 07:31:00 +00:00
({ item, rate, quantity, fromLocation, toLocation, isReturn }) => ({
item,
rate,
quantity,
fromLocation: toLocation,
toLocation: fromLocation,
2023-07-01 07:31:00 +00:00
isReturn,
})
);
await this.validateTransfers(reverseTransferDetails);
}
async #sync() {
2022-10-29 07:07:52 +00:00
for (const item of this.items) {
await item.sync();
}
}
#createTransfer(details: SMIDetails) {
const item = new StockManagerItem(details, this.fyo);
2022-11-03 10:53:34 +00:00
item.transferStock();
this.items.push(item);
}
2022-10-29 07:07:52 +00:00
#getSMIDetails(transferDetails: SMTransferDetails): SMIDetails {
return Object.assign({}, this.details, transferDetails);
}
2023-01-18 12:07:31 +00:00
2022-11-03 10:53:34 +00:00
async #validate(details: SMIDetails) {
this.#validateRate(details);
this.#validateQuantity(details);
this.#validateLocation(details);
await this.#validateStockAvailability(details);
}
#validateQuantity(details: SMIDetails) {
if (!details.quantity) {
throw new ValidationError(t`Quantity needs to be set`);
}
2023-07-01 07:31:00 +00:00
if (!details.isReturn && details.quantity <= 0) {
2022-11-03 10:53:34 +00:00
throw new ValidationError(
t`Quantity (${details.quantity}) has to be greater than zero`
);
}
}
#validateRate(details: SMIDetails) {
if (!details.rate) {
throw new ValidationError(t`Rate needs to be set`);
}
if (details.rate.lte(0)) {
throw new ValidationError(
t`Rate (${details.rate.float}) has to be greater than zero`
);
}
}
#validateLocation(details: SMIDetails) {
if (details.fromLocation) {
return;
}
if (details.toLocation) {
return;
}
throw new ValidationError(t`Both From and To Location cannot be undefined`);
}
2023-01-18 12:07:31 +00:00
2023-02-17 10:34:46 +00:00
async #validateStockAvailability(details: SMIDetails) {
2023-01-18 12:07:31 +00:00
if (!details.fromLocation) {
return;
}
2023-02-17 10:34:46 +00:00
const date = details.date.toISOString();
2023-02-21 06:07:55 +00:00
const formattedDate = this.fyo.format(details.date, 'Datetime');
2023-02-28 06:01:04 +00:00
const batch = details.batch || undefined;
2023-05-04 10:45:12 +00:00
const serialNumbers = getSerialNumbers(details.serialNumber ?? '');
let quantityBefore =
(await this.fyo.db.getStockQuantity(
details.item,
details.fromLocation,
undefined,
date,
batch,
serialNumbers
)) ?? 0;
if (this.isCancelled) {
quantityBefore += details.quantity;
}
2023-02-28 06:01:04 +00:00
const batchMessage = !!batch ? t` in Batch ${batch}` : '';
2023-02-27 13:46:04 +00:00
if (!details.isReturn && quantityBefore < details.quantity) {
2022-11-03 10:53:34 +00:00
throw new ValidationError(
[
t`Insufficient Quantity.`,
2023-01-18 12:07:31 +00:00
t`Additional quantity (${
details.quantity - quantityBefore
2023-02-28 06:01:04 +00:00
}) required${batchMessage} to make outward transfer of item ${
2023-02-27 13:46:04 +00:00
details.item
} from ${details.fromLocation} on ${formattedDate}`,
2022-11-03 10:53:34 +00:00
].join('\n')
);
}
const quantityAfter = await this.fyo.db.getStockQuantity(
details.item,
details.fromLocation,
details.date.toISOString(),
undefined,
2023-04-25 07:07:29 +00:00
batch,
2023-05-04 10:45:12 +00:00
serialNumbers
2022-11-03 10:53:34 +00:00
);
2023-02-27 13:46:04 +00:00
2022-11-03 10:53:34 +00:00
if (quantityAfter === null) {
// No future transactions
return;
}
const quantityRemaining = quantityBefore - details.quantity;
const futureQuantity = quantityRemaining + quantityAfter;
if (futureQuantity < 0) {
2022-11-03 10:53:34 +00:00
throw new ValidationError(
[
t`Insufficient Quantity.`,
t`Transfer will cause future entries to have negative stock.`,
t`Additional quantity (${-futureQuantity}) required${batchMessage} to make outward transfer of item ${
2023-02-27 13:46:04 +00:00
details.item
} from ${details.fromLocation} on ${formattedDate}`,
2022-11-03 10:53:34 +00:00
].join('\n')
);
}
}
2022-10-29 07:07:52 +00:00
}
class StockManagerItem {
2022-10-29 07:07:52 +00:00
/**
* The Stock Manager Item is used to move stock to and from a location. It
2022-10-28 08:04:08 +00:00
* updates the Stock Queue and creates Stock Ledger Entries.
*
* 1. Get existing stock Queue
* 5. Create Stock Ledger Entry
* 7. Insert Stock Ledger Entry
*/
2022-10-29 06:15:23 +00:00
date: Date;
item: string;
rate: Money;
quantity: number;
referenceName: string;
referenceType: string;
2022-10-28 08:04:08 +00:00
fromLocation?: string;
toLocation?: string;
2023-02-28 06:01:04 +00:00
batch?: string;
2023-05-04 10:45:12 +00:00
serialNumber?: string;
2022-10-28 08:04:08 +00:00
2022-10-29 06:15:23 +00:00
stockLedgerEntries?: StockLedgerEntry[];
2022-10-28 08:04:08 +00:00
2022-10-29 06:15:23 +00:00
fyo: Fyo;
2022-10-28 08:04:08 +00:00
2022-10-29 07:07:52 +00:00
constructor(details: SMIDetails, fyo: Fyo) {
2022-10-28 08:04:08 +00:00
this.date = details.date;
this.item = details.item;
2022-10-29 06:15:23 +00:00
this.rate = details.rate;
2022-10-28 08:04:08 +00:00
this.quantity = details.quantity;
this.fromLocation = details.fromLocation;
this.toLocation = details.toLocation;
this.referenceName = details.referenceName;
this.referenceType = details.referenceType;
2023-02-28 06:01:04 +00:00
this.batch = details.batch;
2023-05-04 10:45:12 +00:00
this.serialNumber = details.serialNumber;
2022-10-29 06:15:23 +00:00
this.fyo = fyo;
2022-10-28 08:04:08 +00:00
}
transferStock() {
2022-10-29 06:15:23 +00:00
this.#clear();
this.#moveStockForBothLocations();
2022-10-29 06:15:23 +00:00
}
async sync() {
2022-11-03 10:53:34 +00:00
const sles = [
this.stockLedgerEntries?.filter((s) => s.quantity! <= 0),
this.stockLedgerEntries?.filter((s) => s.quantity! > 0),
]
.flat()
.filter(Boolean);
for (const sle of sles) {
await sle!.sync();
2022-10-29 06:15:23 +00:00
}
}
#moveStockForBothLocations() {
2022-10-28 08:04:08 +00:00
if (this.fromLocation) {
this.#moveStockForSingleLocation(this.fromLocation, true);
2022-10-28 08:04:08 +00:00
}
if (this.toLocation) {
this.#moveStockForSingleLocation(this.toLocation, false);
2022-10-28 08:04:08 +00:00
}
}
#moveStockForSingleLocation(location: string, isOutward: boolean) {
2023-05-04 10:45:12 +00:00
let quantity: number = this.quantity;
2022-10-29 06:15:23 +00:00
if (quantity === 0) {
return;
}
2023-05-04 10:45:12 +00:00
const serialNumbers = getSerialNumbers(this.serialNumber ?? '');
if (serialNumbers.length) {
const snStockLedgerEntries = this.#getSerialNumberedStockLedgerEntries(
location,
isOutward,
serialNumbers
);
this.stockLedgerEntries?.push(...snStockLedgerEntries);
2023-04-25 07:07:29 +00:00
return;
}
2022-10-28 08:04:08 +00:00
if (isOutward) {
quantity = -quantity;
}
const stockLedgerEntry = this.#getStockLedgerEntry(location, quantity);
this.stockLedgerEntries?.push(stockLedgerEntry);
2022-10-28 08:04:08 +00:00
}
2023-05-04 10:45:12 +00:00
#getSerialNumberedStockLedgerEntries(
location: string,
isOutward: boolean,
serialNumbers: string[]
): StockLedgerEntry[] {
let quantity = 1;
if (isOutward) {
quantity = -1;
}
return serialNumbers.map((sn) =>
this.#getStockLedgerEntry(location, quantity, sn)
);
}
#getStockLedgerEntry(
location: string,
quantity: number,
serialNumber?: string
): StockLedgerEntry {
2022-10-28 08:04:08 +00:00
return this.fyo.doc.getNewDoc(ModelNameEnum.StockLedgerEntry, {
date: this.date,
item: this.item,
rate: this.rate,
2023-02-28 06:01:04 +00:00
batch: this.batch || null,
2023-05-04 10:45:12 +00:00
serialNumber: serialNumber || null,
2022-10-28 08:04:08 +00:00
quantity,
location,
referenceName: this.referenceName,
referenceType: this.referenceType,
}) as StockLedgerEntry;
}
2022-10-29 06:15:23 +00:00
#clear() {
this.stockLedgerEntries = [];
}
2022-10-28 08:04:08 +00:00
}