mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
commit
e5a854c9d5
@ -138,7 +138,8 @@ export class BespokeQueries {
|
|||||||
location?: string,
|
location?: string,
|
||||||
fromDate?: string,
|
fromDate?: string,
|
||||||
toDate?: string,
|
toDate?: string,
|
||||||
batch?: string
|
batch?: string,
|
||||||
|
serialNo?: string
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
const query = db.knex!(ModelNameEnum.StockLedgerEntry)
|
const query = db.knex!(ModelNameEnum.StockLedgerEntry)
|
||||||
.sum('quantity')
|
.sum('quantity')
|
||||||
@ -152,6 +153,10 @@ export class BespokeQueries {
|
|||||||
query.andWhere('batch', batch);
|
query.andWhere('batch', batch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serialNo) {
|
||||||
|
query.andWhere('serialNo', serialNo);
|
||||||
|
}
|
||||||
|
|
||||||
if (fromDate) {
|
if (fromDate) {
|
||||||
query.andWhereRaw('datetime(date) > datetime(?)', [fromDate]);
|
query.andWhereRaw('datetime(date) > datetime(?)', [fromDate]);
|
||||||
}
|
}
|
||||||
|
@ -313,7 +313,8 @@ export class DatabaseHandler extends DatabaseBase {
|
|||||||
location?: string,
|
location?: string,
|
||||||
fromDate?: string,
|
fromDate?: string,
|
||||||
toDate?: string,
|
toDate?: string,
|
||||||
batch?: string
|
batch?: string,
|
||||||
|
serialNo?: string
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
return (await this.#demux.callBespoke(
|
return (await this.#demux.callBespoke(
|
||||||
'getStockQuantity',
|
'getStockQuantity',
|
||||||
@ -321,7 +322,8 @@ export class DatabaseHandler extends DatabaseBase {
|
|||||||
location,
|
location,
|
||||||
fromDate,
|
fromDate,
|
||||||
toDate,
|
toDate,
|
||||||
batch
|
batch,
|
||||||
|
serialNo
|
||||||
)) as number | null;
|
)) as number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ export class Item extends Doc {
|
|||||||
itemType?: 'Product' | 'Service';
|
itemType?: 'Product' | 'Service';
|
||||||
for?: 'Purchases' | 'Sales' | 'Both';
|
for?: 'Purchases' | 'Sales' | 'Both';
|
||||||
hasBatch?: boolean;
|
hasBatch?: boolean;
|
||||||
|
hasSerialNo?: boolean;
|
||||||
|
|
||||||
formulas: FormulaMap = {
|
formulas: FormulaMap = {
|
||||||
incomeAccount: {
|
incomeAccount: {
|
||||||
@ -124,6 +125,8 @@ export class Item extends Doc {
|
|||||||
barcode: () => !this.fyo.singles.InventorySettings?.enableBarcodes,
|
barcode: () => !this.fyo.singles.InventorySettings?.enableBarcodes,
|
||||||
hasBatch: () =>
|
hasBatch: () =>
|
||||||
!(this.fyo.singles.InventorySettings?.enableBatches && this.trackItem),
|
!(this.fyo.singles.InventorySettings?.enableBatches && this.trackItem),
|
||||||
|
hasSerialNo: () =>
|
||||||
|
!(this.fyo.singles.InventorySettings?.enableSerialNo && this.trackItem),
|
||||||
uomConversions: () =>
|
uomConversions: () =>
|
||||||
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
||||||
};
|
};
|
||||||
@ -133,5 +136,6 @@ export class Item extends Doc {
|
|||||||
itemType: () => this.inserted,
|
itemType: () => this.inserted,
|
||||||
trackItem: () => this.inserted,
|
trackItem: () => this.inserted,
|
||||||
hasBatch: () => this.inserted,
|
hasBatch: () => this.inserted,
|
||||||
|
hasSerialNo: () => this.inserted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
import { Invoice } from './baseModels/Invoice/Invoice';
|
import { Invoice } from './baseModels/Invoice/Invoice';
|
||||||
import { StockMovement } from './inventory/StockMovement';
|
import { StockMovement } from './inventory/StockMovement';
|
||||||
import { StockTransfer } from './inventory/StockTransfer';
|
import { StockTransfer } from './inventory/StockTransfer';
|
||||||
import { InvoiceStatus, ModelNameEnum } from './types';
|
import { InvoiceStatus, SerialNoStatus, ModelNameEnum } from './types';
|
||||||
|
|
||||||
export function getInvoiceActions(
|
export function getInvoiceActions(
|
||||||
fyo: Fyo,
|
fyo: Fyo,
|
||||||
@ -248,6 +248,45 @@ export function getInvoiceStatus(doc: RenderData | Doc): InvoiceStatus {
|
|||||||
return 'Saved';
|
return 'Saved';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSerialNoStatusColumn(): ColumnConfig {
|
||||||
|
return {
|
||||||
|
label: t`Status`,
|
||||||
|
fieldname: 'status',
|
||||||
|
fieldtype: 'Select',
|
||||||
|
render(doc) {
|
||||||
|
const status = doc.status as SerialNoStatus;
|
||||||
|
const color = serialNoStatusColor[status];
|
||||||
|
const label = getSerialNoStatusText(status as string);
|
||||||
|
|
||||||
|
return {
|
||||||
|
template: `<Badge class="text-xs" color="${color}">${label}</Badge>`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serialNoStatusColor: Record<SerialNoStatus, string | undefined> = {
|
||||||
|
Inactive: 'gray',
|
||||||
|
Active: 'green',
|
||||||
|
Delivered: 'green',
|
||||||
|
Expired: 'red',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getSerialNoStatusText(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'Inactive':
|
||||||
|
return t`Inactive`;
|
||||||
|
case 'Active':
|
||||||
|
return t`Active`;
|
||||||
|
case 'Delivered':
|
||||||
|
return t`Delivered`;
|
||||||
|
case 'Expired':
|
||||||
|
return t`Expired`;
|
||||||
|
default:
|
||||||
|
return t`Inactive`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getExchangeRate({
|
export async function getExchangeRate({
|
||||||
fromCurrency,
|
fromCurrency,
|
||||||
toCurrency,
|
toCurrency,
|
||||||
|
@ -19,6 +19,7 @@ import { SetupWizard } from './baseModels/SetupWizard/SetupWizard';
|
|||||||
import { Tax } from './baseModels/Tax/Tax';
|
import { Tax } from './baseModels/Tax/Tax';
|
||||||
import { TaxSummary } from './baseModels/TaxSummary/TaxSummary';
|
import { TaxSummary } from './baseModels/TaxSummary/TaxSummary';
|
||||||
import { Batch } from './inventory/Batch';
|
import { Batch } from './inventory/Batch';
|
||||||
|
import { SerialNo } from './inventory/SerialNo';
|
||||||
import { InventorySettings } from './inventory/InventorySettings';
|
import { InventorySettings } from './inventory/InventorySettings';
|
||||||
import { Location } from './inventory/Location';
|
import { Location } from './inventory/Location';
|
||||||
import { PurchaseReceipt } from './inventory/PurchaseReceipt';
|
import { PurchaseReceipt } from './inventory/PurchaseReceipt';
|
||||||
@ -48,6 +49,7 @@ export const models = {
|
|||||||
PurchaseInvoiceItem,
|
PurchaseInvoiceItem,
|
||||||
SalesInvoice,
|
SalesInvoice,
|
||||||
SalesInvoiceItem,
|
SalesInvoiceItem,
|
||||||
|
SerialNo,
|
||||||
SetupWizard,
|
SetupWizard,
|
||||||
PrintTemplate,
|
PrintTemplate,
|
||||||
Tax,
|
Tax,
|
||||||
|
@ -11,6 +11,7 @@ export class InventorySettings extends Doc {
|
|||||||
costOfGoodsSold?: string;
|
costOfGoodsSold?: string;
|
||||||
enableBarcodes?: boolean;
|
enableBarcodes?: boolean;
|
||||||
enableBatches?: boolean;
|
enableBatches?: boolean;
|
||||||
|
enableSerialNo?: boolean;
|
||||||
enableUomConversions?: boolean;
|
enableUomConversions?: boolean;
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
@ -35,6 +36,9 @@ export class InventorySettings extends Doc {
|
|||||||
enableBatches: () => {
|
enableBatches: () => {
|
||||||
return !!this.enableBatches;
|
return !!this.enableBatches;
|
||||||
},
|
},
|
||||||
|
enableSerialNo: () => {
|
||||||
|
return !!this.enableSerialNo;
|
||||||
|
},
|
||||||
enableUomConversions: () => {
|
enableUomConversions: () => {
|
||||||
return !!this.enableUomConversions;
|
return !!this.enableUomConversions;
|
||||||
},
|
},
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import { ListViewSettings } from 'fyo/model/types';
|
import { ListViewSettings } from 'fyo/model/types';
|
||||||
import { getTransactionStatusColumn } from 'models/helpers';
|
import { getTransactionStatusColumn } from 'models/helpers';
|
||||||
|
import { updateSerialNoStatus } from './helpers';
|
||||||
import { PurchaseReceiptItem } from './PurchaseReceiptItem';
|
import { PurchaseReceiptItem } from './PurchaseReceiptItem';
|
||||||
import { StockTransfer } from './StockTransfer';
|
import { StockTransfer } from './StockTransfer';
|
||||||
|
|
||||||
export class PurchaseReceipt extends StockTransfer {
|
export class PurchaseReceipt extends StockTransfer {
|
||||||
items?: PurchaseReceiptItem[];
|
items?: PurchaseReceiptItem[];
|
||||||
|
|
||||||
|
async afterSubmit(): Promise<void> {
|
||||||
|
await super.afterSubmit();
|
||||||
|
await updateSerialNoStatus(this, this.items!, 'Active');
|
||||||
|
}
|
||||||
|
|
||||||
static getListViewSettings(): ListViewSettings {
|
static getListViewSettings(): ListViewSettings {
|
||||||
return {
|
return {
|
||||||
columns: [
|
columns: [
|
||||||
|
17
models/inventory/SerialNo.ts
Normal file
17
models/inventory/SerialNo.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Doc } from 'fyo/model/doc';
|
||||||
|
import { ListViewSettings } from 'fyo/model/types';
|
||||||
|
import { getSerialNoStatusColumn } from 'models/helpers';
|
||||||
|
|
||||||
|
export class SerialNo extends Doc {
|
||||||
|
static getListViewSettings(): ListViewSettings {
|
||||||
|
return {
|
||||||
|
columns: [
|
||||||
|
'name',
|
||||||
|
getSerialNoStatusColumn(),
|
||||||
|
'item',
|
||||||
|
'description',
|
||||||
|
'party',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,22 @@
|
|||||||
import { ListViewSettings } from 'fyo/model/types';
|
import { ListViewSettings } from 'fyo/model/types';
|
||||||
import { getTransactionStatusColumn } from 'models/helpers';
|
import { getTransactionStatusColumn } from 'models/helpers';
|
||||||
|
import { updateSerialNoStatus } from './helpers';
|
||||||
import { ShipmentItem } from './ShipmentItem';
|
import { ShipmentItem } from './ShipmentItem';
|
||||||
import { StockTransfer } from './StockTransfer';
|
import { StockTransfer } from './StockTransfer';
|
||||||
|
|
||||||
export class Shipment extends StockTransfer {
|
export class Shipment extends StockTransfer {
|
||||||
items?: ShipmentItem[];
|
items?: ShipmentItem[];
|
||||||
|
|
||||||
|
async afterSubmit(): Promise<void> {
|
||||||
|
await super.afterSubmit();
|
||||||
|
await updateSerialNoStatus(this, this.items!, 'Delivered');
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterCancel(): Promise<void> {
|
||||||
|
await super.afterCancel();
|
||||||
|
await updateSerialNoStatus(this, this.items!, 'Active');
|
||||||
|
}
|
||||||
|
|
||||||
static getListViewSettings(): ListViewSettings {
|
static getListViewSettings(): ListViewSettings {
|
||||||
return {
|
return {
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -11,6 +11,7 @@ export class StockLedgerEntry extends Doc {
|
|||||||
referenceName?: string;
|
referenceName?: string;
|
||||||
referenceType?: string;
|
referenceType?: string;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
|
serialNo?: string;
|
||||||
|
|
||||||
static override getListViewSettings(): ListViewSettings {
|
static override getListViewSettings(): ListViewSettings {
|
||||||
return {
|
return {
|
||||||
|
@ -4,6 +4,7 @@ import { ModelNameEnum } from 'models/types';
|
|||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { StockLedgerEntry } from './StockLedgerEntry';
|
import { StockLedgerEntry } from './StockLedgerEntry';
|
||||||
import { SMDetails, SMIDetails, SMTransferDetails } from './types';
|
import { SMDetails, SMIDetails, SMTransferDetails } from './types';
|
||||||
|
import { getSerialNumbers } from './helpers';
|
||||||
|
|
||||||
export class StockManager {
|
export class StockManager {
|
||||||
/**
|
/**
|
||||||
@ -133,15 +134,33 @@ export class StockManager {
|
|||||||
const date = details.date.toISOString();
|
const date = details.date.toISOString();
|
||||||
const formattedDate = this.fyo.format(details.date, 'Datetime');
|
const formattedDate = this.fyo.format(details.date, 'Datetime');
|
||||||
const batch = details.batch || undefined;
|
const batch = details.batch || undefined;
|
||||||
|
const serialNo = details.serialNo || undefined;
|
||||||
|
let quantityBefore = 0;
|
||||||
|
|
||||||
let quantityBefore =
|
if (serialNo) {
|
||||||
(await this.fyo.db.getStockQuantity(
|
const serialNos = getSerialNumbers(serialNo);
|
||||||
details.item,
|
for (const serialNo of serialNos) {
|
||||||
details.fromLocation,
|
quantityBefore +=
|
||||||
undefined,
|
(await this.fyo.db.getStockQuantity(
|
||||||
date,
|
details.item,
|
||||||
batch
|
details.fromLocation,
|
||||||
)) ?? 0;
|
undefined,
|
||||||
|
date,
|
||||||
|
batch,
|
||||||
|
serialNo
|
||||||
|
)) ?? 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quantityBefore =
|
||||||
|
(await this.fyo.db.getStockQuantity(
|
||||||
|
details.item,
|
||||||
|
details.fromLocation,
|
||||||
|
undefined,
|
||||||
|
date,
|
||||||
|
batch,
|
||||||
|
serialNo
|
||||||
|
)) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isCancelled) {
|
if (this.isCancelled) {
|
||||||
quantityBefore += details.quantity;
|
quantityBefore += details.quantity;
|
||||||
@ -167,7 +186,8 @@ export class StockManager {
|
|||||||
details.fromLocation,
|
details.fromLocation,
|
||||||
details.date.toISOString(),
|
details.date.toISOString(),
|
||||||
undefined,
|
undefined,
|
||||||
batch
|
batch,
|
||||||
|
serialNo
|
||||||
);
|
);
|
||||||
|
|
||||||
if (quantityAfter === null) {
|
if (quantityAfter === null) {
|
||||||
@ -211,6 +231,7 @@ class StockManagerItem {
|
|||||||
fromLocation?: string;
|
fromLocation?: string;
|
||||||
toLocation?: string;
|
toLocation?: string;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
|
serialNo?: string;
|
||||||
|
|
||||||
stockLedgerEntries?: StockLedgerEntry[];
|
stockLedgerEntries?: StockLedgerEntry[];
|
||||||
|
|
||||||
@ -226,6 +247,7 @@ class StockManagerItem {
|
|||||||
this.referenceName = details.referenceName;
|
this.referenceName = details.referenceName;
|
||||||
this.referenceType = details.referenceType;
|
this.referenceType = details.referenceType;
|
||||||
this.batch = details.batch;
|
this.batch = details.batch;
|
||||||
|
this.serialNo = details.serialNo;
|
||||||
|
|
||||||
this.fyo = fyo;
|
this.fyo = fyo;
|
||||||
}
|
}
|
||||||
@ -260,10 +282,30 @@ class StockManagerItem {
|
|||||||
|
|
||||||
#moveStockForSingleLocation(location: string, isOutward: boolean) {
|
#moveStockForSingleLocation(location: string, isOutward: boolean) {
|
||||||
let quantity = this.quantity!;
|
let quantity = this.quantity!;
|
||||||
|
const serialNo = this.serialNo;
|
||||||
if (quantity === 0) {
|
if (quantity === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serialNo) {
|
||||||
|
const serialNos = getSerialNumbers(serialNo!);
|
||||||
|
if (isOutward) {
|
||||||
|
quantity = -1;
|
||||||
|
} else {
|
||||||
|
quantity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const serialNo of serialNos) {
|
||||||
|
const stockLedgerEntry = this.#getStockLedgerEntry(
|
||||||
|
location,
|
||||||
|
quantity,
|
||||||
|
serialNo!
|
||||||
|
);
|
||||||
|
this.stockLedgerEntries?.push(stockLedgerEntry);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isOutward) {
|
if (isOutward) {
|
||||||
quantity = -quantity;
|
quantity = -quantity;
|
||||||
}
|
}
|
||||||
@ -273,12 +315,13 @@ class StockManagerItem {
|
|||||||
this.stockLedgerEntries?.push(stockLedgerEntry);
|
this.stockLedgerEntries?.push(stockLedgerEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
#getStockLedgerEntry(location: string, quantity: number) {
|
#getStockLedgerEntry(location: string, quantity: number, serialNo?: string) {
|
||||||
return this.fyo.doc.getNewDoc(ModelNameEnum.StockLedgerEntry, {
|
return this.fyo.doc.getNewDoc(ModelNameEnum.StockLedgerEntry, {
|
||||||
date: this.date,
|
date: this.date,
|
||||||
item: this.item,
|
item: this.item,
|
||||||
rate: this.rate,
|
rate: this.rate,
|
||||||
batch: this.batch || null,
|
batch: this.batch || null,
|
||||||
|
serialNo: serialNo || null,
|
||||||
quantity,
|
quantity,
|
||||||
location,
|
location,
|
||||||
referenceName: this.referenceName,
|
referenceName: this.referenceName,
|
||||||
|
@ -15,7 +15,12 @@ import {
|
|||||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { validateBatch } from './helpers';
|
import {
|
||||||
|
getSerialNumbers,
|
||||||
|
updateSerialNoStatus,
|
||||||
|
validateBatch,
|
||||||
|
validateSerialNo,
|
||||||
|
} from './helpers';
|
||||||
import { StockMovementItem } from './StockMovementItem';
|
import { StockMovementItem } from './StockMovementItem';
|
||||||
import { Transfer } from './Transfer';
|
import { Transfer } from './Transfer';
|
||||||
import { MovementType } from './types';
|
import { MovementType } from './types';
|
||||||
@ -52,6 +57,7 @@ export class StockMovement extends Transfer {
|
|||||||
await super.validate();
|
await super.validate();
|
||||||
this.validateManufacture();
|
this.validateManufacture();
|
||||||
await validateBatch(this);
|
await validateBatch(this);
|
||||||
|
await validateSerialNo(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateManufacture() {
|
validateManufacture() {
|
||||||
@ -71,6 +77,16 @@ export class StockMovement extends Transfer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async afterSubmit(): Promise<void> {
|
||||||
|
await super.afterSubmit();
|
||||||
|
await updateSerialNoStatus(this, this.items!, 'Active');
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterCancel(): Promise<void> {
|
||||||
|
await super.afterCancel();
|
||||||
|
await updateSerialNoStatus(this, this.items!, 'Inactive');
|
||||||
|
}
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
|
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
|
||||||
};
|
};
|
||||||
@ -110,6 +126,7 @@ export class StockMovement extends Transfer {
|
|||||||
rate: row.rate!,
|
rate: row.rate!,
|
||||||
quantity: row.quantity!,
|
quantity: row.quantity!,
|
||||||
batch: row.batch!,
|
batch: row.batch!,
|
||||||
|
serialNo: row.serialNo!,
|
||||||
fromLocation: row.fromLocation,
|
fromLocation: row.fromLocation,
|
||||||
toLocation: row.toLocation,
|
toLocation: row.toLocation,
|
||||||
}));
|
}));
|
||||||
|
@ -32,6 +32,7 @@ export class StockMovementItem extends Doc {
|
|||||||
amount?: Money;
|
amount?: Money;
|
||||||
parentdoc?: StockMovement;
|
parentdoc?: StockMovement;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
|
serialNo?: string;
|
||||||
|
|
||||||
get isIssue() {
|
get isIssue() {
|
||||||
return this.parentdoc?.movementType === MovementType.MaterialIssue;
|
return this.parentdoc?.movementType === MovementType.MaterialIssue;
|
||||||
@ -236,6 +237,7 @@ export class StockMovementItem extends Doc {
|
|||||||
|
|
||||||
override hidden: HiddenMap = {
|
override hidden: HiddenMap = {
|
||||||
batch: () => !this.fyo.singles.InventorySettings?.enableBatches,
|
batch: () => !this.fyo.singles.InventorySettings?.enableBatches,
|
||||||
|
serialNo: () => !this.fyo.singles.InventorySettings?.enableSerialNo,
|
||||||
transferUnit: () =>
|
transferUnit: () =>
|
||||||
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
||||||
transferQuantity: () =>
|
transferQuantity: () =>
|
||||||
|
@ -17,7 +17,7 @@ import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
|||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { TargetField } from 'schemas/types';
|
import { TargetField } from 'schemas/types';
|
||||||
import { validateBatch } from './helpers';
|
import { validateBatch, validateSerialNo } from './helpers';
|
||||||
import { StockTransferItem } from './StockTransferItem';
|
import { StockTransferItem } from './StockTransferItem';
|
||||||
import { Transfer } from './Transfer';
|
import { Transfer } from './Transfer';
|
||||||
|
|
||||||
@ -91,6 +91,7 @@ export abstract class StockTransfer extends Transfer {
|
|||||||
rate: row.rate!,
|
rate: row.rate!,
|
||||||
quantity: row.quantity!,
|
quantity: row.quantity!,
|
||||||
batch: row.batch!,
|
batch: row.batch!,
|
||||||
|
serialNo: row.serialNo!,
|
||||||
fromLocation,
|
fromLocation,
|
||||||
toLocation,
|
toLocation,
|
||||||
};
|
};
|
||||||
@ -162,6 +163,7 @@ export abstract class StockTransfer extends Transfer {
|
|||||||
override async validate(): Promise<void> {
|
override async validate(): Promise<void> {
|
||||||
await super.validate();
|
await super.validate();
|
||||||
await validateBatch(this);
|
await validateBatch(this);
|
||||||
|
await validateSerialNo(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getActions(fyo: Fyo): Action[] {
|
static getActions(fyo: Fyo): Action[] {
|
||||||
|
@ -28,6 +28,7 @@ export class StockTransferItem extends Doc {
|
|||||||
description?: string;
|
description?: string;
|
||||||
hsnCode?: number;
|
hsnCode?: number;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
|
serialNo?: string;
|
||||||
|
|
||||||
parentdoc?: StockTransfer;
|
parentdoc?: StockTransfer;
|
||||||
|
|
||||||
@ -215,6 +216,7 @@ export class StockTransferItem extends Doc {
|
|||||||
|
|
||||||
override hidden: HiddenMap = {
|
override hidden: HiddenMap = {
|
||||||
batch: () => !this.fyo.singles.InventorySettings?.enableBatches,
|
batch: () => !this.fyo.singles.InventorySettings?.enableBatches,
|
||||||
|
serialNo: () => !this.fyo.singles.InventorySettings?.enableSerialNo,
|
||||||
transferUnit: () =>
|
transferUnit: () =>
|
||||||
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
||||||
transferQuantity: () =>
|
transferQuantity: () =>
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import { t } from 'fyo';
|
||||||
|
import { Doc } from 'fyo/model/doc';
|
||||||
|
import { DocValueMap } from 'fyo/core/types';
|
||||||
import { ValidationError } from 'fyo/utils/errors';
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
import { Invoice } from 'models/baseModels/Invoice/Invoice';
|
import { Invoice } from 'models/baseModels/Invoice/Invoice';
|
||||||
import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
|
import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import { ShipmentItem } from './ShipmentItem';
|
||||||
import { StockMovement } from './StockMovement';
|
import { StockMovement } from './StockMovement';
|
||||||
import { StockMovementItem } from './StockMovementItem';
|
import { StockMovementItem } from './StockMovementItem';
|
||||||
import { StockTransfer } from './StockTransfer';
|
import { StockTransfer } from './StockTransfer';
|
||||||
@ -25,11 +29,7 @@ async function validateItemRowBatch(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasBatch = await doc.fyo.getValue(
|
const hasBatch = await doc.fyo.getValue(ModelNameEnum.Item, item, 'hasBatch');
|
||||||
ModelNameEnum.Item,
|
|
||||||
item,
|
|
||||||
'hasBatch'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasBatch && batch) {
|
if (!hasBatch && batch) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
@ -49,3 +49,211 @@ async function validateItemRowBatch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function validateSerialNo(
|
||||||
|
doc: StockMovement | StockTransfer | Invoice
|
||||||
|
) {
|
||||||
|
for (const row of doc.items ?? []) {
|
||||||
|
await validateItemRowSerialNo(row, doc.movementType as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateItemRowSerialNo(
|
||||||
|
doc: StockMovementItem | StockTransferItem | InvoiceItem,
|
||||||
|
movementType: string
|
||||||
|
) {
|
||||||
|
const idx = doc.idx ?? 0 + 1;
|
||||||
|
const item = doc.item;
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.parentdoc?.cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSerialNo = await doc.fyo.getValue(
|
||||||
|
ModelNameEnum.Item,
|
||||||
|
item,
|
||||||
|
'hasSerialNo'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasSerialNo && !doc.serialNo) {
|
||||||
|
throw new ValidationError(
|
||||||
|
[
|
||||||
|
doc.fyo.t`Serial No not set for row ${idx}.`,
|
||||||
|
doc.fyo.t`Serial No is enabled for Item ${item}`,
|
||||||
|
].join(' ')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSerialNo && doc.serialNo) {
|
||||||
|
throw new ValidationError(
|
||||||
|
[
|
||||||
|
doc.fyo.t`Serial No set for row ${idx}.`,
|
||||||
|
doc.fyo.t`Serial No is not enabled for Item ${item}`,
|
||||||
|
].join(' ')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSerialNo) return;
|
||||||
|
|
||||||
|
const serialNos = getSerialNumbers(doc.serialNo as string);
|
||||||
|
|
||||||
|
if (serialNos.length !== doc.quantity) {
|
||||||
|
throw new ValidationError(
|
||||||
|
t`${doc.quantity!} Serial Numbers required for ${doc.item!}. You have provided ${
|
||||||
|
serialNos.length
|
||||||
|
}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const serialNo of serialNos) {
|
||||||
|
const { name, status, item } = await doc.fyo.db.get(
|
||||||
|
ModelNameEnum.SerialNo,
|
||||||
|
serialNo,
|
||||||
|
['name', 'status', 'item']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (movementType == 'MaterialIssue') {
|
||||||
|
await validateSNMaterialIssue(
|
||||||
|
doc,
|
||||||
|
name as string,
|
||||||
|
item as string,
|
||||||
|
serialNo,
|
||||||
|
status as string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movementType == 'MaterialReceipt') {
|
||||||
|
await validateSNMaterialReceipt(
|
||||||
|
doc,
|
||||||
|
name as string,
|
||||||
|
serialNo,
|
||||||
|
status as string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movementType === 'Shipment') {
|
||||||
|
await validateSNShipment(doc, serialNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.parentSchemaName === 'PurchaseReceipt') {
|
||||||
|
await validateSNPurchaseReceipt(
|
||||||
|
doc,
|
||||||
|
name as string,
|
||||||
|
serialNo,
|
||||||
|
status as string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSerialNumbers = (serialNo: string): string[] => {
|
||||||
|
return serialNo ? serialNo.split('\n') : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSerialNoStatus = async (
|
||||||
|
doc: Doc,
|
||||||
|
items: StockMovementItem[] | ShipmentItem[],
|
||||||
|
newStatus: string
|
||||||
|
) => {
|
||||||
|
for (const item of items) {
|
||||||
|
const serialNos = getSerialNumbers(item.serialNo!);
|
||||||
|
if (!serialNos.length) break;
|
||||||
|
|
||||||
|
for (const serialNo of serialNos) {
|
||||||
|
await doc.fyo.db.update(ModelNameEnum.SerialNo, {
|
||||||
|
name: serialNo,
|
||||||
|
status: newStatus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const validateSNMaterialReceipt = async (
|
||||||
|
doc: Doc,
|
||||||
|
name: string,
|
||||||
|
serialNo: string,
|
||||||
|
status: string
|
||||||
|
) => {
|
||||||
|
if (name === undefined) {
|
||||||
|
const values = {
|
||||||
|
name: serialNo,
|
||||||
|
item: doc.item,
|
||||||
|
party: doc.parentdoc?.party as string,
|
||||||
|
};
|
||||||
|
(
|
||||||
|
await doc.fyo.doc
|
||||||
|
.getNewDoc(ModelNameEnum.SerialNo, values as DocValueMap)
|
||||||
|
.sync()
|
||||||
|
).submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status !== 'Inactive') {
|
||||||
|
throw new ValidationError(t`SerialNo ${serialNo} status is not Inactive`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSNPurchaseReceipt = async (
|
||||||
|
doc: Doc,
|
||||||
|
name: string,
|
||||||
|
serialNo: string,
|
||||||
|
status: string
|
||||||
|
) => {
|
||||||
|
if (name === undefined) {
|
||||||
|
const values = {
|
||||||
|
name: serialNo,
|
||||||
|
item: doc.item,
|
||||||
|
party: doc.parentdoc?.party as string,
|
||||||
|
status: 'Inactive',
|
||||||
|
};
|
||||||
|
(
|
||||||
|
await doc.fyo.doc
|
||||||
|
.getNewDoc(ModelNameEnum.SerialNo, values as DocValueMap)
|
||||||
|
.sync()
|
||||||
|
).submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status !== 'Inactive') {
|
||||||
|
throw new ValidationError(t`SerialNo ${serialNo} status is not Inactive`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSNMaterialIssue = async (
|
||||||
|
doc: Doc,
|
||||||
|
name: string,
|
||||||
|
item: string,
|
||||||
|
serialNo: string,
|
||||||
|
status: string
|
||||||
|
) => {
|
||||||
|
if (doc.isCancelled) return;
|
||||||
|
|
||||||
|
if (!name)
|
||||||
|
throw new ValidationError(t`Serial Number ${serialNo} does not exist.`);
|
||||||
|
|
||||||
|
if (status !== 'Active')
|
||||||
|
throw new ValidationError(
|
||||||
|
t`Serial Number ${serialNo} status is not Active`
|
||||||
|
);
|
||||||
|
if (doc.item !== item) {
|
||||||
|
throw new ValidationError(
|
||||||
|
t`Serial Number ${serialNo} does not belong to the item ${
|
||||||
|
doc.item! as string
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSNShipment = async (doc: Doc, serialNo: string) => {
|
||||||
|
const { status } = await doc.fyo.db.get(
|
||||||
|
ModelNameEnum.SerialNo,
|
||||||
|
serialNo,
|
||||||
|
'status'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status !== 'Active')
|
||||||
|
throw new ValidationError(t`Serial No ${serialNo} status is not Active`);
|
||||||
|
};
|
||||||
|
@ -28,6 +28,7 @@ type Transfer = {
|
|||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
|
serialNo?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
rate: number;
|
rate: number;
|
||||||
};
|
};
|
||||||
@ -36,8 +37,8 @@ interface TransferTwo extends Omit<Transfer, 'from' | 'to'> {
|
|||||||
location: string;
|
location: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getItem(name: string, rate: number, hasBatch: boolean = false) {
|
export function getItem(name: string, rate: number, hasBatch: boolean = false, hasSerialNo: boolean = false) {
|
||||||
return { name, rate, trackItem: true, hasBatch };
|
return { name, rate, trackItem: true, hasBatch, hasSerialNo };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBatch(
|
export async function getBatch(
|
||||||
@ -84,6 +85,7 @@ export async function getStockMovement(
|
|||||||
from: fromLocation,
|
from: fromLocation,
|
||||||
to: toLocation,
|
to: toLocation,
|
||||||
batch,
|
batch,
|
||||||
|
serialNo,
|
||||||
quantity,
|
quantity,
|
||||||
rate,
|
rate,
|
||||||
} of transfers) {
|
} of transfers) {
|
||||||
@ -92,6 +94,7 @@ export async function getStockMovement(
|
|||||||
fromLocation,
|
fromLocation,
|
||||||
toLocation,
|
toLocation,
|
||||||
batch,
|
batch,
|
||||||
|
serialNo,
|
||||||
rate,
|
rate,
|
||||||
quantity,
|
quantity,
|
||||||
});
|
});
|
||||||
|
312
models/inventory/tests/testSerialNos.spec.ts
Normal file
312
models/inventory/tests/testSerialNos.spec.ts
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import { assertThrows } from 'backend/database/tests/helpers';
|
||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import test from 'tape';
|
||||||
|
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||||
|
import { MovementType } from '../types';
|
||||||
|
import { getItem, getStockMovement } from './helpers';
|
||||||
|
|
||||||
|
const fyo = getTestFyo();
|
||||||
|
|
||||||
|
setupTestFyo(fyo, __filename);
|
||||||
|
|
||||||
|
const itemMap = {
|
||||||
|
Pen: {
|
||||||
|
name: 'Pen',
|
||||||
|
rate: 700,
|
||||||
|
},
|
||||||
|
Ink: {
|
||||||
|
name: 'Ink',
|
||||||
|
rate: 50,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const locationMap = {
|
||||||
|
LocationOne: 'LocationOne',
|
||||||
|
LocationTwo: 'LocationTwo',
|
||||||
|
};
|
||||||
|
|
||||||
|
const partyMap = {
|
||||||
|
partyOne: { name: 'Someone', Role: 'Both' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const serialNoMap = {
|
||||||
|
serialOne: {
|
||||||
|
name: 'PN-AB001',
|
||||||
|
item: itemMap.Pen.name,
|
||||||
|
},
|
||||||
|
serialTwo: {
|
||||||
|
name: 'PN-AB002',
|
||||||
|
item: itemMap.Pen.name,
|
||||||
|
},
|
||||||
|
serialThree: {
|
||||||
|
name: 'PN-AB003',
|
||||||
|
item: itemMap.Pen.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('create dummy items, locations, party & serialNos', async (t) => {
|
||||||
|
// Create Items
|
||||||
|
for (const { name, rate } of Object.values(itemMap)) {
|
||||||
|
const item = getItem(name, rate, false, true);
|
||||||
|
await fyo.doc.getNewDoc(ModelNameEnum.Item, item).sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Locations
|
||||||
|
for (const name of Object.values(locationMap)) {
|
||||||
|
await fyo.doc.getNewDoc(ModelNameEnum.Location, { name }).sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Party
|
||||||
|
await fyo.doc.getNewDoc(ModelNameEnum.Party, partyMap.partyOne).sync();
|
||||||
|
|
||||||
|
t.ok(
|
||||||
|
await fyo.db.exists(ModelNameEnum.Party, partyMap.partyOne.name),
|
||||||
|
'party created'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create SerialNos
|
||||||
|
for (const serialNo of Object.values(serialNoMap)) {
|
||||||
|
const doc = fyo.doc.getNewDoc(ModelNameEnum.SerialNo, serialNo);
|
||||||
|
await doc.sync();
|
||||||
|
|
||||||
|
const status = await fyo.getValue(
|
||||||
|
ModelNameEnum.SerialNo,
|
||||||
|
serialNo.name,
|
||||||
|
'status'
|
||||||
|
);
|
||||||
|
t.equal(
|
||||||
|
status,
|
||||||
|
'Inactive',
|
||||||
|
`${serialNo.name} exists and inital status Inactive`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serialNo enabled item, create stock movement, material receipt', async (t) => {
|
||||||
|
const { rate } = itemMap.Pen;
|
||||||
|
const serialNo =
|
||||||
|
serialNoMap.serialOne.name + '\n' + serialNoMap.serialTwo.name;
|
||||||
|
const stockMovement = await getStockMovement(
|
||||||
|
MovementType.MaterialReceipt,
|
||||||
|
new Date('2022-11-03T09:57:04.528'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item: itemMap.Pen.name,
|
||||||
|
to: locationMap.LocationOne,
|
||||||
|
quantity: 2,
|
||||||
|
serialNo,
|
||||||
|
rate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fyo
|
||||||
|
);
|
||||||
|
|
||||||
|
await (await stockMovement.sync()).submit();
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Pen.name,
|
||||||
|
locationMap.LocationOne,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNoMap.serialOne.name
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
'serialNo one has quantity one'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Pen.name,
|
||||||
|
locationMap.LocationOne,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNoMap.serialTwo.name
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
'serialNo two has quantity one'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Pen.name,
|
||||||
|
locationMap.LocationOne,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNoMap.serialThree.name
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
'serialNo three has no quantity'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Ink.name,
|
||||||
|
locationMap.LocationOne,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNoMap.serialOne.name
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
'non transacted item has no quantity'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serialNo enabled item, create stock movement, material issue', async (t) => {
|
||||||
|
const { rate } = itemMap.Pen;
|
||||||
|
const quantity = 1;
|
||||||
|
|
||||||
|
const stockMovement = await getStockMovement(
|
||||||
|
MovementType.MaterialIssue,
|
||||||
|
new Date('2022-11-03T10:00:00.528'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item: itemMap.Pen.name,
|
||||||
|
from: locationMap.LocationOne,
|
||||||
|
serialNo: serialNoMap.serialOne.name,
|
||||||
|
quantity,
|
||||||
|
rate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fyo
|
||||||
|
);
|
||||||
|
|
||||||
|
await (await stockMovement.sync()).submit();
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Pen.name,
|
||||||
|
locationMap.LocationOne,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNoMap.serialOne.name
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
'serialNo one quantity transacted out'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Pen.name,
|
||||||
|
locationMap.LocationOne,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNoMap.serialTwo.name
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
'serialNo two quantity intact'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serialNo enabled item, create stock movement, material transfer', async (t) => {
|
||||||
|
const { rate } = itemMap.Pen;
|
||||||
|
const quantity = 1;
|
||||||
|
const serialNo = serialNoMap.serialTwo.name;
|
||||||
|
|
||||||
|
const stockMovement = await getStockMovement(
|
||||||
|
MovementType.MaterialTransfer,
|
||||||
|
new Date('2022-11-03T09:58:04.528'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item: itemMap.Pen.name,
|
||||||
|
from: locationMap.LocationOne,
|
||||||
|
to: locationMap.LocationTwo,
|
||||||
|
serialNo,
|
||||||
|
quantity,
|
||||||
|
rate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fyo
|
||||||
|
);
|
||||||
|
|
||||||
|
await (await stockMovement.sync()).submit();
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Pen.name,
|
||||||
|
locationMap.LocationOne,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNo
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
'location one serialNoTwo transacted out'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Pen.name,
|
||||||
|
locationMap.LocationTwo,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNo
|
||||||
|
),
|
||||||
|
quantity,
|
||||||
|
'location two serialNo transacted in'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serialNo enabled item, create invalid stock movements', async (t) => {
|
||||||
|
const { name, rate } = itemMap.Pen;
|
||||||
|
const quantity = await fyo.db.getStockQuantity(
|
||||||
|
itemMap.Pen.name,
|
||||||
|
locationMap.LocationTwo,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
serialNoMap.serialTwo.name
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(quantity, 1, 'location two, serialNo one has quantity');
|
||||||
|
if (!quantity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stockMovement = await getStockMovement(
|
||||||
|
MovementType.MaterialIssue,
|
||||||
|
new Date('2022-11-03T09:59:04.528'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item: itemMap.Pen.name,
|
||||||
|
from: locationMap.LocationTwo,
|
||||||
|
serialNo: serialNoMap.serialOne.name,
|
||||||
|
quantity,
|
||||||
|
rate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fyo
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertThrows(
|
||||||
|
async () => (await stockMovement.sync()).submit(),
|
||||||
|
'invalid stockMovement with insufficient quantity did not throw'
|
||||||
|
);
|
||||||
|
|
||||||
|
stockMovement = await getStockMovement(
|
||||||
|
MovementType.MaterialIssue,
|
||||||
|
new Date('2022-11-03T09:59:04.528'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item: itemMap.Pen.name,
|
||||||
|
from: locationMap.LocationTwo,
|
||||||
|
quantity,
|
||||||
|
rate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fyo
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertThrows(
|
||||||
|
async () => (await stockMovement.sync()).submit(),
|
||||||
|
'invalid stockMovement without serialNo did not throw'
|
||||||
|
);
|
||||||
|
t.equal(await fyo.db.getStockQuantity(name), 1, 'item still has quantity');
|
||||||
|
});
|
||||||
|
|
||||||
|
closeTestFyo(fyo, __filename);
|
@ -23,6 +23,7 @@ export interface SMTransferDetails {
|
|||||||
rate: Money;
|
rate: Money;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
|
serialNo?: string;
|
||||||
fromLocation?: string;
|
fromLocation?: string;
|
||||||
toLocation?: string;
|
toLocation?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export type InvoiceStatus = 'Draft' | 'Saved' | 'Unpaid' | 'Cancelled' | 'Paid';
|
export type InvoiceStatus = 'Draft' | 'Saved' | 'Unpaid' | 'Cancelled' | 'Paid';
|
||||||
|
export type SerialNoStatus = 'Inactive' | 'Active' | 'Delivered' | 'Expired';
|
||||||
export enum ModelNameEnum {
|
export enum ModelNameEnum {
|
||||||
Account = 'Account',
|
Account = 'Account',
|
||||||
AccountingLedgerEntry = 'AccountingLedgerEntry',
|
AccountingLedgerEntry = 'AccountingLedgerEntry',
|
||||||
@ -25,6 +26,7 @@ export enum ModelNameEnum {
|
|||||||
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
|
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
|
||||||
SalesInvoice = 'SalesInvoice',
|
SalesInvoice = 'SalesInvoice',
|
||||||
SalesInvoiceItem = 'SalesInvoiceItem',
|
SalesInvoiceItem = 'SalesInvoiceItem',
|
||||||
|
SerialNo = 'SerialNo',
|
||||||
SetupWizard = 'SetupWizard',
|
SetupWizard = 'SetupWizard',
|
||||||
Tax = 'Tax',
|
Tax = 'Tax',
|
||||||
TaxDetail = 'TaxDetail',
|
TaxDetail = 'TaxDetail',
|
||||||
|
@ -28,6 +28,7 @@ export class StockLedger extends Report {
|
|||||||
item?: string;
|
item?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
|
serialNo?: string;
|
||||||
fromDate?: string;
|
fromDate?: string;
|
||||||
toDate?: string;
|
toDate?: string;
|
||||||
ascending?: boolean;
|
ascending?: boolean;
|
||||||
@ -41,6 +42,11 @@ export class StockLedger extends Report {
|
|||||||
.enableBatches;
|
.enableBatches;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasSerialNos(): boolean {
|
||||||
|
return !!(this.fyo.singles.InventorySettings as InventorySettings)
|
||||||
|
.enableSerialNo;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(fyo: Fyo) {
|
constructor(fyo: Fyo) {
|
||||||
super(fyo);
|
super(fyo);
|
||||||
this._setObservers();
|
this._setObservers();
|
||||||
@ -276,6 +282,11 @@ export class StockLedger extends Report {
|
|||||||
{ fieldname: 'batch', label: 'Batch', fieldtype: 'Link' },
|
{ fieldname: 'batch', label: 'Batch', fieldtype: 'Link' },
|
||||||
] as ColumnField[])
|
] as ColumnField[])
|
||||||
: []),
|
: []),
|
||||||
|
...(this.hasSerialNos
|
||||||
|
? ([
|
||||||
|
{ fieldname: 'serialNo', label: 'Serial No', fieldtype: 'Data' },
|
||||||
|
] as ColumnField[])
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
fieldname: 'quantity',
|
fieldname: 'quantity',
|
||||||
label: 'Quantity',
|
label: 'Quantity',
|
||||||
|
@ -19,6 +19,7 @@ export async function getRawStockLedgerEntries(fyo: Fyo) {
|
|||||||
'date',
|
'date',
|
||||||
'item',
|
'item',
|
||||||
'batch',
|
'batch',
|
||||||
|
'serialNo',
|
||||||
'rate',
|
'rate',
|
||||||
'quantity',
|
'quantity',
|
||||||
'location',
|
'location',
|
||||||
@ -49,6 +50,7 @@ export function getStockLedgerEntries(
|
|||||||
const rate = safeParseFloat(sle.rate);
|
const rate = safeParseFloat(sle.rate);
|
||||||
const { item, location, quantity, referenceName, referenceType } = sle;
|
const { item, location, quantity, referenceName, referenceType } = sle;
|
||||||
const batch = sle.batch ?? '';
|
const batch = sle.batch ?? '';
|
||||||
|
const serialNo = sle.serialNo ?? '';
|
||||||
|
|
||||||
if (quantity === 0) {
|
if (quantity === 0) {
|
||||||
continue;
|
continue;
|
||||||
@ -88,6 +90,7 @@ export function getStockLedgerEntries(
|
|||||||
item,
|
item,
|
||||||
location,
|
location,
|
||||||
batch,
|
batch,
|
||||||
|
serialNo,
|
||||||
|
|
||||||
quantity,
|
quantity,
|
||||||
balanceQuantity,
|
balanceQuantity,
|
||||||
|
@ -6,6 +6,7 @@ export interface RawStockLedgerEntry {
|
|||||||
item: string;
|
item: string;
|
||||||
rate: string;
|
rate: string;
|
||||||
batch: string | null;
|
batch: string | null;
|
||||||
|
serialNo: string | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
location: string;
|
location: string;
|
||||||
referenceName: string;
|
referenceName: string;
|
||||||
@ -21,6 +22,7 @@ export interface ComputedStockLedgerEntry{
|
|||||||
item: string;
|
item: string;
|
||||||
location:string;
|
location:string;
|
||||||
batch: string;
|
batch: string;
|
||||||
|
serialNo: string;
|
||||||
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
balanceQuantity: number;
|
balanceQuantity: number;
|
||||||
|
@ -138,6 +138,13 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"section": "Inventory"
|
"section": "Inventory"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "hasSerialNo",
|
||||||
|
"label": "Has Serial No",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"default": false,
|
||||||
|
"section": "Inventory"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "uomConversions",
|
"fieldname": "uomConversions",
|
||||||
"label": "UOM Conversions",
|
"label": "UOM Conversions",
|
||||||
|
@ -63,6 +63,12 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"section": "Features"
|
"section": "Features"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "enableSerialNo",
|
||||||
|
"label": "Enable Serial No",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"section": "Features"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "enableUomConversions",
|
"fieldname": "enableUomConversions",
|
||||||
"label": "Enable UOM Conversion",
|
"label": "Enable UOM Conversion",
|
||||||
|
36
schemas/app/inventory/SerialNo.json
Normal file
36
schemas/app/inventory/SerialNo.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "SerialNo",
|
||||||
|
"label": "Serial No",
|
||||||
|
"naming": "manual",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Serial Number",
|
||||||
|
"create": true,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item",
|
||||||
|
"target": "Item",
|
||||||
|
"create": true,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"label": "Description",
|
||||||
|
"placeholder": "Serial Number Description",
|
||||||
|
"fieldtype": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"label": "Status",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"default": "Inactive"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quickEditFields": ["item", "description", "party"],
|
||||||
|
"keywordFields": ["name"]
|
||||||
|
}
|
@ -37,6 +37,14 @@
|
|||||||
"readOnly": true,
|
"readOnly": true,
|
||||||
"section": "Details"
|
"section": "Details"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "serialNo",
|
||||||
|
"label": "Serial No",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"target": "SerialNo",
|
||||||
|
"readOnly": true,
|
||||||
|
"section": "Details"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "item",
|
"fieldname": "item",
|
||||||
"label": "Item",
|
"label": "Item",
|
||||||
|
@ -57,6 +57,11 @@
|
|||||||
"target": "Batch",
|
"target": "Batch",
|
||||||
"create": true
|
"create": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "serialNo",
|
||||||
|
"label": "Serial No",
|
||||||
|
"fieldtype": "Text"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "quantity",
|
"fieldname": "quantity",
|
||||||
"label": "Quantity",
|
"label": "Quantity",
|
||||||
@ -94,6 +99,7 @@
|
|||||||
"transferQuantity",
|
"transferQuantity",
|
||||||
"transferUnit",
|
"transferUnit",
|
||||||
"batch",
|
"batch",
|
||||||
|
"serialNo",
|
||||||
"quantity",
|
"quantity",
|
||||||
"unit",
|
"unit",
|
||||||
"unitConversionFactor",
|
"unitConversionFactor",
|
||||||
|
@ -47,6 +47,11 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"target": "Batch"
|
"target": "Batch"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "serialNo",
|
||||||
|
"label": "Serial No",
|
||||||
|
"fieldtype": "Text"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "quantity",
|
"fieldname": "quantity",
|
||||||
"label": "Quantity",
|
"label": "Quantity",
|
||||||
@ -93,6 +98,7 @@
|
|||||||
"transferQuantity",
|
"transferQuantity",
|
||||||
"transferUnit",
|
"transferUnit",
|
||||||
"batch",
|
"batch",
|
||||||
|
"serialNo",
|
||||||
"quantity",
|
"quantity",
|
||||||
"unit",
|
"unit",
|
||||||
"unitConversionFactor",
|
"unitConversionFactor",
|
||||||
|
@ -11,6 +11,7 @@ import InventorySettings from './app/inventory/InventorySettings.json';
|
|||||||
import Location from './app/inventory/Location.json';
|
import Location from './app/inventory/Location.json';
|
||||||
import PurchaseReceipt from './app/inventory/PurchaseReceipt.json';
|
import PurchaseReceipt from './app/inventory/PurchaseReceipt.json';
|
||||||
import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.json';
|
import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.json';
|
||||||
|
import SerialNo from './app/inventory/SerialNo.json';
|
||||||
import Shipment from './app/inventory/Shipment.json';
|
import Shipment from './app/inventory/Shipment.json';
|
||||||
import ShipmentItem from './app/inventory/ShipmentItem.json';
|
import ShipmentItem from './app/inventory/ShipmentItem.json';
|
||||||
import StockLedgerEntry from './app/inventory/StockLedgerEntry.json';
|
import StockLedgerEntry from './app/inventory/StockLedgerEntry.json';
|
||||||
@ -117,4 +118,5 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
|||||||
PurchaseReceiptItem as Schema,
|
PurchaseReceiptItem as Schema,
|
||||||
|
|
||||||
Batch as Schema,
|
Batch as Schema,
|
||||||
|
SerialNo as Schema,
|
||||||
];
|
];
|
||||||
|
@ -96,6 +96,13 @@ async function getInventorySidebar(): Promise<SidebarRoot[]> {
|
|||||||
name: 'stock-balance',
|
name: 'stock-balance',
|
||||||
route: '/report/StockBalance',
|
route: '/report/StockBalance',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t`Serial No`,
|
||||||
|
name: 'serial-no',
|
||||||
|
route: `/list/SerialNo`,
|
||||||
|
schemaName: 'SerialNo',
|
||||||
|
hidden: () => !fyo.singles.InventorySettings?.enableSerialNo as boolean,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user