2023-05-04 10:45:12 +00:00
|
|
|
import { Fyo, t } from 'fyo';
|
2023-02-27 13:09:18 +00:00
|
|
|
import { ValidationError } from 'fyo/utils/errors';
|
2023-05-04 10:45:12 +00:00
|
|
|
import type { Invoice } from 'models/baseModels/Invoice/Invoice';
|
|
|
|
import type { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
|
2023-02-27 13:09:18 +00:00
|
|
|
import { ModelNameEnum } from 'models/types';
|
2023-05-04 10:45:12 +00:00
|
|
|
import { SerialNumber } from './SerialNumber';
|
|
|
|
import type { StockMovement } from './StockMovement';
|
|
|
|
import type { StockMovementItem } from './StockMovementItem';
|
|
|
|
import type { StockTransfer } from './StockTransfer';
|
|
|
|
import type { StockTransferItem } from './StockTransferItem';
|
2023-05-05 05:43:04 +00:00
|
|
|
import { Transfer } from './Transfer';
|
|
|
|
import { TransferItem } from './TransferItem';
|
2023-05-04 10:45:12 +00:00
|
|
|
import type { SerialNumberStatus } from './types';
|
2022-10-28 08:04:08 +00:00
|
|
|
|
2023-02-28 06:01:04 +00:00
|
|
|
export async function validateBatch(
|
2023-02-27 13:09:18 +00:00
|
|
|
doc: StockMovement | StockTransfer | Invoice
|
|
|
|
) {
|
|
|
|
for (const row of doc.items ?? []) {
|
2023-02-28 06:01:04 +00:00
|
|
|
await validateItemRowBatch(row);
|
2023-02-27 13:09:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-28 06:01:04 +00:00
|
|
|
async function validateItemRowBatch(
|
2023-02-27 13:09:18 +00:00
|
|
|
doc: StockMovementItem | StockTransferItem | InvoiceItem
|
|
|
|
) {
|
2023-05-04 10:45:12 +00:00
|
|
|
const idx = doc.idx ?? 0;
|
2023-02-27 13:09:18 +00:00
|
|
|
const item = doc.item;
|
2023-02-28 06:01:04 +00:00
|
|
|
const batch = doc.batch;
|
2023-02-27 13:46:04 +00:00
|
|
|
if (!item) {
|
2023-02-27 13:09:18 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-04-25 07:07:29 +00:00
|
|
|
const hasBatch = await doc.fyo.getValue(ModelNameEnum.Item, item, 'hasBatch');
|
2023-02-27 13:09:18 +00:00
|
|
|
|
2023-02-28 06:01:04 +00:00
|
|
|
if (!hasBatch && batch) {
|
2023-02-27 13:46:04 +00:00
|
|
|
throw new ValidationError(
|
|
|
|
[
|
2023-05-04 10:45:12 +00:00
|
|
|
doc.fyo.t`Batch set for row ${idx + 1}.`,
|
2023-02-27 13:46:04 +00:00
|
|
|
doc.fyo.t`Item ${item} is not a batched item`,
|
|
|
|
].join(' ')
|
|
|
|
);
|
2023-02-27 13:09:18 +00:00
|
|
|
}
|
|
|
|
|
2023-02-28 06:01:04 +00:00
|
|
|
if (hasBatch && !batch) {
|
2023-02-27 13:46:04 +00:00
|
|
|
throw new ValidationError(
|
|
|
|
[
|
2023-05-04 10:45:12 +00:00
|
|
|
doc.fyo.t`Batch not set for row ${idx + 1}.`,
|
2023-02-27 13:46:04 +00:00
|
|
|
doc.fyo.t`Item ${item} is a batched item`,
|
|
|
|
].join(' ')
|
|
|
|
);
|
|
|
|
}
|
2023-02-27 13:09:18 +00:00
|
|
|
}
|
2023-04-25 07:07:29 +00:00
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
export async function validateSerialNumber(doc: StockMovement | StockTransfer) {
|
2023-04-25 07:07:29 +00:00
|
|
|
for (const row of doc.items ?? []) {
|
2023-05-04 10:45:12 +00:00
|
|
|
await validateItemRowSerialNumber(row);
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
async function validateItemRowSerialNumber(
|
|
|
|
row: StockMovementItem | StockTransferItem
|
2023-04-25 07:07:29 +00:00
|
|
|
) {
|
2023-05-04 10:45:12 +00:00
|
|
|
const idx = row.idx ?? 0;
|
|
|
|
const item = row.item;
|
2023-04-25 07:07:29 +00:00
|
|
|
|
|
|
|
if (!item) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
const hasSerialNumber = await row.fyo.getValue(
|
2023-04-25 07:07:29 +00:00
|
|
|
ModelNameEnum.Item,
|
|
|
|
item,
|
2023-05-04 10:45:12 +00:00
|
|
|
'hasSerialNumber'
|
2023-04-25 07:07:29 +00:00
|
|
|
);
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
if (hasSerialNumber && !row.serialNumber) {
|
2023-04-25 07:07:29 +00:00
|
|
|
throw new ValidationError(
|
|
|
|
[
|
2023-05-04 10:45:12 +00:00
|
|
|
row.fyo.t`Serial Number not set for row ${idx + 1}.`,
|
|
|
|
row.fyo.t`Serial Number is enabled for Item ${item}`,
|
2023-04-25 07:07:29 +00:00
|
|
|
].join(' ')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
if (!hasSerialNumber && row.serialNumber) {
|
2023-04-25 07:07:29 +00:00
|
|
|
throw new ValidationError(
|
|
|
|
[
|
2023-05-04 10:45:12 +00:00
|
|
|
row.fyo.t`Serial Number set for row ${idx + 1}.`,
|
|
|
|
row.fyo.t`Serial Number is not enabled for Item ${item}`,
|
2023-04-25 07:07:29 +00:00
|
|
|
].join(' ')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
const serialNumber = row.serialNumber;
|
|
|
|
if (!hasSerialNumber || typeof serialNumber !== 'string') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const serialNumbers = getSerialNumbers(serialNumber);
|
2023-04-25 07:07:29 +00:00
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
const quantity = row.quantity ?? 0;
|
|
|
|
if (serialNumbers.length !== quantity) {
|
2023-04-25 07:07:29 +00:00
|
|
|
throw new ValidationError(
|
2023-05-04 10:45:12 +00:00
|
|
|
t`Additional ${
|
|
|
|
quantity - serialNumbers.length
|
|
|
|
} Serial Numbers required for ${quantity} quantity of ${item}.`
|
2023-04-25 07:07:29 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-05 05:07:00 +00:00
|
|
|
const nonExistingIncomingSerialNumbers: string[] = [];
|
|
|
|
for (const serialNumber of serialNumbers) {
|
|
|
|
if (await row.fyo.db.exists(ModelNameEnum.SerialNumber, serialNumber)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isSerialNumberIncoming(row)) {
|
|
|
|
nonExistingIncomingSerialNumbers.push(serialNumber);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new ValidationError(t`Serial Number ${serialNumber} does not exist.`);
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
for (const serialNumber of serialNumbers) {
|
2023-05-05 05:07:00 +00:00
|
|
|
if (nonExistingIncomingSerialNumbers.includes(serialNumber)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
const snDoc = await row.fyo.doc.getDoc(
|
|
|
|
ModelNameEnum.SerialNumber,
|
|
|
|
serialNumber
|
2023-04-25 07:07:29 +00:00
|
|
|
);
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
if (!(snDoc instanceof SerialNumber)) {
|
|
|
|
continue;
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
if (snDoc.item !== item) {
|
|
|
|
throw new ValidationError(
|
|
|
|
t`Serial Number ${serialNumber} does not belong to the item ${item}.`
|
2023-04-25 07:07:29 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
const status = snDoc.status ?? 'Inactive';
|
|
|
|
const schemaName = row.parentSchemaName;
|
|
|
|
|
|
|
|
if (schemaName === 'PurchaseReceipt' && status !== 'Inactive') {
|
|
|
|
throw new ValidationError(
|
|
|
|
t`Serial Number ${serialNumber} is not Inactive`
|
|
|
|
);
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
if (schemaName === 'Shipment' && status !== 'Active') {
|
|
|
|
throw new ValidationError(
|
|
|
|
t`Serial Number ${serialNumber} is not Active.`
|
2023-04-25 07:07:29 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
export function getSerialNumbers(serialNumber: string): string[] {
|
|
|
|
if (!serialNumber) {
|
|
|
|
return [];
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
2023-05-04 10:45:12 +00:00
|
|
|
|
2023-05-05 06:25:09 +00:00
|
|
|
return serialNumber
|
|
|
|
.split('\n')
|
|
|
|
.map((s) => s.trim())
|
|
|
|
.filter(Boolean);
|
2023-05-04 10:45:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function getSerialNumberFromDoc(doc: StockTransfer | StockMovement) {
|
|
|
|
if (!doc.items?.length) {
|
|
|
|
return [];
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
return doc.items
|
2023-05-05 05:43:04 +00:00
|
|
|
.map((item) =>
|
|
|
|
getSerialNumbers(item.serialNumber ?? '').map((serialNumber) => ({
|
|
|
|
serialNumber,
|
|
|
|
item,
|
|
|
|
}))
|
|
|
|
)
|
2023-05-04 10:45:12 +00:00
|
|
|
.flat()
|
|
|
|
.filter(Boolean);
|
|
|
|
}
|
|
|
|
|
2023-05-05 05:43:04 +00:00
|
|
|
export async function createSerialNumbers(doc: Transfer) {
|
2023-05-05 05:07:00 +00:00
|
|
|
const items = doc.items ?? [];
|
|
|
|
const serialNumberCreateList = items
|
|
|
|
.map((item) => {
|
|
|
|
const serialNumbers = getSerialNumbers(item.serialNumber ?? '');
|
|
|
|
return serialNumbers.map((serialNumber) => ({
|
2023-05-05 06:25:09 +00:00
|
|
|
item: item.item ?? '',
|
2023-05-05 05:07:00 +00:00
|
|
|
serialNumber,
|
|
|
|
isIncoming: isSerialNumberIncoming(item),
|
|
|
|
}));
|
|
|
|
})
|
|
|
|
.flat()
|
|
|
|
.filter(({ item, isIncoming }) => isIncoming && item);
|
|
|
|
|
|
|
|
for (const { item, serialNumber } of serialNumberCreateList) {
|
|
|
|
if (await doc.fyo.db.exists(ModelNameEnum.SerialNumber, serialNumber)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const snDoc = doc.fyo.doc.getNewDoc(ModelNameEnum.SerialNumber, {
|
|
|
|
name: serialNumber,
|
|
|
|
item,
|
|
|
|
});
|
|
|
|
|
|
|
|
const status: SerialNumberStatus = 'Active';
|
|
|
|
await snDoc.set('status', status);
|
|
|
|
await snDoc.sync();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-05 05:43:04 +00:00
|
|
|
function isSerialNumberIncoming(item: TransferItem) {
|
2023-05-05 05:07:00 +00:00
|
|
|
if (item.parentdoc?.schemaName === ModelNameEnum.Shipment) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item.parentdoc?.schemaName === ModelNameEnum.PurchaseReceipt) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return !!item.toLocation && !item.fromLocation;
|
|
|
|
}
|
|
|
|
|
2023-05-05 05:43:04 +00:00
|
|
|
export async function canValidateSerialNumber(
|
|
|
|
item: StockTransferItem | StockMovementItem,
|
|
|
|
serialNumber: string
|
|
|
|
) {
|
|
|
|
if (!isSerialNumberIncoming(item)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return await item.fyo.db.exists(ModelNameEnum.SerialNumber, serialNumber);
|
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
export async function updateSerialNumbers(
|
|
|
|
doc: StockTransfer | StockMovement,
|
|
|
|
isCancel: boolean
|
|
|
|
) {
|
|
|
|
for (const row of doc.items ?? []) {
|
|
|
|
if (!row.serialNumber) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-05-05 05:07:00 +00:00
|
|
|
const status = getSerialNumberStatus(doc, row, isCancel);
|
2023-05-04 10:45:12 +00:00
|
|
|
await updateSerialNumberStatus(status, row.serialNumber, doc.fyo);
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
2023-05-04 10:45:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function updateSerialNumberStatus(
|
|
|
|
status: SerialNumberStatus,
|
|
|
|
serialNumber: string,
|
|
|
|
fyo: Fyo
|
|
|
|
) {
|
|
|
|
for (const name of getSerialNumbers(serialNumber)) {
|
2023-05-05 06:25:09 +00:00
|
|
|
const doc = await fyo.doc.getDoc(ModelNameEnum.SerialNumber, name);
|
|
|
|
await doc.setAndSync('status', status);
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
2023-05-04 10:45:12 +00:00
|
|
|
}
|
2023-04-25 07:07:29 +00:00
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
function getSerialNumberStatus(
|
|
|
|
doc: StockTransfer | StockMovement,
|
2023-05-05 05:07:00 +00:00
|
|
|
item: StockTransferItem | StockMovementItem,
|
|
|
|
isCancel: boolean
|
2023-05-04 10:45:12 +00:00
|
|
|
): SerialNumberStatus {
|
|
|
|
if (doc.schemaName === ModelNameEnum.Shipment) {
|
|
|
|
return isCancel ? 'Active' : 'Delivered';
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
if (doc.schemaName === ModelNameEnum.PurchaseReceipt) {
|
|
|
|
return isCancel ? 'Inactive' : 'Active';
|
|
|
|
}
|
2023-04-25 07:07:29 +00:00
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
return getSerialNumberStatusForStockMovement(
|
|
|
|
doc as StockMovement,
|
2023-05-05 05:07:00 +00:00
|
|
|
item,
|
|
|
|
isCancel
|
2023-05-04 10:45:12 +00:00
|
|
|
);
|
|
|
|
}
|
2023-04-25 07:07:29 +00:00
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
function getSerialNumberStatusForStockMovement(
|
|
|
|
doc: StockMovement,
|
2023-05-05 05:07:00 +00:00
|
|
|
item: StockTransferItem | StockMovementItem,
|
|
|
|
isCancel: boolean
|
2023-05-04 10:45:12 +00:00
|
|
|
): SerialNumberStatus {
|
|
|
|
if (doc.movementType === 'MaterialIssue') {
|
2023-05-05 06:25:09 +00:00
|
|
|
return isCancel ? 'Active' : 'Delivered';
|
2023-04-25 07:07:29 +00:00
|
|
|
}
|
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
if (doc.movementType === 'MaterialReceipt') {
|
|
|
|
return isCancel ? 'Inactive' : 'Active';
|
|
|
|
}
|
2023-04-25 07:07:29 +00:00
|
|
|
|
2023-05-04 10:45:12 +00:00
|
|
|
if (doc.movementType === 'MaterialTransfer') {
|
|
|
|
return 'Active';
|
|
|
|
}
|
|
|
|
|
|
|
|
// MovementType is Manufacture
|
2023-05-05 05:07:00 +00:00
|
|
|
if (item.fromLocation) {
|
2023-05-05 06:25:09 +00:00
|
|
|
return isCancel ? 'Active' : 'Delivered';
|
2023-05-04 10:45:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return isCancel ? 'Inactive' : 'Active';
|
|
|
|
}
|