mirror of
https://github.com/frappe/books.git
synced 2026-08-24 10:04:45 -05:00
Merge pull request #1419 from Gadha2311/serial-autogenerate
feat: add auto-generation for serial numbers
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
import { Doc } from 'fyo/model/doc';
|
||||||
|
import { ReadOnlyMap, ValidationMap } from 'fyo/model/types';
|
||||||
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
|
|
||||||
|
const invalidNumberSeries = /[/\=\?\&\%]/;
|
||||||
|
|
||||||
|
function getPaddedName(prefix: string, next: number, padZeros: number): string {
|
||||||
|
return prefix + next.toString().padStart(padZeros ?? 4, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SerialNumberSeries extends Doc {
|
||||||
|
validations: ValidationMap = {
|
||||||
|
name: (value) => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidNumberSeries.test(value)) {
|
||||||
|
throw new ValidationError(
|
||||||
|
this.fyo
|
||||||
|
.t`The following characters cannot be used ${'/, ?, &, =, %'} in a Number Series name.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrent() {
|
||||||
|
let current = this.get('current') as number | null;
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
current = this.get('start') as number;
|
||||||
|
} else {
|
||||||
|
current = current + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
async next(schemaName: string) {
|
||||||
|
this.setCurrent();
|
||||||
|
const exists = await this.checkIfCurrentExists(schemaName);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
this.current = (this.current as number) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sync();
|
||||||
|
return this.getPaddedName(this.current as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIfCurrentExists(schemaName: string) {
|
||||||
|
if (!schemaName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = this.getPaddedName(this.current as number);
|
||||||
|
return await this.fyo.db.exists(schemaName, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPaddedName(next: number): string {
|
||||||
|
return getPaddedName(this.name as string, next, this.padZeros as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
readOnly: ReadOnlyMap = {
|
||||||
|
referenceType: () => this.inserted,
|
||||||
|
padZeros: () => this.inserted,
|
||||||
|
start: () => this.inserted,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { ModelMap } from 'fyo/model/types';
|
import { ModelMap } from 'fyo/model/types';
|
||||||
import NumberSeries from './NumberSeries';
|
import NumberSeries from './NumberSeries';
|
||||||
|
import SerialNumberSeries from './SerialNumberSeries';
|
||||||
import SystemSettings from './SystemSettings';
|
import SystemSettings from './SystemSettings';
|
||||||
import { CustomField } from './CustomField';
|
import { CustomField } from './CustomField';
|
||||||
import { CustomForm } from './CustomForm';
|
import { CustomForm } from './CustomForm';
|
||||||
|
|
||||||
export const coreModels = {
|
export const coreModels = {
|
||||||
NumberSeries,
|
NumberSeries,
|
||||||
|
SerialNumberSeries,
|
||||||
SystemSettings,
|
SystemSettings,
|
||||||
CustomForm,
|
CustomForm,
|
||||||
CustomField,
|
CustomField,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class Item extends Doc {
|
|||||||
itemGroup?: string;
|
itemGroup?: string;
|
||||||
hsnCode?: number;
|
hsnCode?: number;
|
||||||
hasSerialNumber?: boolean;
|
hasSerialNumber?: boolean;
|
||||||
|
serialNumberSeries?: string;
|
||||||
uomConversions: UOMConversionItem[] = [];
|
uomConversions: UOMConversionItem[] = [];
|
||||||
|
|
||||||
formulas: FormulaMap = {
|
formulas: FormulaMap = {
|
||||||
@@ -92,6 +93,38 @@ export class Item extends Doc {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.uomConversions = Array.from(latestByUom.values());
|
this.uomConversions = Array.from(latestByUom.values());
|
||||||
|
|
||||||
|
if (this.serialNumberSeries && this.hasSerialNumber) {
|
||||||
|
const series = this.serialNumberSeries.trim();
|
||||||
|
if (series && !series.endsWith('-')) {
|
||||||
|
this.serialNumberSeries = series + '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterSync(): Promise<void> {
|
||||||
|
await super.afterSync();
|
||||||
|
|
||||||
|
if (this.hasSerialNumber && this.serialNumberSeries) {
|
||||||
|
const seriesName = this.serialNumberSeries?.trim();
|
||||||
|
|
||||||
|
if (!seriesName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await this.fyo.db.exists('SerialNumberSeries', seriesName);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
await this.fyo.doc
|
||||||
|
.getNewDoc('SerialNumberSeries', {
|
||||||
|
name: seriesName,
|
||||||
|
start: 1001,
|
||||||
|
padZeros: 4,
|
||||||
|
current: 1001,
|
||||||
|
})
|
||||||
|
.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
@@ -125,6 +158,21 @@ export class Item extends Doc {
|
|||||||
throw new ValidationError(this.fyo.t`Invalid HSN Code.`);
|
throw new ValidationError(this.fyo.t`Invalid HSN Code.`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
serialNumberSeries: (value: DocValue) => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = (value as string).trim();
|
||||||
|
const invalidChars = /[/\=\?\&\%]/;
|
||||||
|
|
||||||
|
if (invalidChars.test(series)) {
|
||||||
|
throw new ValidationError(
|
||||||
|
this.fyo
|
||||||
|
.t`Serial Number Series cannot contain the following characters: /, ?, &, =, %`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
static getActions(fyo: Fyo): Action[] {
|
static getActions(fyo: Fyo): Action[] {
|
||||||
@@ -177,6 +225,7 @@ export class Item extends Doc {
|
|||||||
!(
|
!(
|
||||||
this.fyo.singles.InventorySettings?.enableSerialNumber && this.trackItem
|
this.fyo.singles.InventorySettings?.enableSerialNumber && this.trackItem
|
||||||
),
|
),
|
||||||
|
serialNumberSeries: () => !this.hasSerialNumber,
|
||||||
uomConversions: () =>
|
uomConversions: () =>
|
||||||
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
||||||
itemGroup: () => !this.fyo.singles.AccountingSettings?.enableitemGroup,
|
itemGroup: () => !this.fyo.singles.AccountingSettings?.enableitemGroup,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
getStockLedgerEntries,
|
getStockLedgerEntries,
|
||||||
} from 'reports/inventory/helpers';
|
} from 'reports/inventory/helpers';
|
||||||
import { LoyaltyPointEntry } from './baseModels/LoyaltyPointEntry/LoyaltyPointEntry';
|
import { LoyaltyPointEntry } from './baseModels/LoyaltyPointEntry/LoyaltyPointEntry';
|
||||||
|
import { generateSerialNumbersForItem } from './inventory/helpers';
|
||||||
|
|
||||||
export function getQuoteActions(
|
export function getQuoteActions(
|
||||||
fyo: Fyo,
|
fyo: Fyo,
|
||||||
@@ -759,6 +760,16 @@ export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await item.set('item', name);
|
await item.set('item', name);
|
||||||
|
|
||||||
|
if (
|
||||||
|
doc instanceof StockTransfer &&
|
||||||
|
doc.schemaName === ModelNameEnum.PurchaseReceipt
|
||||||
|
) {
|
||||||
|
const serialNumbers = await generateSerialNumbersForItem(doc.fyo, name, 1);
|
||||||
|
if (serialNumbers) {
|
||||||
|
await item.set('serialNumber', serialNumbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReturnLoyaltyPoints(doc: Invoice) {
|
export async function getReturnLoyaltyPoints(doc: Invoice) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
import { DocValue } from 'fyo/core/types';
|
import { DocValue } from 'fyo/core/types';
|
||||||
import {
|
import {
|
||||||
|
ChangeArg,
|
||||||
FiltersMap,
|
FiltersMap,
|
||||||
FormulaMap,
|
FormulaMap,
|
||||||
HiddenMap,
|
HiddenMap,
|
||||||
@@ -12,6 +13,7 @@ import { ValidationError } from 'fyo/utils/errors';
|
|||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { safeParseFloat } from 'utils/index';
|
import { safeParseFloat } from 'utils/index';
|
||||||
|
import { generateSerialNumbersForItem } from './helpers';
|
||||||
import { StockMovement } from './StockMovement';
|
import { StockMovement } from './StockMovement';
|
||||||
import { TransferItem } from './TransferItem';
|
import { TransferItem } from './TransferItem';
|
||||||
import { MovementTypeEnum } from './types';
|
import { MovementTypeEnum } from './types';
|
||||||
@@ -323,4 +325,46 @@ export class StockMovementItem extends TransferItem {
|
|||||||
static createFilters: FiltersMap = {
|
static createFilters: FiltersMap = {
|
||||||
item: () => ({ trackItem: true, itemType: 'Product' }),
|
item: () => ({ trackItem: true, itemType: 'Product' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
override async change(ch: ChangeArg): Promise<void> {
|
||||||
|
await super.change(ch);
|
||||||
|
|
||||||
|
const shouldGenerateSerialNumbers =
|
||||||
|
this.parentdoc?.movementType === MovementTypeEnum.MaterialReceipt &&
|
||||||
|
this.item &&
|
||||||
|
this.quantity &&
|
||||||
|
this.quantity > 0;
|
||||||
|
|
||||||
|
if (ch.changed === 'item') {
|
||||||
|
await this.set('serialNumber', '');
|
||||||
|
|
||||||
|
if (shouldGenerateSerialNumbers) {
|
||||||
|
await this.generateAndSetSerialNumbers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch.changed === 'quantity') {
|
||||||
|
if (!this.quantity || this.quantity <= 0) {
|
||||||
|
await this.set('serialNumber', '');
|
||||||
|
} else if (shouldGenerateSerialNumbers) {
|
||||||
|
await this.generateAndSetSerialNumbers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateAndSetSerialNumbers(): Promise<void> {
|
||||||
|
if (!this.item || !this.quantity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumbers = await generateSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
this.item,
|
||||||
|
Math.abs(this.quantity)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
await this.set('serialNumber', serialNumbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
updateSerialNumbers,
|
updateSerialNumbers,
|
||||||
validateBatch,
|
validateBatch,
|
||||||
validateSerialNumber,
|
validateSerialNumber,
|
||||||
|
generateSerialNumbersForItem,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { ReturnDocItem } from './types';
|
import { ReturnDocItem } from './types';
|
||||||
import { getShipmentCOGSAmountFromSLEs } from 'reports/inventory/helpers';
|
import { getShipmentCOGSAmountFromSLEs } from 'reports/inventory/helpers';
|
||||||
@@ -468,6 +469,32 @@ export abstract class StockTransfer extends Transfer {
|
|||||||
await this.set('terms', stDoc.terms);
|
await this.set('terms', stDoc.terms);
|
||||||
await this.set('date', stDoc.date);
|
await this.set('date', stDoc.date);
|
||||||
await this.set('items', stDoc.items);
|
await this.set('items', stDoc.items);
|
||||||
|
|
||||||
|
if (this.items) {
|
||||||
|
for (const item of this.items) {
|
||||||
|
if (!item.item || !item.quantity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSerialNumber = await this.fyo.getValue(
|
||||||
|
ModelNameEnum.Item,
|
||||||
|
item.item,
|
||||||
|
'hasSerialNumber'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasSerialNumber) {
|
||||||
|
const serialNumbers = await generateSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
item.item,
|
||||||
|
Math.abs(item.quantity)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
await item.set('serialNumber', serialNumbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInvoice(): Promise<Invoice | null> {
|
async getInvoice(): Promise<Invoice | null> {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import { StockTransfer } from './StockTransfer';
|
|||||||
import { TransferItem } from './TransferItem';
|
import { TransferItem } from './TransferItem';
|
||||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||||
import { PurchaseInvoice } from 'models/baseModels/PurchaseInvoice/PurchaseInvoice';
|
import { PurchaseInvoice } from 'models/baseModels/PurchaseInvoice/PurchaseInvoice';
|
||||||
|
import {
|
||||||
|
generateSerialNumbersForItem,
|
||||||
|
getExistingActiveSerialNumbersForItem,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
export class StockTransferItem extends TransferItem {
|
export class StockTransferItem extends TransferItem {
|
||||||
item?: string;
|
item?: string;
|
||||||
@@ -243,6 +247,80 @@ export class StockTransferItem extends TransferItem {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
serialNumber: {
|
||||||
|
formula: async () => {
|
||||||
|
if (this.serialNumber) {
|
||||||
|
return this.serialNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.item || !this.parentdoc?.backReference) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSerialNumber = await this.fyo.getValue(
|
||||||
|
ModelNameEnum.Item,
|
||||||
|
this.item,
|
||||||
|
'hasSerialNumber'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasSerialNumber) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantity = Math.abs(this.quantity ?? 0);
|
||||||
|
if (quantity <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!this.isSales &&
|
||||||
|
this.parentdoc?.schemaName === ModelNameEnum.PurchaseReceipt
|
||||||
|
) {
|
||||||
|
const serialNumbers = await generateSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
this.item,
|
||||||
|
quantity
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
return serialNumbers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isSales &&
|
||||||
|
this.parentdoc?.schemaName === ModelNameEnum.Shipment
|
||||||
|
) {
|
||||||
|
const salesInvoice = (await this.fyo.doc.getDoc(
|
||||||
|
ModelNameEnum.SalesInvoice,
|
||||||
|
this.parentdoc.backReference
|
||||||
|
)) as SalesInvoice;
|
||||||
|
|
||||||
|
const invoiceItem = salesInvoice?.items?.find(
|
||||||
|
(val) => val.item === this.item
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invoiceItem?.serialNumber) {
|
||||||
|
return invoiceItem.serialNumber as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumbers = await getExistingActiveSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
this.item,
|
||||||
|
quantity
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
return serialNumbers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
dependsOn: ['item', 'quantity'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
validations: ValidationMap = {
|
validations: ValidationMap = {
|
||||||
|
|||||||
+192
-1
@@ -6,11 +6,12 @@ import { ModelNameEnum } from 'models/types';
|
|||||||
import { SerialNumber } from './SerialNumber';
|
import { SerialNumber } from './SerialNumber';
|
||||||
import type { StockMovement } from './StockMovement';
|
import type { StockMovement } from './StockMovement';
|
||||||
import type { StockMovementItem } from './StockMovementItem';
|
import type { StockMovementItem } from './StockMovementItem';
|
||||||
import type { StockTransfer } from './StockTransfer';
|
import { StockTransfer } from './StockTransfer';
|
||||||
import type { StockTransferItem } from './StockTransferItem';
|
import type { StockTransferItem } from './StockTransferItem';
|
||||||
import { Transfer } from './Transfer';
|
import { Transfer } from './Transfer';
|
||||||
import { TransferItem } from './TransferItem';
|
import { TransferItem } from './TransferItem';
|
||||||
import type { SerialNumberStatus } from './types';
|
import type { SerialNumberStatus } from './types';
|
||||||
|
import SerialNumberSeries from 'fyo/models/SerialNumberSeries';
|
||||||
|
|
||||||
export async function validateBatch(
|
export async function validateBatch(
|
||||||
doc: StockMovement | StockTransfer | Invoice
|
doc: StockMovement | StockTransfer | Invoice
|
||||||
@@ -340,3 +341,193 @@ function getSerialNumberStatusForStockMovement(
|
|||||||
|
|
||||||
return isCancel ? 'Inactive' : 'Active';
|
return isCancel ? 'Inactive' : 'Active';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateSerialNumbersForItem(
|
||||||
|
fyo: Fyo,
|
||||||
|
item: string,
|
||||||
|
quantity: number
|
||||||
|
): Promise<string> {
|
||||||
|
if (!quantity || quantity <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSerialNumber = await fyo.getValue(
|
||||||
|
ModelNameEnum.Item,
|
||||||
|
item,
|
||||||
|
'hasSerialNumber'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasSerialNumber) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumberSeries = await fyo.getValue(
|
||||||
|
ModelNameEnum.Item,
|
||||||
|
item,
|
||||||
|
'serialNumberSeries'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!serialNumberSeries || typeof serialNumberSeries !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesName = serialNumberSeries.trim();
|
||||||
|
if (!seriesName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await fyo.db.exists(
|
||||||
|
ModelNameEnum.SerialNumberSeries,
|
||||||
|
seriesName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesDoc = (await fyo.doc.getDoc(
|
||||||
|
ModelNameEnum.SerialNumberSeries,
|
||||||
|
seriesName
|
||||||
|
)) as SerialNumberSeries;
|
||||||
|
|
||||||
|
const serialNumbers: string[] = [];
|
||||||
|
const padZeros = seriesDoc.padZeros as number;
|
||||||
|
|
||||||
|
let currentValue = await getHighestSerialNumberForItem(fyo, item, seriesName);
|
||||||
|
|
||||||
|
if (currentValue === null) {
|
||||||
|
currentValue =
|
||||||
|
((seriesDoc.current as number) || (seriesDoc.start as number)) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (serialNumbers.length < quantity) {
|
||||||
|
currentValue++;
|
||||||
|
|
||||||
|
const serialNumber = getPaddedName(seriesName, currentValue, padZeros);
|
||||||
|
|
||||||
|
const snExists = await fyo.db.exists(
|
||||||
|
ModelNameEnum.SerialNumber,
|
||||||
|
serialNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!snExists) {
|
||||||
|
serialNumbers.push(serialNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serialNumbers.length > 0) {
|
||||||
|
await seriesDoc.set('current', currentValue);
|
||||||
|
await seriesDoc.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = serialNumbers.join('\n');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHighestSerialNumberForItem(
|
||||||
|
fyo: Fyo,
|
||||||
|
item: string,
|
||||||
|
seriesName: string
|
||||||
|
): Promise<number | null> {
|
||||||
|
const serialNumbers = await fyo.db.getAllRaw(ModelNameEnum.SerialNumber, {
|
||||||
|
filters: { item: item },
|
||||||
|
fields: ['name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!serialNumbers || serialNumbers.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let highestValue = -1;
|
||||||
|
|
||||||
|
for (const sn of serialNumbers) {
|
||||||
|
const name = sn.name as string;
|
||||||
|
|
||||||
|
if (name.startsWith(seriesName)) {
|
||||||
|
const numericPart = name.substring(seriesName.length);
|
||||||
|
const value = parseInt(numericPart, 10);
|
||||||
|
|
||||||
|
if (!isNaN(value) && value > highestValue) {
|
||||||
|
highestValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return highestValue >= 0 ? highestValue : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaddedName(prefix: string, next: number, padZeros: number): string {
|
||||||
|
return prefix + next.toString().padStart(padZeros ?? 4, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExistingActiveSerialNumbersForItem(
|
||||||
|
fyo: Fyo,
|
||||||
|
item: string,
|
||||||
|
quantity: number
|
||||||
|
): Promise<string> {
|
||||||
|
if (!quantity || quantity <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSerialNumber = await fyo.getValue(
|
||||||
|
ModelNameEnum.Item,
|
||||||
|
item,
|
||||||
|
'hasSerialNumber'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasSerialNumber) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockLedgerEntries = (await fyo.db.getAllRaw(
|
||||||
|
ModelNameEnum.StockLedgerEntry,
|
||||||
|
{
|
||||||
|
fields: ['serialNumber', 'date', 'quantity'],
|
||||||
|
filters: {
|
||||||
|
item: item,
|
||||||
|
serialNumber: ['!=', ''],
|
||||||
|
},
|
||||||
|
orderBy: ['date', 'created', 'name'],
|
||||||
|
order: 'asc',
|
||||||
|
}
|
||||||
|
)) as { serialNumber: string; date: string; quantity: number }[];
|
||||||
|
|
||||||
|
if (!stockLedgerEntries || stockLedgerEntries.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumberStockMap: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const entry of stockLedgerEntries) {
|
||||||
|
const sn = entry.serialNumber.trim();
|
||||||
|
if (!sn) continue;
|
||||||
|
|
||||||
|
serialNumberStockMap[sn] = (serialNumberStockMap[sn] || 0) + entry.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableSerialNumbers: string[] = [];
|
||||||
|
const seenSerialNumbers = new Set<string>();
|
||||||
|
|
||||||
|
for (const entry of stockLedgerEntries) {
|
||||||
|
const sn = entry.serialNumber.trim();
|
||||||
|
if (!sn) continue;
|
||||||
|
if (seenSerialNumbers.has(sn)) continue;
|
||||||
|
|
||||||
|
if ((serialNumberStockMap[sn] || 0) > 0) {
|
||||||
|
availableSerialNumbers.push(sn);
|
||||||
|
seenSerialNumbers.add(sn);
|
||||||
|
|
||||||
|
if (availableSerialNumbers.length >= quantity) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableSerialNumbers.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedSerialNumbers = availableSerialNumbers.slice(0, quantity);
|
||||||
|
|
||||||
|
return selectedSerialNumbers.join('\n');
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export enum ModelNameEnum {
|
|||||||
JournalEntryAccount = 'JournalEntryAccount',
|
JournalEntryAccount = 'JournalEntryAccount',
|
||||||
Misc = 'Misc',
|
Misc = 'Misc',
|
||||||
NumberSeries = 'NumberSeries',
|
NumberSeries = 'NumberSeries',
|
||||||
|
SerialNumberSeries = 'SerialNumberSeries',
|
||||||
Lead = 'Lead',
|
Lead = 'Lead',
|
||||||
Party = 'Party',
|
Party = 'Party',
|
||||||
LoyaltyProgram = 'LoyaltyProgram',
|
LoyaltyProgram = 'LoyaltyProgram',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ColumnField, ReportData } from 'reports/types';
|
|||||||
import { Field } from 'schemas/types';
|
import { Field } from 'schemas/types';
|
||||||
import { getStockBalanceEntries } from './helpers';
|
import { getStockBalanceEntries } from './helpers';
|
||||||
import { StockLedger } from './StockLedger';
|
import { StockLedger } from './StockLedger';
|
||||||
import { ReferenceType } from './types';
|
import { ReferenceType, SerialNumberStatus } from './types';
|
||||||
|
|
||||||
export class StockBalance extends StockLedger {
|
export class StockBalance extends StockLedger {
|
||||||
static title = t`Stock Balance`;
|
static title = t`Stock Balance`;
|
||||||
@@ -17,6 +17,9 @@ export class StockBalance extends StockLedger {
|
|||||||
override referenceType: ReferenceType = 'All';
|
override referenceType: ReferenceType = 'All';
|
||||||
override referenceName = '';
|
override referenceName = '';
|
||||||
|
|
||||||
|
showSerialNumbers = false;
|
||||||
|
serialNumberFilter: SerialNumberStatus = 'All';
|
||||||
|
|
||||||
override async _getReportData(force?: boolean): Promise<ReportData> {
|
override async _getReportData(force?: boolean): Promise<ReportData> {
|
||||||
if (this.shouldRefresh || force || !this._rawData?.length) {
|
if (this.shouldRefresh || force || !this._rawData?.length) {
|
||||||
await this._setRawData();
|
await this._setRawData();
|
||||||
@@ -30,7 +33,12 @@ export class StockBalance extends StockLedger {
|
|||||||
toDate: this.toDate,
|
toDate: this.toDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawData = getStockBalanceEntries(this._rawData ?? [], filters);
|
const rawData = getStockBalanceEntries(
|
||||||
|
this._rawData ?? [],
|
||||||
|
filters,
|
||||||
|
this.showSerialNumbers,
|
||||||
|
this.serialNumberFilter
|
||||||
|
);
|
||||||
|
|
||||||
return rawData.map((sbe, i) => {
|
return rawData.map((sbe, i) => {
|
||||||
const row = { ...sbe, name: i + 1 } as RawValueMap;
|
const row = { ...sbe, name: i + 1 } as RawValueMap;
|
||||||
@@ -81,12 +89,53 @@ export class StockBalance extends StockLedger {
|
|||||||
label: t`To Date`,
|
label: t`To Date`,
|
||||||
fieldname: 'toDate',
|
fieldname: 'toDate',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'Check',
|
||||||
|
placeholder: t`Serial Number`,
|
||||||
|
label: t`Serial Number`,
|
||||||
|
fieldname: 'showSerialNumbers',
|
||||||
|
},
|
||||||
|
...(this.showSerialNumbers
|
||||||
|
? ([
|
||||||
|
{
|
||||||
|
fieldtype: 'Select',
|
||||||
|
options: [
|
||||||
|
{ label: t`All`, value: 'All' },
|
||||||
|
{ label: t`In stock`, value: 'In stock' },
|
||||||
|
{ label: t`Out stock`, value: 'Out stock' },
|
||||||
|
],
|
||||||
|
placeholder: t`Serial Number Status`,
|
||||||
|
label: t`Serial Number Status`,
|
||||||
|
fieldname: 'serialNumberFilter',
|
||||||
|
default: 'All',
|
||||||
|
},
|
||||||
|
] as Field[])
|
||||||
|
: []),
|
||||||
] as Field[];
|
] as Field[];
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumns(): ColumnField[] {
|
getColumns(): ColumnField[] {
|
||||||
|
const batch: ColumnField[] = [];
|
||||||
|
const serialNumber: ColumnField[] = [];
|
||||||
|
|
||||||
|
if (this.hasBatches) {
|
||||||
|
batch.push({
|
||||||
|
fieldname: 'batch',
|
||||||
|
label: 'Batch',
|
||||||
|
fieldtype: 'Link',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showSerialNumbers && this.hasSerialNumbers) {
|
||||||
|
serialNumber.push({
|
||||||
|
fieldname: 'serialNumber',
|
||||||
|
label: 'Serial Number',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
fieldname: 'name',
|
fieldname: 'name',
|
||||||
@@ -104,11 +153,8 @@ export class StockBalance extends StockLedger {
|
|||||||
label: 'Location',
|
label: 'Location',
|
||||||
fieldtype: 'Link',
|
fieldtype: 'Link',
|
||||||
},
|
},
|
||||||
...(this.hasBatches
|
...batch,
|
||||||
? ([
|
...serialNumber,
|
||||||
{ fieldname: 'batch', label: 'Batch', fieldtype: 'Link' },
|
|
||||||
] as ColumnField[])
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
fieldname: 'balanceQuantity',
|
fieldname: 'balanceQuantity',
|
||||||
label: 'Balance Qty.',
|
label: 'Balance Qty.',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { safeParseFloat, safeParseInt } from 'utils/index';
|
|||||||
import type {
|
import type {
|
||||||
ComputedStockLedgerEntry,
|
ComputedStockLedgerEntry,
|
||||||
RawStockLedgerEntry,
|
RawStockLedgerEntry,
|
||||||
|
SerialNumberStatus,
|
||||||
StockBalanceEntry,
|
StockBalanceEntry,
|
||||||
} from './types';
|
} from './types';
|
||||||
import type { QueryFilter } from 'utils/db/types';
|
import type { QueryFilter } from 'utils/db/types';
|
||||||
@@ -192,11 +193,13 @@ export function getStockBalanceEntries(
|
|||||||
fromDate?: string;
|
fromDate?: string;
|
||||||
toDate?: string;
|
toDate?: string;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
}
|
},
|
||||||
|
showSerialNumbers = false,
|
||||||
|
serialNumberFilter: SerialNumberStatus = 'All'
|
||||||
): StockBalanceEntry[] {
|
): StockBalanceEntry[] {
|
||||||
const sbeMap: Record<
|
const sbeMap: Record<
|
||||||
Item,
|
Item,
|
||||||
Record<Location, Record<Batch, StockBalanceEntry>>
|
Record<Location, Record<Batch, Record<string, StockBalanceEntry>>>
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
const fromDate = filters.fromDate ? Date.parse(filters.fromDate) : null;
|
const fromDate = filters.fromDate ? Date.parse(filters.fromDate) : null;
|
||||||
@@ -215,19 +218,29 @@ export function getStockBalanceEntries(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
showSerialNumbers &&
|
||||||
|
(!sle.serialNumber || sle.serialNumber.trim() === '')
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const batch = sle.batch || '';
|
const batch = sle.batch || '';
|
||||||
|
const serialNumber = showSerialNumbers ? sle.serialNumber : '';
|
||||||
|
|
||||||
sbeMap[sle.item] ??= {};
|
sbeMap[sle.item] ??= {};
|
||||||
sbeMap[sle.item][sle.location] ??= {};
|
sbeMap[sle.item][sle.location] ??= {};
|
||||||
sbeMap[sle.item][sle.location][batch] ??= getSBE(
|
sbeMap[sle.item][sle.location][batch] ??= {};
|
||||||
|
sbeMap[sle.item][sle.location][batch][serialNumber] ??= getSBE(
|
||||||
sle.item,
|
sle.item,
|
||||||
sle.location,
|
sle.location,
|
||||||
batch
|
batch,
|
||||||
|
serialNumber
|
||||||
);
|
);
|
||||||
const date = sle.date.valueOf();
|
const date = sle.date.valueOf();
|
||||||
|
|
||||||
if (fromDate && date < fromDate) {
|
if (fromDate && date < fromDate) {
|
||||||
const sbe = sbeMap[sle.item][sle.location][batch];
|
const sbe = sbeMap[sle.item][sle.location][batch][serialNumber];
|
||||||
updateOpeningBalances(sbe, sle);
|
updateOpeningBalances(sbe, sle);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -236,21 +249,33 @@ export function getStockBalanceEntries(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sbe = sbeMap[sle.item][sle.location][batch];
|
const sbe = sbeMap[sle.item][sle.location][batch][serialNumber];
|
||||||
updateCurrentBalances(sbe, sle);
|
updateCurrentBalances(sbe, sle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(sbeMap)
|
let entries = Object.values(sbeMap)
|
||||||
.map((sbeBatched) =>
|
.map((sbeBatched) =>
|
||||||
Object.values(sbeBatched).map((sbes) => Object.values(sbes))
|
Object.values(sbeBatched).map((sbes) =>
|
||||||
|
Object.values(sbes).map((sbeWithSN) => Object.values(sbeWithSN))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.flat(2);
|
.flat(3);
|
||||||
|
|
||||||
|
// Filter by serial number status
|
||||||
|
if (serialNumberFilter === 'In stock') {
|
||||||
|
entries = entries.filter((entry) => entry.balanceQuantity > 0);
|
||||||
|
} else if (serialNumberFilter === 'Out stock') {
|
||||||
|
entries = entries.filter((entry) => entry.balanceQuantity <= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSBE(
|
function getSBE(
|
||||||
item: string,
|
item: string,
|
||||||
location: string,
|
location: string,
|
||||||
batch: string
|
batch: string,
|
||||||
|
serialNumber: string
|
||||||
): StockBalanceEntry {
|
): StockBalanceEntry {
|
||||||
return {
|
return {
|
||||||
name: 0,
|
name: 0,
|
||||||
@@ -258,6 +283,7 @@ function getSBE(
|
|||||||
item,
|
item,
|
||||||
location,
|
location,
|
||||||
batch,
|
batch,
|
||||||
|
serialNumber,
|
||||||
|
|
||||||
balanceQuantity: 0,
|
balanceQuantity: 0,
|
||||||
balanceValue: 0,
|
balanceValue: 0,
|
||||||
|
|||||||
@@ -44,16 +44,17 @@ export interface StockBalanceEntry{
|
|||||||
item: string;
|
item: string;
|
||||||
location:string;
|
location:string;
|
||||||
batch: string;
|
batch: string;
|
||||||
|
serialNumber: string;
|
||||||
|
|
||||||
balanceQuantity: number;
|
balanceQuantity: number;
|
||||||
balanceValue: number;
|
balanceValue: number;
|
||||||
|
|
||||||
openingQuantity: number;
|
openingQuantity: number;
|
||||||
openingValue:number;
|
openingValue:number;
|
||||||
|
|
||||||
incomingQuantity:number;
|
incomingQuantity:number;
|
||||||
incomingValue:number;
|
incomingValue:number;
|
||||||
|
|
||||||
outgoingQuantity:number;
|
outgoingQuantity:number;
|
||||||
outgoingValue:number;
|
outgoingValue:number;
|
||||||
|
|
||||||
@@ -65,3 +66,6 @@ export type ReferenceType =
|
|||||||
| ModelNameEnum.Shipment
|
| ModelNameEnum.Shipment
|
||||||
| ModelNameEnum.PurchaseReceipt
|
| ModelNameEnum.PurchaseReceipt
|
||||||
| 'All';
|
| 'All';
|
||||||
|
|
||||||
|
export type SerialNumberStatus = 'All' | 'In stock' | 'Out stock';
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,12 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"section": "Inventory"
|
"section": "Inventory"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "serialNumberSeries",
|
||||||
|
"label": "Serial Number Series",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"section": "Inventory"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "uomConversions",
|
"fieldname": "uomConversions",
|
||||||
"label": "UOM Conversions",
|
"label": "UOM Conversions",
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "SerialNumberSeries",
|
||||||
|
"label": "Serial Number Series",
|
||||||
|
"naming": "manual",
|
||||||
|
"isSingle": false,
|
||||||
|
"isChild": false,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "name",
|
||||||
|
"label": "Prefix",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "start",
|
||||||
|
"label": "Start",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"default": 1001,
|
||||||
|
"required": true,
|
||||||
|
"minvalue": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "padZeros",
|
||||||
|
"label": "Pad Zeros",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"default": 4,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "current",
|
||||||
|
"label": "Current",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"required": true,
|
||||||
|
"readOnly": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quickEditFields": ["start", "padZeros"]
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import JournalEntry from './app/JournalEntry.json';
|
|||||||
import JournalEntryAccount from './app/JournalEntryAccount.json';
|
import JournalEntryAccount from './app/JournalEntryAccount.json';
|
||||||
import Misc from './app/Misc.json';
|
import Misc from './app/Misc.json';
|
||||||
import NumberSeries from './app/NumberSeries.json';
|
import NumberSeries from './app/NumberSeries.json';
|
||||||
|
import SerialNumberSeries from './app/SerialNumberSeries.json';
|
||||||
import Party from './app/Party.json';
|
import Party from './app/Party.json';
|
||||||
import Lead from './app/Lead.json';
|
import Lead from './app/Lead.json';
|
||||||
import LoyaltyProgram from './app/LoyaltyProgram.json';
|
import LoyaltyProgram from './app/LoyaltyProgram.json';
|
||||||
@@ -106,6 +107,7 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
|||||||
Currency as Schema,
|
Currency as Schema,
|
||||||
Defaults as Schema,
|
Defaults as Schema,
|
||||||
NumberSeries as Schema,
|
NumberSeries as Schema,
|
||||||
|
SerialNumberSeries as Schema,
|
||||||
|
|
||||||
PrintSettings as Schema,
|
PrintSettings as Schema,
|
||||||
|
|
||||||
|
|||||||
@@ -267,7 +267,7 @@
|
|||||||
fieldtype: 'Text',
|
fieldtype: 'Text',
|
||||||
fieldname: 'serialNumber',
|
fieldname: 'serialNumber',
|
||||||
}"
|
}"
|
||||||
:value="row.serialNumber"
|
:value="itemSerialNumbers[row.item as string] || row.serialNumber"
|
||||||
:show-label="true"
|
:show-label="true"
|
||||||
:border="true"
|
:border="true"
|
||||||
:required="hasSerialNumber"
|
:required="hasSerialNumber"
|
||||||
@@ -297,6 +297,7 @@ import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
|||||||
import { showToast } from 'src/utils/interactive';
|
import { showToast } from 'src/utils/interactive';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import AutoComplete from 'src/components/Controls/AutoComplete.vue';
|
import AutoComplete from 'src/components/Controls/AutoComplete.vue';
|
||||||
|
import { getExistingActiveSerialNumbersForItem } from 'models/inventory/helpers';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SelectedItemRow',
|
name: 'SelectedItemRow',
|
||||||
@@ -324,6 +325,9 @@ export default defineComponent({
|
|||||||
profileDiscountSetting: null as boolean | null,
|
profileDiscountSetting: null as boolean | null,
|
||||||
profileRateSetting: null as boolean | null,
|
profileRateSetting: null as boolean | null,
|
||||||
transferUnitOptions: [] as Array<{ label: string; value: string }>,
|
transferUnitOptions: [] as Array<{ label: string; value: string }>,
|
||||||
|
isMounted: false,
|
||||||
|
pendingTransferUnitChange: false,
|
||||||
|
transferUnitChangeOldQty: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -346,6 +350,66 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
|
'row.quantity': {
|
||||||
|
async handler(newQuantity, oldQuantity) {
|
||||||
|
if (
|
||||||
|
this.hasSerialNumber &&
|
||||||
|
newQuantity &&
|
||||||
|
newQuantity > 0 &&
|
||||||
|
this.isMounted &&
|
||||||
|
newQuantity !== oldQuantity
|
||||||
|
) {
|
||||||
|
await this.fetchSerialNumbers(false, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: false,
|
||||||
|
},
|
||||||
|
'row.transferQuantity': {
|
||||||
|
async handler(newTransferQuantity, oldTransferQuantity) {
|
||||||
|
if (
|
||||||
|
this.pendingTransferUnitChange &&
|
||||||
|
newTransferQuantity !== this.transferUnitChangeOldQty
|
||||||
|
) {
|
||||||
|
this.pendingTransferUnitChange = false;
|
||||||
|
this.transferUnitChangeOldQty = 0;
|
||||||
|
|
||||||
|
await this.fetchSerialNumbers(true, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isUOMConversionEnabled &&
|
||||||
|
this.hasSerialNumber &&
|
||||||
|
newTransferQuantity &&
|
||||||
|
newTransferQuantity > 0 &&
|
||||||
|
this.isMounted &&
|
||||||
|
newTransferQuantity !== oldTransferQuantity &&
|
||||||
|
!this.pendingTransferUnitChange
|
||||||
|
) {
|
||||||
|
await this.fetchSerialNumbers(false, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: false,
|
||||||
|
},
|
||||||
|
'row.transferUnit': {
|
||||||
|
async handler(newTransferUnit, oldTransferUnit) {
|
||||||
|
if (
|
||||||
|
this.isUOMConversionEnabled &&
|
||||||
|
this.hasSerialNumber &&
|
||||||
|
newTransferUnit &&
|
||||||
|
oldTransferUnit &&
|
||||||
|
newTransferUnit !== oldTransferUnit &&
|
||||||
|
this.isMounted
|
||||||
|
) {
|
||||||
|
delete this.itemSerialNumbers[this.row.item as string];
|
||||||
|
await this.row.set('serialNumber', '');
|
||||||
|
|
||||||
|
this.pendingTransferUnitChange = true;
|
||||||
|
this.transferUnitChangeOldQty = this.row.transferQuantity ?? 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isUOMConversionEnabled(): boolean {
|
isUOMConversionEnabled(): boolean {
|
||||||
@@ -391,6 +455,14 @@ export default defineComponent({
|
|||||||
this.profileRateSetting = !!this.fyo.singles.POSSettings?.canChangeRate;
|
this.profileRateSetting = !!this.fyo.singles.POSSettings?.canChangeRate;
|
||||||
this.itemVisibility = await getItemVisibility(this.fyo);
|
this.itemVisibility = await getItemVisibility(this.fyo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
this.isMounted = true;
|
||||||
|
|
||||||
|
if (this.hasSerialNumber) {
|
||||||
|
await this.fetchSerialNumbers();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -483,6 +555,8 @@ export default defineComponent({
|
|||||||
if (!serialNumber) {
|
if (!serialNumber) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.row.set('serialNumber', serialNumber);
|
||||||
this.itemSerialNumbers[this.row.item as string] = serialNumber;
|
this.itemSerialNumbers[this.row.item as string] = serialNumber;
|
||||||
|
|
||||||
validateSerialNumberCount(
|
validateSerialNumberCount(
|
||||||
@@ -491,6 +565,52 @@ export default defineComponent({
|
|||||||
this.row.item!
|
this.row.item!
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
async fetchSerialNumbers(forceRefetch = false, useDirectQuantity = false) {
|
||||||
|
if (!this.hasSerialNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quantity = 0;
|
||||||
|
if (useDirectQuantity) {
|
||||||
|
quantity = Math.abs(this.row.quantity ?? 0);
|
||||||
|
} else if (this.isUOMConversionEnabled && this.row.transferQuantity) {
|
||||||
|
quantity = Math.abs(this.row.transferQuantity);
|
||||||
|
} else if (this.row.quantity) {
|
||||||
|
quantity = Math.abs(this.row.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantity <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingSerialNumbers =
|
||||||
|
this.itemSerialNumbers[this.row.item as string];
|
||||||
|
|
||||||
|
if (existingSerialNumbers && !forceRefetch) {
|
||||||
|
const existingCount = existingSerialNumbers
|
||||||
|
.split('\n')
|
||||||
|
.filter((s) => s.trim()).length;
|
||||||
|
|
||||||
|
if (existingCount === quantity) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serialNumbers = await getExistingActiveSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
this.row.item as string,
|
||||||
|
quantity
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
await this.row.set('serialNumber', serialNumbers);
|
||||||
|
this.itemSerialNumbers[this.row.item as string] = serialNumbers;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
isRateReadOnly() {
|
isRateReadOnly() {
|
||||||
const canChangeRate = this.profileRateSetting;
|
const canChangeRate = this.profileRateSetting;
|
||||||
return this.row.isFreeItem || !canChangeRate;
|
return this.row.isFreeItem || !canChangeRate;
|
||||||
@@ -514,6 +634,7 @@ export default defineComponent({
|
|||||||
!hasManualDiscount && this.row.itemDiscountPercent !== 0;
|
!hasManualDiscount && this.row.itemDiscountPercent !== 0;
|
||||||
const manualDiscountAmount = this.row.itemDiscountAmount;
|
const manualDiscountAmount = this.row.itemDiscountAmount;
|
||||||
const manualDiscountPercent = this.row.itemDiscountPercent;
|
const manualDiscountPercent = this.row.itemDiscountPercent;
|
||||||
|
|
||||||
if (!this.row.isReturn && quantity <= 0) {
|
if (!this.row.isReturn && quantity <= 0) {
|
||||||
showToast({
|
showToast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
+105
-25
@@ -173,6 +173,7 @@ import {
|
|||||||
ItemSerialNumbers,
|
ItemSerialNumbers,
|
||||||
} from 'src/components/POS/types';
|
} from 'src/components/POS/types';
|
||||||
import { ValidationError } from 'fyo/utils/errors';
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
|
import { getExistingActiveSerialNumbersForItem } from 'models/inventory/helpers';
|
||||||
|
|
||||||
const COMPONENT_NAME = 'POS';
|
const COMPONENT_NAME = 'POS';
|
||||||
|
|
||||||
@@ -852,26 +853,27 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemName = item.name;
|
||||||
|
|
||||||
if (item.hasBatch) {
|
if (item.hasBatch) {
|
||||||
this.selectedItemForBatch = item.name;
|
this.selectedItemForBatch = itemName;
|
||||||
this.pendingBatchItem = { item, quantity: quantity ?? 1 };
|
this.pendingBatchItem = { item, quantity: quantity ?? 1 };
|
||||||
|
|
||||||
this.toggleModal('BatchSelection', true);
|
this.toggleModal('BatchSelection', true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInventoryItem = await this.fyo.getValue(
|
const isInventoryItem = await this.fyo.getValue(
|
||||||
ModelNameEnum.Item,
|
ModelNameEnum.Item,
|
||||||
item.name,
|
itemName,
|
||||||
'trackItem'
|
'trackItem'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isInventoryItem) {
|
if (isInventoryItem) {
|
||||||
const availableQty = this.itemQtyMap[item.name]?.availableQty ?? 0;
|
const availableQty = this.itemQtyMap[itemName]?.availableQty ?? 0;
|
||||||
if (availableQty <= 0) {
|
if (availableQty <= 0) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
t`Item is out of stock (quantity is zero)`
|
t`Item ${itemName} is out of stock (quantity is zero)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -879,7 +881,7 @@ export default defineComponent({
|
|||||||
const existingItems =
|
const existingItems =
|
||||||
this.sinvDoc.items?.filter(
|
this.sinvDoc.items?.filter(
|
||||||
(invoiceItem) =>
|
(invoiceItem) =>
|
||||||
invoiceItem.item === item.name && !invoiceItem.isFreeItem
|
invoiceItem.item === itemName && !invoiceItem.isFreeItem
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
await validateQty(
|
await validateQty(
|
||||||
@@ -890,7 +892,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const itemsHsncode = (await this.fyo.getValue(
|
const itemsHsncode = (await this.fyo.getValue(
|
||||||
'Item',
|
'Item',
|
||||||
item?.name,
|
itemName,
|
||||||
'hsnCode'
|
'hsnCode'
|
||||||
)) as number;
|
)) as number;
|
||||||
|
|
||||||
@@ -898,21 +900,38 @@ export default defineComponent({
|
|||||||
const addQty = quantity ?? 1;
|
const addQty = quantity ?? 1;
|
||||||
|
|
||||||
if (existingItems.length > 0) {
|
if (existingItems.length > 0) {
|
||||||
for (let item of existingItems) {
|
for (let existingItem of existingItems) {
|
||||||
const availableQty = await fyo.db.getStockQuantity(
|
const availableQty = await this.fyo.db.getStockQuantity(
|
||||||
item.item as string,
|
existingItem.item as string,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
item.batch
|
existingItem.batch
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
item.batch != null &&
|
existingItem.batch != null &&
|
||||||
availableQty != null &&
|
availableQty != null &&
|
||||||
availableQty > (item.quantity as number)
|
availableQty > (existingItem.quantity as number)
|
||||||
) {
|
) {
|
||||||
const currentQty = item.quantity ?? 0;
|
const currentQty = existingItem.quantity ?? 0;
|
||||||
await item.set('quantity', currentQty + addQty);
|
await existingItem.set('quantity', currentQty + addQty);
|
||||||
|
|
||||||
|
if (item.hasSerialNumber) {
|
||||||
|
const qty = currentQty + addQty;
|
||||||
|
|
||||||
|
const serialNumbers =
|
||||||
|
await getExistingActiveSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
itemName,
|
||||||
|
qty
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
this.itemSerialNumbers[itemName] = serialNumbers;
|
||||||
|
await existingItem.set('serialNumber', serialNumbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.applyPricingRule();
|
await this.applyPricingRule();
|
||||||
await this.sinvDoc.runFormulas();
|
await this.sinvDoc.runFormulas();
|
||||||
return;
|
return;
|
||||||
@@ -922,10 +941,32 @@ export default defineComponent({
|
|||||||
|
|
||||||
await this.sinvDoc.append('items', {
|
await this.sinvDoc.append('items', {
|
||||||
rate: item.rate,
|
rate: item.rate,
|
||||||
item: item.name,
|
item: itemName,
|
||||||
quantity: addQty,
|
quantity: addQty,
|
||||||
hsnCode: itemsHsncode,
|
hsnCode: itemsHsncode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (item.hasSerialNumber) {
|
||||||
|
const serialNumbers = await getExistingActiveSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
itemName,
|
||||||
|
addQty
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
this.itemSerialNumbers[itemName] = serialNumbers;
|
||||||
|
|
||||||
|
const newItemRows = this.sinvDoc.items?.filter(
|
||||||
|
(row) => row.item === itemName && !row.isFreeItem
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newItemRows && newItemRows.length > 0) {
|
||||||
|
const newRow = newItemRows[newItemRows.length - 1];
|
||||||
|
await newRow.set('serialNumber', serialNumbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.applyPricingRule();
|
await this.applyPricingRule();
|
||||||
await this.sinvDoc.runFormulas();
|
await this.sinvDoc.runFormulas();
|
||||||
return;
|
return;
|
||||||
@@ -939,15 +980,30 @@ export default defineComponent({
|
|||||||
const currentQty = existingItems[0].quantity ?? 0;
|
const currentQty = existingItems[0].quantity ?? 0;
|
||||||
const addQty = quantity ?? 1;
|
const addQty = quantity ?? 1;
|
||||||
if (isInventoryItem) {
|
if (isInventoryItem) {
|
||||||
const availableQty = this.itemQtyMap[item.name]?.availableQty ?? 0;
|
const availableQty = this.itemQtyMap[itemName]?.availableQty ?? 0;
|
||||||
if (currentQty + addQty > availableQty) {
|
if (currentQty + addQty > availableQty) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
'Cannot add more than the available quantity'
|
`Cannot add more than the available quantity for ${itemName}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await existingItems[0].set('quantity', currentQty + addQty);
|
await existingItems[0].set('quantity', currentQty + addQty);
|
||||||
|
if (item.hasSerialNumber) {
|
||||||
|
const qty = currentQty + addQty;
|
||||||
|
|
||||||
|
const serialNumbers = await getExistingActiveSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
itemName,
|
||||||
|
qty
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
this.itemSerialNumbers[itemName] = serialNumbers;
|
||||||
|
await existingItems[0].set('serialNumber', serialNumbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.applyPricingRule();
|
await this.applyPricingRule();
|
||||||
await this.sinvDoc.runFormulas();
|
await this.sinvDoc.runFormulas();
|
||||||
if (isInventoryItem) {
|
if (isInventoryItem) {
|
||||||
@@ -962,24 +1018,48 @@ export default defineComponent({
|
|||||||
|
|
||||||
await this.sinvDoc.append('items', {
|
await this.sinvDoc.append('items', {
|
||||||
rate: item.rate,
|
rate: item.rate,
|
||||||
item: item.name,
|
item: itemName,
|
||||||
quantity: quantity ? quantity : 1,
|
quantity: quantity ? quantity : 1,
|
||||||
hsnCode: itemsHsncode,
|
hsnCode: itemsHsncode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.sinvDoc.priceList) {
|
if (this.sinvDoc.priceList) {
|
||||||
let itemData = this.sinvDoc.items?.filter(
|
const itemData = this.sinvDoc.items?.filter(
|
||||||
(val) => val.item == item.name
|
(val) => val.item == itemName
|
||||||
) as SalesInvoiceItem[];
|
) as SalesInvoiceItem[];
|
||||||
|
|
||||||
itemData[0].rate = await getItemRateFromPriceList(
|
if (itemData.length > 0) {
|
||||||
itemData[0],
|
itemData[0].rate = await getItemRateFromPriceList(
|
||||||
this.sinvDoc.priceList
|
itemData[0],
|
||||||
|
this.sinvDoc.priceList
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.hasSerialNumber) {
|
||||||
|
const qty = quantity ?? 1;
|
||||||
|
|
||||||
|
const serialNumbers = await getExistingActiveSerialNumbersForItem(
|
||||||
|
this.fyo,
|
||||||
|
itemName,
|
||||||
|
qty
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (serialNumbers) {
|
||||||
|
this.itemSerialNumbers[itemName] = serialNumbers;
|
||||||
|
|
||||||
|
const newItemRows = this.sinvDoc.items?.filter(
|
||||||
|
(row) => row.item === itemName && !row.isFreeItem
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newItemRows && newItemRows.length > 0) {
|
||||||
|
const newRow = newItemRows[newItemRows.length - 1];
|
||||||
|
await newRow.set('serialNumber', serialNumbers);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.applyPricingRule();
|
await this.applyPricingRule();
|
||||||
|
|
||||||
await this.sinvDoc.runFormulas();
|
await this.sinvDoc.runFormulas();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return showToast({
|
return showToast({
|
||||||
|
|||||||
Reference in New Issue
Block a user