2023-05-04 10:45:12 +00:00
|
|
|
import { Fyo, t } from 'fyo';
|
|
|
|
import type { Doc } from 'fyo/model/doc';
|
2022-10-06 08:50:13 +00:00
|
|
|
import {
|
2022-11-21 06:35:05 +00:00
|
|
|
Action,
|
2022-10-06 08:50:13 +00:00
|
|
|
DefaultMap,
|
|
|
|
FiltersMap,
|
|
|
|
FormulaMap,
|
2022-11-30 14:46:20 +00:00
|
|
|
ListViewSettings,
|
2022-10-06 08:50:13 +00:00
|
|
|
} from 'fyo/model/types';
|
2023-01-31 08:06:03 +00:00
|
|
|
import { ValidationError } from 'fyo/utils/errors';
|
2023-05-04 10:45:12 +00:00
|
|
|
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
2023-01-31 08:06:03 +00:00
|
|
|
import {
|
|
|
|
addItem,
|
|
|
|
getDocStatusListColumn,
|
|
|
|
getLedgerLinkAction,
|
|
|
|
} from 'models/helpers';
|
2022-10-05 14:37:17 +00:00
|
|
|
import { ModelNameEnum } from 'models/types';
|
|
|
|
import { Money } from 'pesa';
|
2023-05-04 10:45:12 +00:00
|
|
|
import { SerialNumber } from './SerialNumber';
|
|
|
|
import { StockMovementItem } from './StockMovementItem';
|
|
|
|
import { Transfer } from './Transfer';
|
2023-04-25 07:07:29 +00:00
|
|
|
import {
|
2023-05-04 10:45:12 +00:00
|
|
|
getSerialNumberFromDoc,
|
|
|
|
updateSerialNumbers,
|
2023-04-25 07:07:29 +00:00
|
|
|
validateBatch,
|
2023-05-04 10:45:12 +00:00
|
|
|
validateSerialNumber,
|
2023-04-25 07:07:29 +00:00
|
|
|
} from './helpers';
|
2023-05-04 10:45:12 +00:00
|
|
|
import { MovementType, MovementTypeEnum, SerialNumberStatus } from './types';
|
2022-10-05 14:37:17 +00:00
|
|
|
|
2022-11-18 17:31:50 +00:00
|
|
|
export class StockMovement extends Transfer {
|
2022-10-05 14:37:17 +00:00
|
|
|
name?: string;
|
|
|
|
date?: Date;
|
|
|
|
numberSeries?: string;
|
|
|
|
movementType?: MovementType;
|
2022-10-06 08:50:13 +00:00
|
|
|
items?: StockMovementItem[];
|
2022-10-05 14:37:17 +00:00
|
|
|
amount?: Money;
|
|
|
|
|
2022-11-18 17:31:50 +00:00
|
|
|
override get isTransactional(): boolean {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
override async getPosting(): Promise<LedgerPosting | null> {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-10-06 08:50:13 +00:00
|
|
|
formulas: FormulaMap = {
|
|
|
|
amount: {
|
|
|
|
formula: () => {
|
|
|
|
return this.items?.reduce(
|
|
|
|
(acc, item) => acc.add(item.amount ?? 0),
|
|
|
|
this.fyo.pesa(0)
|
|
|
|
);
|
|
|
|
},
|
|
|
|
dependsOn: ['items'],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2023-01-31 08:06:03 +00:00
|
|
|
async validate() {
|
|
|
|
await super.validate();
|
2023-02-27 13:09:18 +00:00
|
|
|
this.validateManufacture();
|
2023-02-28 06:01:04 +00:00
|
|
|
await validateBatch(this);
|
2023-05-04 10:45:12 +00:00
|
|
|
await validateSerialNumber(this);
|
|
|
|
await validateSerialNumberStatus(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
async afterSubmit(): Promise<void> {
|
|
|
|
await super.afterSubmit();
|
|
|
|
await updateSerialNumbers(this, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
async afterCancel(): Promise<void> {
|
|
|
|
await super.afterCancel();
|
|
|
|
await updateSerialNumbers(this, true);
|
2023-02-27 13:09:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
validateManufacture() {
|
2023-05-04 10:45:12 +00:00
|
|
|
if (this.movementType !== MovementTypeEnum.Manufacture) {
|
2023-01-31 08:06:03 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasFrom = this.items?.findIndex((f) => f.fromLocation) !== -1;
|
|
|
|
const hasTo = this.items?.findIndex((f) => f.toLocation) !== -1;
|
|
|
|
|
|
|
|
if (!hasFrom) {
|
|
|
|
throw new ValidationError(this.fyo.t`Item with From location not found`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!hasTo) {
|
|
|
|
throw new ValidationError(this.fyo.t`Item with To location not found`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-05 14:37:17 +00:00
|
|
|
static filters: FiltersMap = {
|
|
|
|
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
|
|
|
|
};
|
|
|
|
|
|
|
|
static defaults: DefaultMap = {
|
|
|
|
date: () => new Date(),
|
|
|
|
};
|
|
|
|
|
2022-11-30 14:46:20 +00:00
|
|
|
static getListViewSettings(fyo: Fyo): ListViewSettings {
|
2023-03-07 07:09:49 +00:00
|
|
|
const movementTypeMap = {
|
2023-05-04 10:45:12 +00:00
|
|
|
[MovementTypeEnum.MaterialIssue]: fyo.t`Material Issue`,
|
|
|
|
[MovementTypeEnum.MaterialReceipt]: fyo.t`Material Receipt`,
|
|
|
|
[MovementTypeEnum.MaterialTransfer]: fyo.t`Material Transfer`,
|
|
|
|
[MovementTypeEnum.Manufacture]: fyo.t`Manufacture`,
|
2023-03-07 07:09:49 +00:00
|
|
|
};
|
|
|
|
|
2022-11-16 08:35:38 +00:00
|
|
|
return {
|
2022-11-30 14:46:20 +00:00
|
|
|
columns: [
|
|
|
|
'name',
|
|
|
|
getDocStatusListColumn(),
|
|
|
|
'date',
|
|
|
|
{
|
|
|
|
label: fyo.t`Movement Type`,
|
|
|
|
fieldname: 'movementType',
|
|
|
|
fieldtype: 'Select',
|
2023-03-07 07:09:49 +00:00
|
|
|
display(value): string {
|
2023-05-04 10:45:12 +00:00
|
|
|
return movementTypeMap[value as MovementTypeEnum] ?? '';
|
2022-11-30 14:46:20 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2022-11-16 08:35:38 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-11-02 14:56:37 +00:00
|
|
|
_getTransferDetails() {
|
|
|
|
return (this.items ?? []).map((row) => ({
|
|
|
|
item: row.item!,
|
|
|
|
rate: row.rate!,
|
|
|
|
quantity: row.quantity!,
|
2023-02-28 06:01:04 +00:00
|
|
|
batch: row.batch!,
|
2023-05-04 10:45:12 +00:00
|
|
|
serialNumber: row.serialNumber!,
|
2022-11-02 14:56:37 +00:00
|
|
|
fromLocation: row.fromLocation,
|
|
|
|
toLocation: row.toLocation,
|
|
|
|
}));
|
2022-10-29 06:15:23 +00:00
|
|
|
}
|
2022-11-21 06:35:05 +00:00
|
|
|
|
|
|
|
static getActions(fyo: Fyo): Action[] {
|
|
|
|
return [getLedgerLinkAction(fyo, true)];
|
|
|
|
}
|
2023-01-16 09:38:02 +00:00
|
|
|
|
|
|
|
async addItem(name: string) {
|
|
|
|
return await addItem(name, this);
|
|
|
|
}
|
2022-10-05 14:37:17 +00:00
|
|
|
}
|
2023-05-04 10:45:12 +00:00
|
|
|
|
|
|
|
async function validateSerialNumberStatus(doc: StockMovement) {
|
|
|
|
for (const serialNumber of getSerialNumberFromDoc(doc)) {
|
|
|
|
const snDoc = await doc.fyo.doc.getDoc(
|
|
|
|
ModelNameEnum.SerialNumber,
|
|
|
|
serialNumber
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!(snDoc instanceof SerialNumber)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const status = snDoc.status ?? 'Inactive';
|
|
|
|
|
|
|
|
if (doc.movementType === 'MaterialReceipt' && status !== 'Inactive') {
|
|
|
|
throw new ValidationError(
|
|
|
|
t`Active Serial Number ${serialNumber} cannot be used for Material Issue`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (doc.movementType === 'MaterialIssue' && status !=='Active') {
|
|
|
|
validateMaterialIssueSerialNumber(serialNumber, status);
|
|
|
|
throw new ValidationError(
|
|
|
|
t`Inactive Serial Number ${serialNumber} cannot be used for Material Issue`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function validateMaterialReceiptSerialNumber(
|
|
|
|
serialNumber: string,
|
|
|
|
status: string
|
|
|
|
) {
|
|
|
|
if (status === 'Inactive') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function validateMaterialIssueSerialNumber(
|
|
|
|
serialNumber: string,
|
|
|
|
status: SerialNumberStatus
|
|
|
|
) {
|
|
|
|
if (status === 'Active') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new ValidationError(t`Serial Number ${serialNumber} is not Active.`);
|
|
|
|
}
|