Merge branch 'master' into Integrationerrorlog

This commit is contained in:
Gadha Venu
2026-02-26 13:11:45 +05:30
committed by GitHub
44 changed files with 1553 additions and 147 deletions
+24 -2
View File
@@ -46,6 +46,7 @@ import {
import { validateOptions, validateRequired } from './validationFunction';
import { getShouldDocSyncToERPNext } from 'src/utils/erpnextSync';
import { ModelNameEnum } from 'models/types';
import { DocItem } from 'models/inventory/types';
export class Doc extends Observable<DocValue | Doc[]> {
/* eslint-disable @typescript-eslint/no-floating-promises */
@@ -920,6 +921,25 @@ export class Doc extends Observable<DocValue | Doc[]> {
return this;
}
async _hasERPSyncableItems(): Promise<boolean> {
const isSalesInvoice = this.schemaName === ModelNameEnum.SalesInvoice;
if (!isSalesInvoice) {
return true;
}
for (const item of this.items as DocItem[]) {
if (!item.item) {
continue;
}
const isFromERP = await this.fyo.getValue(
ModelNameEnum.Item,
item.item,
'datafromErp'
);
if (isFromERP) continue;
else return false;
}
return true;
}
async sync(): Promise<Doc> {
this._syncing = true;
@@ -936,10 +956,12 @@ export class Doc extends Observable<DocValue | Doc[]> {
if (this._addDocToSyncQueue && !!this.shouldDocSyncToERPNext) {
const isSalesInvoice = this.schemaName === ModelNameEnum.SalesInvoice;
const hasERPSyncableItems = await this._hasERPSyncableItems();
if (
!(isSalesInvoice && this.isSyncedWithErp) ||
(isSalesInvoice && !!this.isReturn)
hasERPSyncableItems &&
(!(isSalesInvoice && this.isSyncedWithErp) ||
(isSalesInvoice && !!this.isReturn))
) {
if (isSalesInvoice && !this.isReturn) {
await this.setAndSync('isSyncedWithErp', true);
+69
View File
@@ -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 BatchSeries 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,
};
}
+2
View File
@@ -1,4 +1,5 @@
import { ModelMap } from 'fyo/model/types';
import BatchSeries from './BatchSeries';
import NumberSeries from './NumberSeries';
import SerialNumberSeries from './SerialNumberSeries';
import SystemSettings from './SystemSettings';
@@ -6,6 +7,7 @@ import { CustomField } from './CustomField';
import { CustomForm } from './CustomForm';
export const coreModels = {
BatchSeries,
NumberSeries,
SerialNumberSeries,
SystemSettings,
+45
View File
@@ -0,0 +1,45 @@
import { DatabaseManager } from '../backend/database/manager';
import { ModelNameEnum } from '../models/types';
export async function checkLoyaltyProgramExpiry() {
const dm = new DatabaseManager();
try {
const currentDate = new Date();
const loyaltyPrograms = await dm.db?.getAll(ModelNameEnum.LoyaltyProgram, {
fields: ['name', 'toDate', 'status', 'isEnabled', 'maximumUse', 'used'],
filters: {
status: ['not in', ['Expired']],
isEnabled: true,
},
});
if (loyaltyPrograms) {
for (const program of loyaltyPrograms) {
if (program.toDate && new Date(String(program.toDate)) <= currentDate) {
await dm.db?.knex!(ModelNameEnum.LoyaltyProgram)
.where({ name: program.name })
.update({
status: 'Expired',
isEnabled: false,
});
}
}
}
const result = {
timestamp: currentDate.toISOString(),
};
return result;
} catch (error) {
throw error;
} finally {
await dm.call('close');
}
}
checkLoyaltyProgramExpiry().catch((error) => {
throw error;
});
+9
View File
@@ -24,6 +24,15 @@ export async function initScheduler(interval: string) {
},
},
},
{
name: 'checkLoyaltyProgramExpiry',
interval: '0 1 * * *',
worker: {
workerData: {
useTsNode: true,
},
},
},
],
worker: {
argv: ['--require', 'ts-node/register'],
+182 -12
View File
@@ -28,6 +28,9 @@ import {
getReturnLoyaltyPoints,
getItemQtyMap,
getItemVisibility,
validateLoyaltyProgram,
getLoyaltyProgramTier,
isLoyaltyProgramExpiredAndMaxed,
} from 'models/helpers';
import { StockTransfer } from 'models/inventory/StockTransfer';
import { validateBatch } from 'models/inventory/helpers';
@@ -196,6 +199,23 @@ export abstract class Invoice extends Transactional {
if (this.isQuote) {
return;
}
if (!this.submitted && this.loyaltyProgram) {
const isExpiredOrMaxed = await isLoyaltyProgramExpiredAndMaxed(
this.fyo,
this.loyaltyProgram
);
if (isExpiredOrMaxed) {
const { showToast } = await import('src/utils/interactive');
showToast({
type: 'warning',
message: t`Loyalty program has expired or reached maximum usage`,
duration: 'short',
});
}
}
if (
this.enableDiscounting &&
!this.fyo.singles?.AccountingSettings?.discountAccount
@@ -212,13 +232,49 @@ export abstract class Invoice extends Transactional {
this.party
)) as Party;
if ((this.loyaltyPoints as number) > (partyDoc?.loyaltyPoints || 0)) {
if (this.redeemLoyaltyPoints && (this.loyaltyPoints as number) > 0) {
const currentPoints = partyDoc?.loyaltyPoints || 0;
let pointsToBeEarned = 0;
if (!this.isReturn && this.loyaltyProgram) {
const loyaltyProgramDoc = (await this.fyo.doc.getDoc(
ModelNameEnum.LoyaltyProgram,
this.loyaltyProgram
)) as LoyaltyProgram;
const tier = getLoyaltyProgramTier(
loyaltyProgramDoc,
this?.grandTotal as Money
);
if (tier) {
const collectionFactor = tier.collectionFactor as number;
pointsToBeEarned =
Math.round(this?.grandTotal?.float || 0) * collectionFactor;
}
}
const totalAvailablePoints = currentPoints + pointsToBeEarned;
if ((this.loyaltyPoints as number) > totalAvailablePoints) {
throw new ValidationError(
t`${
this.party as string
} only has ${currentPoints} points (${pointsToBeEarned} will be earned from this transaction)`
);
}
} else if (
(this.loyaltyPoints as number) > (partyDoc?.loyaltyPoints || 0)
) {
throw new ValidationError(
t`${this.party as string} only has ${
partyDoc.loyaltyPoints as number
} points`
);
}
if (this.loyaltyProgram) {
await validateLoyaltyProgram(this, this.loyaltyProgram);
}
}
async afterSubmit() {
@@ -275,6 +331,10 @@ export abstract class Invoice extends Transactional {
if (this.schemaName === ModelNameEnum.SalesInvoice) {
this.updateUsedCountOfCoupons();
}
if (this.loyaltyProgram) {
await this.updateUsedCountOfLoyaltyProgram();
}
}
async afterCancel() {
@@ -284,6 +344,10 @@ export abstract class Invoice extends Transactional {
await this._updateIsItemsReturned();
await this._removeLoyaltyPointEntry();
this.reduceUsedCountOfCoupons();
if (this.loyaltyProgram) {
await this.reduceUsedCountOfLoyaltyProgram();
}
}
async _removeLoyaltyPointEntry() {
@@ -712,17 +776,26 @@ export abstract class Invoice extends Transactional {
if (item.batch) {
const returnData = totalQtyOfReturnedItems[item.item as string];
if (typeof returnData === 'object' && returnData?.batches) {
returnDocItems = docItems.map((docItem) => ({
...docItem,
name: undefined,
quantity: -returnData?.batches![docItem.batch as string] || 0,
}));
returnDocItems = docItems.map((docItem: DocValueMap) => {
const qty = -returnData?.batches![docItem.batch as string] || 0;
const transferQty =
qty / ((docItem.unitConversionFactor as number) || 1);
return {
...docItem,
name: undefined,
quantity: qty,
transferQuantity: transferQty,
};
});
}
} else {
returnDocItems = docItems.map((docItem) => ({
returnDocItems = docItems.map((docItem: DocValueMap) => ({
...docItem,
name: undefined,
quantity: -(totalQtyOfReturnedItems[docItem.item as string] || 0),
qty:
-(totalQtyOfReturnedItems[docItem.item as string] as number) /
(item.unitConversionFactor as number),
transferQuantity: -(
(totalQtyOfReturnedItems[docItem.item as string] as number) /
(item.unitConversionFactor as number)
@@ -788,6 +861,7 @@ export abstract class Invoice extends Transactional {
serialNumber,
name: undefined,
quantity: quantity,
qty: transferQuantity,
transferQuantity,
});
}
@@ -838,6 +912,63 @@ export abstract class Invoice extends Transactional {
});
}
async updateUsedCountOfLoyaltyProgram() {
if (!this.loyaltyProgram) {
return;
}
const loyaltyProgramDoc = await this.fyo.doc.getDoc(
ModelNameEnum.LoyaltyProgram,
this.loyaltyProgram
);
const maximumUse = loyaltyProgramDoc.maximumUse as number;
const used = (loyaltyProgramDoc.used as number) || 0;
if (this.redeemLoyaltyPoints) {
const newUsedCount = used + 1;
if (maximumUse > 0 && newUsedCount >= maximumUse) {
await loyaltyProgramDoc.setAndSync({
used: newUsedCount,
isEnabled: false,
});
} else {
await loyaltyProgramDoc.setAndSync({
used: newUsedCount,
});
}
}
}
async reduceUsedCountOfLoyaltyProgram() {
if (!this.loyaltyProgram) {
return;
}
const loyaltyProgramDoc = await this.fyo.doc.getDoc(
ModelNameEnum.LoyaltyProgram,
this.loyaltyProgram
);
const maximumUse = loyaltyProgramDoc.maximumUse as number;
const used = (loyaltyProgramDoc.used as number) || 0;
const newUsedCount = used - 1;
if (this.redeemLoyaltyPoints) {
if (newUsedCount < maximumUse) {
await loyaltyProgramDoc.setAndSync({
used: newUsedCount,
isEnabled: true,
});
} else {
await loyaltyProgramDoc.setAndSync({
used: newUsedCount,
});
}
}
}
async updateIsItemsFullyReturned(doc?: Invoice) {
if (!doc?.returnAgainst || doc.schemaName !== ModelNameEnum.SalesInvoice) {
return;
@@ -898,11 +1029,27 @@ export abstract class Invoice extends Transactional {
this.loyaltyProgram
)) as LoyaltyProgram;
const expiryDate = this.date as Date;
const invoiceDate = this.date as Date;
const fromDate = loyaltyProgramDoc.fromDate as Date;
const toDate = loyaltyProgramDoc.toDate as Date;
if (fromDate <= expiryDate && toDate >= expiryDate) {
const normalizedInvoiceDate = new Date(invoiceDate);
normalizedInvoiceDate.setHours(0, 0, 0, 0);
const normalizedFromDate = new Date(fromDate);
normalizedFromDate.setHours(0, 0, 0, 0);
const normalizedToDate = new Date(toDate);
normalizedToDate.setHours(0, 0, 0, 0);
if (normalizedToDate.getTime() < normalizedInvoiceDate.getTime()) {
return;
}
if (
normalizedInvoiceDate.getTime() >= normalizedFromDate.getTime() &&
normalizedInvoiceDate.getTime() <= normalizedToDate.getTime()
) {
const party = (await this.loadAndGetLink('party')) as Party;
await createLoyaltyPointEntry(this);
@@ -989,7 +1136,13 @@ export abstract class Invoice extends Transactional {
ModelNameEnum.Party,
this.party
);
return partyDoc?.loyaltyProgram as string;
const loyaltyProgramName = partyDoc?.loyaltyProgram as string;
if (!loyaltyProgramName) {
return '';
}
return loyaltyProgramName;
},
dependsOn: ['party', 'name'],
},
@@ -999,6 +1152,17 @@ export abstract class Invoice extends Transactional {
return 0;
}
const loyaltyProgramName = this.loyaltyProgram as string;
if (loyaltyProgramName) {
const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed(
this.fyo,
loyaltyProgramName
);
if (isExpiredAndMaxed) {
return 0;
}
}
const loyaltyPoints = await this.fyo.getValue(
ModelNameEnum.Party,
this.party,
@@ -1006,7 +1170,7 @@ export abstract class Invoice extends Transactional {
);
return loyaltyPoints || 0;
},
dependsOn: ['party'],
dependsOn: ['party', 'loyaltyProgram'],
},
currency: {
formula: async () => {
@@ -1199,7 +1363,13 @@ export abstract class Invoice extends Transactional {
loyaltyProgram: () => !this.loyaltyProgram,
availableLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn,
loyaltyPoints: () => !this.redeemLoyaltyPoints || this.isReturn,
redeemLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn,
redeemLoyaltyPoints: () => {
if (!this.loyaltyProgram || this.isReturn) {
return true;
}
return (this.availableLoyaltyPoints ?? 0) <= 0;
},
coupons: () => this.isSubmitted && !this.coupons?.length,
priceList: () =>
!this.fyo.singles.AccountingSettings?.enablePriceList ||
+243 -12
View File
@@ -3,6 +3,7 @@ import { DocValue, DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import {
CurrenciesMap,
ChangeArg,
FiltersMap,
FormulaMap,
HiddenMap,
@@ -19,8 +20,20 @@ import { Item } from '../Item/Item';
import { StockTransfer } from 'models/inventory/StockTransfer';
import { isPesa } from 'fyo/utils';
import { PricingRule } from '../PricingRule/PricingRule';
import { getItemRateFromPriceList, getPricingRule } from 'models/helpers';
import {
getItemRateFromPriceList,
getPricingRule,
getItemVisibility,
} from 'models/helpers';
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
import { getSuggestedBatchName } from 'models/inventory/helpers';
import { ValuationMethod } from 'models/inventory/types';
import {
getRawStockLedgerEntries,
getStockLedgerEntries,
getStockBalanceEntries,
} from 'reports/inventory/helpers';
import { QueryFilter } from 'utils/db/types';
export abstract class InvoiceItem extends Doc {
item?: string;
@@ -107,6 +120,27 @@ export abstract class InvoiceItem extends Doc {
this._setGetCurrencies();
}
override async change(ch: ChangeArg): Promise<void> {
await super.change(ch);
if (ch.changed === 'item') {
if (!this.isSales && this.item) {
const hasBatch = await this.fyo.getValue(
ModelNameEnum.Item,
this.item,
'hasBatch'
);
if (hasBatch) {
const batchName = await getSuggestedBatchName(this.fyo, this.item);
if (batchName) {
await this.set('batch', batchName);
}
}
}
}
}
async getTotalTaxRate(): Promise<number> {
if (!this.tax) {
return 0;
@@ -606,15 +640,115 @@ export abstract class InvoiceItem extends Doc {
filters: { uom: value as string, parent: this.item },
});
if (item.length < 1)
if (item.length < 1) {
throw new ValidationError(
t`Transfer Unit ${value as string} is not applicable for Item ${
this.item
}`
);
}
},
qty: async (value: DocValue) => {
const requiredQuantity = Math.abs(value as number);
if (!this.item || requiredQuantity <= 0) {
return;
}
if (!this.isSales) {
return;
}
if (!this.fyo.singles.InventorySettings?.enableBatches) {
return;
}
if (!this.batch) {
return;
}
await this.validateBatchQuantity(this.batch, requiredQuantity);
},
batch: async (value: DocValue) => {
if (!value || !this.item) {
return;
}
if (!this.isSales) {
return;
}
if (!this.fyo.singles.InventorySettings?.enableBatches) {
return;
}
const requiredQuantity = this.quantity ?? 0;
if (requiredQuantity > 0) {
await this.validateBatchQuantity(value as string, requiredQuantity);
} else if (requiredQuantity < 0) {
await this.validateBatchQuantity(
value as string,
Math.abs(requiredQuantity)
);
}
},
};
async validateBatchQuantity(
batchName: string,
requiredQuantity: number
): Promise<void> {
let inventoryLocation: string | undefined;
if (this.location) {
inventoryLocation = this.location as string;
} else {
const posProfileName = this.fyo.singles.POSSettings?.posProfile;
if (posProfileName) {
const inventory = await this.fyo.getValue(
ModelNameEnum.POSProfile,
posProfileName as string,
'inventory'
);
inventoryLocation = inventory as string | undefined;
} else {
inventoryLocation = this.fyo.singles.POSSettings?.inventory;
}
}
const valuationMethod =
(this.fyo.singles.InventorySettings
?.valuationMethod as ValuationMethod) ?? ValuationMethod.FIFO;
const rawSLEs = await getRawStockLedgerEntries(this.fyo);
const computedSLEs = getStockLedgerEntries(rawSLEs, valuationMethod);
const stockBalance = getStockBalanceEntries(computedSLEs, {
item: this.item!,
location: inventoryLocation,
batch: batchName,
});
const availableQuantity = stockBalance.reduce(
(sum, entry) => sum + (entry.balanceQuantity || 0),
0
);
if (requiredQuantity > availableQuantity) {
throw new ValidationError(
this.fyo.t`
Batch ${batchName} only has ${availableQuantity} quantity available
but ${requiredQuantity} is required
`
);
}
}
hidden: HiddenMap = {
itemDiscountedTotal: () => {
if (!this.enableDiscounting) {
@@ -646,24 +780,121 @@ export abstract class InvoiceItem extends Doc {
};
static filters: FiltersMap = {
item: (doc: Doc) => {
item: async (doc: Doc): Promise<QueryFilter> => {
let itemNotFor = 'Sales';
if (doc.isSales) {
itemNotFor = 'Purchases';
}
return { for: ['not in', [itemNotFor]] };
const filters: QueryFilter = {
for: ['not in', [itemNotFor]],
};
const enableERPNextSync =
doc.fyo.singles.AccountingSettings?.enableERPNextSync;
if (enableERPNextSync) {
const itemVisibility = await getItemVisibility(doc.fyo);
if (itemVisibility === 'Inventory Items') {
filters.trackItem = true;
} else if (itemVisibility === 'ERP Sync Items') {
filters.datafromErp = true;
} else if (itemVisibility === 'Non-Inventory Items') {
filters.trackItem = false;
filters.datafromErp = false;
}
}
return filters;
},
batch: async (doc: Doc) => {
const batches = await doc.fyo.db.getAll(ModelNameEnum.Batch, {
fields: ['name'],
filters: { item: doc.item as string },
});
const batchName = batches.map((b) => b.name) as string[];
const hasBatch = !!(await doc.fyo.getValue(
ModelNameEnum.Item,
doc.item as string,
'hasBatch'
));
return {
name: ['in', batchName],
};
if (!hasBatch) {
return { name: ['in', []] };
}
let suggestedBatch: string | undefined;
if (!doc.isSales) {
suggestedBatch = await getSuggestedBatchName(
doc.fyo,
doc.item as string
);
if (suggestedBatch) {
await doc.set('batch', suggestedBatch);
}
}
try {
let inventoryLocation: string | undefined;
if (doc.location) {
inventoryLocation = doc.location as string;
} else {
const posProfileName = doc.fyo.singles.POSSettings?.posProfile;
if (posProfileName) {
const posProfile = await doc.fyo.doc.getDoc(
ModelNameEnum.POSProfile,
posProfileName as string
);
inventoryLocation = posProfile?.inventory as string | undefined;
} else {
inventoryLocation = doc.fyo.singles.POSSettings?.inventory;
}
}
const rawSLEs = await getRawStockLedgerEntries(doc.fyo);
const valuationMethod =
(doc.fyo.singles.InventorySettings
?.valuationMethod as ValuationMethod) ?? ValuationMethod.FIFO;
const computedSLEs = getStockLedgerEntries(rawSLEs, valuationMethod);
const stockBalance = getStockBalanceEntries(computedSLEs, {
item: doc.item as string,
location: inventoryLocation,
});
const batchesWithStock = stockBalance
.filter((entry) => entry.batch && entry.balanceQuantity > 0)
.map((entry) => entry.batch);
const allBatches = new Set<string>(batchesWithStock);
if (suggestedBatch) {
allBatches.add(suggestedBatch);
}
const finalBatchList = Array.from(allBatches);
return {
name: ['in', finalBatchList],
};
} catch (error) {
const batches = await doc.fyo.db.getAll(ModelNameEnum.Batch, {
fields: ['name'],
filters: { item: doc.item as string },
});
const batchNames = batches.map((b) => b.name) as string[];
const allBatches = new Set<string>(batchNames);
if (suggestedBatch) {
allBatches.add(suggestedBatch);
}
const finalBatchList = Array.from(allBatches);
return {
name: ['in', finalBatchList],
};
}
},
transferUnit: async (doc: Doc) => {
const conversionItems = await doc.fyo.db.getAll(
+46
View File
@@ -26,10 +26,12 @@ export class Item extends Doc {
itemType?: 'Product' | 'Service';
for?: 'Purchases' | 'Sales' | 'Both';
hasBatch?: boolean;
batchSeries?: string;
itemGroup?: string;
hsnCode?: number;
hasSerialNumber?: boolean;
serialNumberSeries?: string;
datafromErp?: boolean;
uomConversions: UOMConversionItem[] = [];
formulas: FormulaMap = {
@@ -100,6 +102,13 @@ export class Item extends Doc {
this.serialNumberSeries = series + '-';
}
}
if (this.batchSeries && this.hasBatch) {
const series = this.batchSeries.trim();
if (series && !series.endsWith('-')) {
this.batchSeries = series + '-';
}
}
}
async afterSync(): Promise<void> {
@@ -125,6 +134,27 @@ export class Item extends Doc {
.sync();
}
}
if (this.hasBatch && this.batchSeries) {
const seriesName = this.batchSeries?.trim();
if (!seriesName) {
return;
}
const exists = await this.fyo.db.exists('BatchSeries', seriesName);
if (!exists) {
await this.fyo.doc
.getNewDoc('BatchSeries', {
name: seriesName,
start: 1001,
padZeros: 4,
current: 1001,
})
.sync();
}
}
}
static filters: FiltersMap = {
@@ -173,6 +203,21 @@ export class Item extends Doc {
);
}
},
batchSeries: (value: DocValue) => {
if (!value) {
return;
}
const series = (value as string).trim();
const invalidChars = /[/\=\?\&\%]/;
if (invalidChars.test(series)) {
throw new ValidationError(
this.fyo
.t`Batch Series cannot contain the following characters: /, ?, &, =, %`
);
}
},
};
static getActions(fyo: Fyo): Action[] {
@@ -226,6 +271,7 @@ export class Item extends Doc {
this.fyo.singles.InventorySettings?.enableSerialNumber && this.trackItem
),
serialNumberSeries: () => !this.hasSerialNumber,
batchSeries: () => !this.hasBatch,
uomConversions: () =>
!this.fyo.singles.InventorySettings?.enableUomConversions,
itemGroup: () => !this.fyo.singles.AccountingSettings?.enableitemGroup,
@@ -1,22 +1,42 @@
import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { FiltersMap, ListViewSettings } from 'fyo/model/types';
import { ListViewSettings, ValidationMap } from 'fyo/model/types';
import { ValidationError } from 'fyo/utils/errors';
import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems';
import { AccountRootTypeEnum } from '../Account/types';
import { getLoyaltyProgramStatusColumn } from '../../helpers';
export class LoyaltyProgram extends Doc {
collectionRules?: CollectionRulesItems[];
expiryDuration?: number;
maximumUse?: number;
used?: number;
status?: 'Active' | 'Expired' | 'Disabled' | 'Maxed';
static filters: FiltersMap = {
expenseAccount: () => ({
rootType: AccountRootTypeEnum.Expense,
isGroup: false,
}),
validations: ValidationMap = {
used: (value: DocValue) => {
const used = value as number;
const maximumUse = this.maximumUse as number;
if (used < 0) {
throw new ValidationError('Used count cannot be negative');
}
if (maximumUse > 0 && used > maximumUse) {
throw new ValidationError('Used count cannot exceed maximum use limit');
}
},
maximumUse: (value: DocValue) => {
const maximumUse = value as number;
if (maximumUse < 0) {
throw new ValidationError('Maximum use cannot be negative');
}
},
};
static getListViewSettings(): ListViewSettings {
return {
columns: ['name', 'fromDate', 'toDate', 'expiryDuration'],
columns: ['name', getLoyaltyProgramStatusColumn(), 'fromDate', 'toDate'],
};
}
}
+12
View File
@@ -14,6 +14,7 @@ import {
import { Money } from 'pesa';
import { PartyRole } from './types';
import { ModelNameEnum } from 'models/types';
import { isLoyaltyProgramExpiredAndMaxed } from 'models/helpers';
export class Party extends Doc {
role?: PartyRole;
@@ -66,6 +67,17 @@ export class Party extends Doc {
}
async _getTotalLoyaltyPoints() {
const loyaltyProgramName = this.loyaltyProgram as string;
if (loyaltyProgramName) {
const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed(
this.fyo,
loyaltyProgramName
);
if (isExpiredAndMaxed) {
return 0;
}
}
const data = (await this.fyo.db.getAll(ModelNameEnum.LoyaltyPointEntry, {
fields: ['name', 'loyaltyPoints', 'expiryDate', 'postingDate'],
filters: {
+8 -6
View File
@@ -461,7 +461,10 @@ export class Payment extends Transactional {
);
const previousOutstandingAmount = referenceDoc.outstandingAmount as Money;
const outstandingAmount = previousOutstandingAmount.sub(row.amount!);
const isReturnInvoice = (referenceDoc as Invoice).isReturn;
const outstandingAmount = isReturnInvoice
? previousOutstandingAmount.add(row.amount!)
: previousOutstandingAmount.sub(row.amount!);
await referenceDoc.setAndSync({ outstandingAmount });
}
}
@@ -505,11 +508,10 @@ export class Payment extends Transactional {
ref.referenceType!,
ref.referenceName
);
const outstandingAmount = (refDoc.outstandingAmount as Money).add(
ref.amount!
);
const isReturnInvoice = (refDoc as Invoice).isReturn;
const outstandingAmount = isReturnInvoice
? (refDoc.outstandingAmount as Money).sub(ref.amount!)
: (refDoc.outstandingAmount as Money).add(ref.amount!);
await refDoc.setAndSync({ outstandingAmount });
}
}
@@ -5,10 +5,44 @@ import { ModelNameEnum } from 'models/types';
import { getInvoiceActions, getTransactionStatusColumn } from '../../helpers';
import { Invoice } from '../Invoice/Invoice';
import { PurchaseInvoiceItem } from '../PurchaseInvoiceItem/PurchaseInvoiceItem';
import { createBatch } from 'models/inventory/helpers';
export class PurchaseInvoice extends Invoice {
items?: PurchaseInvoiceItem[];
async beforeSubmit(): Promise<void> {
await super.beforeSubmit();
if (this.isReturn) {
return;
}
const batchesToCreate: { item: string; batch: string }[] = [];
for (const item of this.items ?? []) {
if (!item.item || !item.batch) {
continue;
}
const hasBatch = await this.fyo.getValue(
ModelNameEnum.Item,
item.item,
'hasBatch'
);
if (hasBatch) {
batchesToCreate.push({
item: item.item,
batch: item.batch,
});
}
}
for (const { item, batch } of batchesToCreate) {
await createBatch(this.fyo, item, batch);
}
}
async getPosting() {
const exchangeRate = this.exchangeRate ?? 1;
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
+11 -6
View File
@@ -43,12 +43,12 @@ export class SalesInvoice extends Invoice {
this.loyaltyProgram
)) as LoyaltyProgram;
let totalAmount;
let loyaltyAmount;
if (this.isReturn) {
totalAmount = this.fyo.pesa(await getReturnLoyaltyPoints(this));
loyaltyAmount = this.fyo.pesa(await getReturnLoyaltyPoints(this));
} else {
totalAmount = await getAddedLPWithGrandTotal(
loyaltyAmount = await getAddedLPWithGrandTotal(
this.fyo,
this.loyaltyProgram as string,
this.loyaltyPoints as number
@@ -57,10 +57,8 @@ export class SalesInvoice extends Invoice {
await posting.debit(
loyaltyProgramDoc.expenseAccount as string,
totalAmount
loyaltyAmount
);
await posting.credit(this.account!, totalAmount);
}
if (this.taxes) {
@@ -115,6 +113,13 @@ export class SalesInvoice extends Invoice {
ModelNameEnum.LoyaltyProgram,
this.loyaltyProgram
)) as LoyaltyProgram;
const toDate = loyaltyProgramDoc?.toDate as Date;
const today = new Date();
today.setHours(0, 0, 0, 0);
if (toDate && new Date(toDate).getTime() < today.getTime()) {
return;
}
if (!this?.grandTotal) {
return;
@@ -27,10 +27,17 @@ const partyData = {
email: 'john@whoe.com',
};
const today = new Date();
const fromDate = new Date(today);
fromDate.setDate(today.getDate() - 10);
const toDate = new Date(today);
toDate.setDate(today.getDate() + 20);
const loyaltyProgramData = {
name: 'program',
fromDate: new Date('12/10/2024'),
toDate: new Date('12/30/2024'),
fromDate: fromDate,
toDate: toDate,
email: 'sample@gmail.com',
mobile: '1234567890',
expenseAccount: accountData.name,
@@ -118,11 +125,11 @@ async function loyaltyPointEntryDoc(sinvName: string) {
}
}
async function createSalesInvoice() {
async function createSalesInvoice(invoiceDate?: Date) {
const sinvDoc = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
account: 'Debtors',
party: partyData.name,
date: new Date('12/11/2024'),
date: invoiceDate || new Date(),
items: [
{
item: itemData.name,
@@ -189,10 +196,10 @@ test('create Sales Invoice and verify loyalty points are created correctly', asy
});
test('create SINV with future date and verify loyalty points are not created', async (t) => {
const futureDate = new Date(new Date().setDate(new Date().getDate() + 20));
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 30);
const sinvDoc = await createSalesInvoice();
sinvDoc.date = futureDate;
const sinvDoc = await createSalesInvoice(futureDate);
await sinvDoc.sync();
await sinvDoc.submit();
+176 -10
View File
@@ -48,7 +48,10 @@ import {
getStockLedgerEntries,
} from 'reports/inventory/helpers';
import { LoyaltyPointEntry } from './baseModels/LoyaltyPointEntry/LoyaltyPointEntry';
import { generateSerialNumbersForItem } from './inventory/helpers';
import {
generateSerialNumbersForItem,
generateBatchForItem,
} from './inventory/helpers';
export function getQuoteActions(
fyo: Fyo,
@@ -116,6 +119,11 @@ export async function getItemQtyMap(doc: SalesInvoice): Promise<ItemQtyMap> {
export async function getItemVisibility(fyo: Fyo): Promise<ItemVisibility> {
const posProfileName = fyo.singles.POSSettings?.posProfile as string;
const enableERPNextSync = fyo.singles.AccountingSettings?.enableERPNextSync;
if (enableERPNextSync) {
return fyo.singles.POSSettings?.itemVisibilityERP as ItemVisibility;
}
if (posProfileName) {
const posProfile = await fyo.doc.getDoc(
@@ -738,6 +746,74 @@ export function getDocStatusListColumn(): ColumnConfig {
};
}
export function getLoyaltyProgramStatusColumn(): ColumnConfig {
return {
label: t`Status`,
fieldname: 'status',
fieldtype: 'Select',
render(doc) {
const status = getLoyaltyProgramStatus(doc);
const color = loyaltyProgramStatusColor[status] ?? 'gray';
const label = getLoyaltyProgramStatusText(status);
return {
template: `<Badge class="text-xs" color="${color}">${label}</Badge>`,
metadata: {
status,
color,
label,
},
};
},
};
}
export function getLoyaltyProgramStatus(doc?: RenderData | Doc): string {
if (!doc) {
return '';
}
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
const toDate = doc.toDate as Date;
if (toDate && toDate <= currentDate) {
return 'Expired';
}
const maximumUse = doc.maximumUse as number;
const used = doc.used as number;
if (maximumUse > 0 && used >= maximumUse) {
return 'Maxed';
}
return 'Active';
}
export const loyaltyProgramStatusColor: Record<string, string | undefined> = {
Active: 'green',
Disabled: 'gray',
Expired: 'red',
Maxed: 'orange',
};
export function getLoyaltyProgramStatusText(status: string): string {
switch (status) {
case 'Active':
return t`Active`;
case 'Disabled':
return t`Disabled`;
case 'Expired':
return t`Expired`;
case 'Maxed':
return t`Maxed`;
default:
return '';
}
}
type ModelsWithItems = Invoice | StockTransfer | StockMovement;
export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
if (!doc.canEdit) {
@@ -761,6 +837,13 @@ export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
await item.set('item', name);
if (doc instanceof Invoice && !doc.isSales) {
const batchName = await generateBatchForItem(doc.fyo, name);
if (batchName) {
await item.set('batch', batchName);
}
}
if (
doc instanceof StockTransfer &&
doc.schemaName === ModelNameEnum.PurchaseReceipt
@@ -883,6 +966,15 @@ export async function createLoyaltyPointEntry(doc: Invoice) {
if (!loyaltyProgramDoc.isEnabled) {
return;
}
const toDate = loyaltyProgramDoc.toDate as Date;
const today = new Date();
today.setHours(0, 0, 0, 0);
if (toDate && new Date(toDate).getTime() < today.getTime()) {
return;
}
const expiryDate = new Date(Date.now());
expiryDate.setDate(
@@ -951,18 +1043,22 @@ export function getLoyaltyProgramTier(
let loyaltyProgramTier: CollectionRulesItems | undefined;
for (const row of loyaltyProgramData.collectionRules) {
if (isPesa(row.minimumTotalSpent)) {
const minimumSpent = row.minimumTotalSpent;
if (row.minimumTotalSpent !== undefined && row.minimumTotalSpent !== null) {
let minimumSpent: Money;
if (!minimumSpent.lte(grandTotal)) {
continue;
if (isPesa(row.minimumTotalSpent)) {
minimumSpent = row.minimumTotalSpent;
} else {
minimumSpent = new Money(row.minimumTotalSpent as number);
}
if (
!loyaltyProgramTier ||
minimumSpent.gt(loyaltyProgramTier.minimumTotalSpent as Money)
) {
loyaltyProgramTier = row;
if (minimumSpent.lte(grandTotal)) {
if (
!loyaltyProgramTier ||
minimumSpent.gt(loyaltyProgramTier.minimumTotalSpent as Money)
) {
loyaltyProgramTier = row;
}
}
}
}
@@ -1537,6 +1633,43 @@ export async function validateCouponCode(
}
}
export async function validateLoyaltyProgram(
doc: Invoice,
loyaltyProgramName: string
) {
const loyaltyProgram = await doc.fyo.db.getAll(ModelNameEnum.LoyaltyProgram, {
fields: ['fromDate', 'toDate', 'maximumUse', 'used', 'isEnabled'],
filters: { name: loyaltyProgramName },
});
if (
(loyaltyProgram[0]?.maximumUse as number) > 0 &&
(loyaltyProgram[0]?.used as number) >=
(loyaltyProgram[0]?.maximumUse as number)
) {
return;
}
if (
loyaltyProgram[0].fromDate &&
(doc.date as Date) < (loyaltyProgram[0].fromDate as Date)
) {
throw new ValidationError('Loyalty program is not yet active');
}
const toDate = loyaltyProgram[0].toDate as Date;
if (toDate) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const normalizedToDate = new Date(toDate);
normalizedToDate.setHours(0, 0, 0, 0);
if (normalizedToDate.getTime() < today.getTime()) {
return;
}
}
}
export function removeFreeItems(sinvDoc: SalesInvoice) {
if (!sinvDoc || !sinvDoc.items) {
return;
@@ -1611,3 +1744,36 @@ export function roundFreeItemQty(
): number {
return Math[roundingMethod](quantity);
}
export async function isLoyaltyProgramExpiredAndMaxed(
fyo: Fyo,
loyaltyProgramName: string
): Promise<boolean> {
if (!loyaltyProgramName) {
return false;
}
const loyaltyProgram = await fyo.db.getAll(ModelNameEnum.LoyaltyProgram, {
fields: ['toDate', 'maximumUse', 'used', 'isEnabled'],
filters: { name: loyaltyProgramName },
});
if (!loyaltyProgram.length) {
return false;
}
const program = loyaltyProgram[0];
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
const toDate = program.toDate as Date;
const isExpired =
toDate && new Date(toDate).getTime() < currentDate.getTime();
const maximumUse = (program.maximumUse as number) || 0;
const used = (program.used as number) || 0;
const isMaxed = maximumUse > 0 && used >= maximumUse;
const result = isExpired || isMaxed;
return result;
}
@@ -16,6 +16,7 @@ export class POSSettings extends Doc {
itemWeightDigits?: number;
defaultAccount?: string;
itemVisibility?: string;
itemVisibilityERP?: 'ERP Sync Items';
posUI?: 'Classic' | 'Modern';
canChangeRate?: boolean;
canEditDiscount?: boolean;
@@ -46,6 +47,9 @@ export class POSSettings extends Doc {
!this.fyo.singles.InventorySettings?.enableBarcodes ||
!this.weightEnabledBarcode,
itemVisibility: () =>
!this.fyo.singles.AccountingSettings?.enablePointOfSaleWithOutInventory,
!this.fyo.singles.AccountingSettings?.enablePointOfSaleWithOutInventory ||
!!this.fyo.singles.AccountingSettings?.enableERPNextSync,
itemVisibilityERP: () =>
!this.fyo.singles.AccountingSettings?.enableERPNextSync,
};
}
+56 -7
View File
@@ -16,6 +16,8 @@ import { StockMovementItem } from './StockMovementItem';
import { Transfer } from './Transfer';
import {
canValidateSerialNumber,
createBatch,
generateBatchForItem,
getSerialNumberFromDoc,
updateSerialNumbers,
validateBatch,
@@ -65,6 +67,42 @@ export class StockMovement extends Transfer {
await updateSerialNumbers(this, false);
}
async beforeSubmit(): Promise<void> {
await super.beforeSubmit();
const batchesToCreate: { item: string; batch: string }[] = [];
for (const item of this.items ?? []) {
if (!item.item || !item.batch) {
continue;
}
const hasBatch = await this.fyo.getValue(
ModelNameEnum.Item,
item.item,
'hasBatch'
);
if (hasBatch) {
const batchExists = await this.fyo.db.exists(
ModelNameEnum.Batch,
item.batch
);
if (!batchExists) {
batchesToCreate.push({
item: item.item,
batch: item.batch,
});
}
}
}
for (const { item, batch } of batchesToCreate) {
await createBatch(this.fyo, item, batch);
}
}
async afterCancel(): Promise<void> {
await super.afterCancel();
await updateSerialNumbers(this, true);
@@ -125,10 +163,10 @@ export class StockMovement extends Transfer {
item: row.item!,
rate: row.rate!,
quantity: row.quantity!,
batch: row.batch!,
serialNumber: row.serialNumber!,
fromLocation: row.fromLocation,
toLocation: row.toLocation,
batch: row.batch ?? undefined,
serialNumber: row.serialNumber ?? undefined,
fromLocation: row.fromLocation ?? undefined,
toLocation: row.toLocation ?? undefined,
}));
}
@@ -142,19 +180,30 @@ export class StockMovement extends Transfer {
throw new ValidationError(t`Item ${name} not found`);
}
let batch: string | null | undefined =
(itemDoc.defaultBatch as string | null | undefined) ?? null;
if (
this.movementType === MovementTypeEnum.MaterialReceipt &&
itemDoc.hasBatch &&
!batch
) {
batch = await generateBatchForItem(this.fyo, name);
}
const item = {
name: itemDoc.name,
batch: itemDoc.defaultBatch ?? null,
batch,
};
if (item.batch) {
const batchDoc = await this.fyo.doc.getDoc(
ModelNameEnum.Batch,
item.batch as string
item.batch
);
if (batchDoc && batchDoc.item !== name) {
throw new ValidationError(
t`Batch ${item.batch as string} does not belong to Item ${name}`
t`Batch ${item.batch} does not belong to Item ${name}`
);
}
}
+50 -3
View File
@@ -13,7 +13,7 @@ import { ValidationError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { safeParseFloat } from 'utils/index';
import { generateSerialNumbersForItem } from './helpers';
import { generateSerialNumbersForItem, getSuggestedBatchName } from './helpers';
import { StockMovement } from './StockMovement';
import { TransferItem } from './TransferItem';
import { MovementTypeEnum } from './types';
@@ -80,14 +80,43 @@ export class StockMovementItem extends TransferItem {
};
},
batch: async (doc: Doc) => {
let suggestedBatch: string | undefined;
let hasBatch = false;
if (doc.parentdoc?.movementType === MovementTypeEnum.MaterialReceipt) {
hasBatch = !!(await doc.fyo.getValue(
ModelNameEnum.Item,
doc.item as string,
'hasBatch'
));
if (hasBatch) {
suggestedBatch = await getSuggestedBatchName(
doc.fyo,
doc.item as string
);
if (suggestedBatch) {
await doc.set('batch', suggestedBatch);
}
}
}
const batches = await doc.fyo.db.getAll(ModelNameEnum.Batch, {
fields: ['name'],
filters: { item: doc.item as string },
});
const batchName = batches.map((b) => b.name) as string[];
const existingBatchNames = batches.map((b) => b.name) as string[];
const allBatches = new Set<string>(existingBatchNames);
if (suggestedBatch) {
allBatches.add(suggestedBatch);
}
const finalBatchList = Array.from(allBatches);
return {
name: ['in', batchName],
name: ['in', finalBatchList],
};
},
};
@@ -338,6 +367,24 @@ export class StockMovementItem extends TransferItem {
if (ch.changed === 'item') {
await this.set('serialNumber', '');
if (
this.parentdoc?.movementType === MovementTypeEnum.MaterialReceipt &&
this.item
) {
const hasBatch = await this.fyo.getValue(
ModelNameEnum.Item,
this.item,
'hasBatch'
);
if (hasBatch) {
const batchName = await getSuggestedBatchName(this.fyo, this.item);
if (batchName) {
await this.set('batch', batchName);
}
}
}
if (shouldGenerateSerialNumbers) {
await this.generateAndSetSerialNumbers();
}
+164
View File
@@ -11,6 +11,7 @@ import type { StockTransferItem } from './StockTransferItem';
import { Transfer } from './Transfer';
import { TransferItem } from './TransferItem';
import type { SerialNumberStatus } from './types';
import BatchSeries from 'fyo/models/BatchSeries';
import SerialNumberSeries from 'fyo/models/SerialNumberSeries';
export async function validateBatch(
@@ -19,6 +20,35 @@ export async function validateBatch(
if (doc.schemaName === ModelNameEnum.SalesQuote) {
return;
}
if (
doc.schemaName === ModelNameEnum.PurchaseInvoice ||
doc.schemaName === ModelNameEnum.PurchaseReceipt ||
doc.schemaName === ModelNameEnum.StockMovement ||
doc.schemaName === ModelNameEnum.Shipment
) {
for (const row of doc.items ?? []) {
if (row.item && row.batch) {
const hasBatch = await doc.fyo.getValue(
ModelNameEnum.Item,
row.item,
'hasBatch'
);
if (hasBatch) {
const batchExists = await doc.fyo.db.exists(
ModelNameEnum.Batch,
row.batch
);
if (!batchExists) {
await createBatch(doc.fyo, row.item, row.batch);
}
}
}
}
}
for (const row of doc.items ?? []) {
await validateItemRowBatch(row);
}
@@ -531,3 +561,137 @@ export async function getExistingActiveSerialNumbersForItem(
return selectedSerialNumbers.join('\n');
}
export async function getSuggestedBatchName(
fyo: Fyo,
itemName: string
): Promise<string | undefined> {
try {
const batchSeries = await fyo.getValue(
ModelNameEnum.Item,
itemName,
'batchSeries'
);
if (!batchSeries) {
return undefined;
}
const seriesName = (batchSeries as string).trim();
const seriesExists = await fyo.db.exists('BatchSeries', seriesName);
if (!seriesExists) {
await fyo.doc
.getNewDoc('BatchSeries', {
name: seriesName,
start: 1001,
padZeros: 4,
current: 1001,
})
.sync();
}
const batchSeriesDoc = (await fyo.doc.getDoc(
'BatchSeries',
seriesName
)) as BatchSeries;
const padZeros = (batchSeriesDoc.padZeros as number) ?? 4;
const existingBatches = (await fyo.db.getAllRaw(ModelNameEnum.Batch, {
fields: ['name'],
filters: { item: itemName },
})) as { name: string }[];
let nextNumber: number;
if (existingBatches && existingBatches.length > 0) {
let highestNumber = -1;
for (const batch of existingBatches) {
const batchName = batch.name;
// Extract numeric part from batch name (handles names like "com-1001")
const numericPart = batchName.replace(seriesName, '');
const num = parseInt(numericPart, 10);
if (!isNaN(num) && num > highestNumber) {
highestNumber = num;
}
}
if (highestNumber >= 0) {
nextNumber = highestNumber + 1;
} else {
nextNumber = (batchSeriesDoc.start as number) ?? 1001;
}
} else {
nextNumber = (batchSeriesDoc.start as number) ?? 1001;
}
const batchName = `${seriesName}${nextNumber
.toString()
.padStart(padZeros, '0')}`;
return batchName;
} catch (error) {
return undefined;
}
}
export async function createBatch(
fyo: Fyo,
itemName: string,
batchName: string
): Promise<boolean> {
try {
const batchExists = await fyo.db.exists(ModelNameEnum.Batch, batchName);
if (batchExists) {
return true;
}
const batchDoc = fyo.doc.getNewDoc('Batch', {
name: batchName,
item: itemName,
});
await batchDoc.sync();
const batchSeries = await fyo.getValue(
ModelNameEnum.Item,
itemName,
'batchSeries'
);
if (batchSeries) {
const seriesName = (batchSeries as string).trim();
const batchSeriesDoc = (await fyo.doc.getDoc(
'BatchSeries',
seriesName
)) as BatchSeries;
const num = parseInt(batchName, 10);
if (!isNaN(num)) {
await batchSeriesDoc.set('current', num);
await batchSeriesDoc.sync();
}
}
return true;
} catch (error) {
return false;
}
}
export async function generateBatchForItem(
fyo: Fyo,
itemName: string
): Promise<string | undefined> {
const batchName = await getSuggestedBatchName(fyo, itemName);
if (!batchName) {
return undefined;
}
const success = await createBatch(fyo, itemName, batchName);
return success ? batchName : undefined;
}
+1
View File
@@ -15,6 +15,7 @@ export enum ModelNameEnum {
AccountingSettings = 'AccountingSettings',
Address = 'Address',
Batch = 'Batch',
BatchSeries = 'BatchSeries',
Color = 'Color',
Currency = 'Currency',
GetStarted = 'GetStarted',
+1 -1
View File
@@ -30,7 +30,7 @@
"electron-store": "^8.0.1",
"feather-icons": "^4.28.0",
"knex": "^2.4.0",
"lodash": "^4.17.21",
"lodash": "^4.17.23",
"luxon": "^2.5.2",
"node-fetch": "2",
"pesa": "^1.1.12",
+38
View File
@@ -0,0 +1,38 @@
{
"name": "BatchSeries",
"label": "Batch 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
View File
@@ -153,6 +153,12 @@
"default": false,
"section": "Inventory"
},
{
"fieldname": "batchSeries",
"label": "Batch Series",
"fieldtype": "Data",
"section": "Inventory"
},
{
"fieldname": "hasSerialNumber",
"label": "Has Serial Number",
@@ -172,6 +178,14 @@
"fieldtype": "Table",
"target": "UOMConversionItem",
"section": "Inventory"
},
{
"fieldname": "datafromErp",
"fieldtype": "Check",
"hidden": true,
"default": false,
"section": "Default",
"readOnly": true
}
],
"quickEditFields": [
+18 -2
View File
@@ -51,7 +51,7 @@
"label": "Expiry Duration",
"fieldtype": "Int",
"default": 1,
"required": true
"hidden": true
},
{
"fieldname": "expenseAccount",
@@ -59,6 +59,21 @@
"fieldtype": "Link",
"target": "Account",
"required": true
},
{
"fieldname": "maximumUse",
"label": "Maximum Use",
"fieldtype": "Int",
"default": 0,
"section": "Validity and Usage"
},
{
"fieldname": "used",
"label": "Used",
"fieldtype": "Int",
"default": 0,
"readOnly": true,
"section": "Validity and Usage"
}
],
"quickEditFields": [
@@ -67,7 +82,8 @@
"toDate",
"conversionFactor",
"expenseAccount",
"expiryDuration"
"maximumUse",
"used"
],
"keywordFields": ["name"]
}
+2 -2
View File
@@ -82,7 +82,7 @@
"fieldtype": "Link",
"target": "LoyaltyProgram",
"label": "Loyalty Program",
"section": "References",
"section": "Loyalty Points Redemption",
"readOnly": true
},
{
@@ -90,7 +90,7 @@
"fieldtype": "Int",
"label": "Available Loyalty Points",
"readOnly": true,
"section": "References"
"section": "Loyalty Points Redemption"
},
{
"fieldname": "redeemLoyaltyPoints",
@@ -96,13 +96,6 @@
"default": 0,
"section": "Barcode"
},
{
"fieldname": "itemWeightDigits",
"label": "item Weight Digits",
"fieldtype": "Int",
"default": 0,
"section": "Barcode"
},
{
"fieldname": "itemVisibility",
"label": "Item Visibility",
@@ -118,7 +111,27 @@
}
],
"default": "Inventory Items",
"required": true,
"section": "Default"
},
{
"fieldname": "itemVisibilityERP",
"label": "Item Visibility",
"fieldtype": "Select",
"options": [
{
"value": "ERP Sync Items",
"label": "ERP Sync Items"
},
{
"value": "Inventory Items",
"label": "Inventory Items"
},
{
"value": "Non-Inventory Items",
"label": "Non-Inventory Items"
}
],
"default": "ERP Sync Items",
"section": "Default"
},
{
+2
View File
@@ -3,6 +3,7 @@ import AccountingLedgerEntry from './app/AccountingLedgerEntry.json';
import AccountingSettings from './app/AccountingSettings.json';
import Address from './app/Address.json';
import Batch from './app/Batch.json';
import BatchSeries from './app/BatchSeries.json';
import Color from './app/Color.json';
import Currency from './app/Currency.json';
import Defaults from './app/Defaults.json';
@@ -108,6 +109,7 @@ export const appSchemas: Schema[] | SchemaStub[] = [
Defaults as Schema,
NumberSeries as Schema,
SerialNumberSeries as Schema,
BatchSeries as Schema,
PrintSettings as Schema,
+5
View File
@@ -63,6 +63,7 @@
</p>
</div>
<p
v-if="itemVisibility !== 'ERP Sync Items'"
class="
absolute
top-1
@@ -111,6 +112,10 @@ export default defineComponent({
itemQtyMap: {
type: Object,
},
itemVisibility: {
type: String,
default: 'Inventory Items',
},
},
methods: {
getExtractedWords(item: string) {
+21 -9
View File
@@ -59,7 +59,7 @@
size="large"
class=""
:df="df"
:value="row[df.fieldname]"
:value="(row as POSItem)[df.fieldname as keyof POSItem]"
:readOnly="true"
/>
</Row>
@@ -81,13 +81,20 @@ export default defineComponent({
props: {
items: Array,
itemQtyMap: Object,
itemVisibility: {
type: String,
default: 'Inventory Items',
},
},
computed: {
ratio() {
if (this.itemVisibility === 'ERP Sync Items') {
return [1, 1.5, 0.8];
}
return [1, 1, 1, 0.7];
},
tableFields() {
return [
const fields = [
{
fieldname: 'name',
fieldtype: 'Data',
@@ -102,13 +109,6 @@ export default defineComponent({
fieldtype: 'Currency',
readOnly: true,
},
{
fieldname: 'availableQty',
label: 'Qty',
placeholder: 'Available Qty',
fieldtype: 'Float',
readOnly: true,
},
{
fieldname: 'unit',
label: 'Unit',
@@ -118,6 +118,18 @@ export default defineComponent({
readOnly: true,
},
] as Field[];
if (this.itemVisibility !== 'ERP Sync Items') {
fields.splice(2, 0, {
fieldname: 'availableQty',
label: 'Qty',
placeholder: 'Available Qty',
fieldtype: 'Float',
readOnly: true,
});
}
return fields;
},
},
methods: {
+34 -6
View File
@@ -2,10 +2,10 @@
<feather-icon
:name="isExapanded ? 'chevron-up' : 'chevron-down'"
class="w-4 h-4 inline-flex cursor-pointer text-gray-700 dark:text-gray-200"
@click="isExapanded = !isExapanded"
@click="toggleExpand"
/>
<div class="relative" @click="emitSelectedRow">
<div class="relative" @click="toggleExpandAndEmit">
<Link
class="pt-2"
:df="{
@@ -27,7 +27,7 @@
</p>
</div>
<div class="flex items-center" @click="emitSelectedRow">
<div class="flex items-center">
<Int
:df="{
fieldname: 'quantity',
@@ -115,7 +115,7 @@
<div></div>
<template v-if="isExapanded">
<div class="px-4 pt-6 col-span-1" @click="emitSelectedRow">
<div class="px-4 pt-6 col-span-1">
<Int
v-if="isUOMConversionEnabled"
:df="{
@@ -286,7 +286,7 @@ import Link from 'src/components/Controls/Link.vue';
import Text from 'src/components/Controls/Text.vue';
import { inject } from 'vue';
import { fyo } from 'src/initFyo';
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { Money } from 'pesa';
import { DiscountType } from '../types';
@@ -305,8 +305,17 @@ export default defineComponent({
props: {
row: { type: SalesInvoiceItem, required: true },
batchAdded: { type: Boolean, default: false },
expandedBatchId: {
type: String as PropType<string | null | undefined>,
default: undefined,
},
},
emits: ['runSinvFormulas', 'applyPricingRule', 'selectedRow'],
emits: [
'runSinvFormulas',
'applyPricingRule',
'selectedRow',
'setExpandedBatchId',
],
setup() {
return {
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
@@ -331,11 +340,17 @@ export default defineComponent({
};
},
watch: {
expandedBatchId(newVal) {
if (newVal !== this.row.name) {
this.isExapanded = false;
}
},
'row.batch': {
async handler(newBatch) {
if (newBatch) {
this.availableQtyInBatch = await this.getAvailableQtyInBatch();
this.isExapanded = true;
this.$emit('setExpandedBatchId', this.row.name);
}
},
immediate: true,
@@ -466,6 +481,19 @@ export default defineComponent({
},
methods: {
toggleExpand() {
if (this.isExapanded) {
this.isExapanded = false;
this.$emit('setExpandedBatchId', undefined);
} else {
this.isExapanded = true;
this.$emit('setExpandedBatchId', this.row.name);
}
},
toggleExpandAndEmit() {
this.toggleExpand();
this.$emit('selectedRow', this.row);
},
emitSelectedRow() {
this.$emit('selectedRow', this.row);
},
@@ -53,6 +53,10 @@
>
<SelectedItemRow
:row="(row as SalesInvoiceItem)"
:expanded-batch-id="expandedBatchId"
@set-expanded-batch-id="
(rowName) => $emit('setExpandedBatchId', rowName)
"
@run-sinv-formulas="runSinvFormulas"
@apply-pricing-rule="$emit('applyPricingRule')"
@selected-row="selectedItemRow"
@@ -70,7 +74,7 @@ import RowEditForm from 'src/pages/CommonForm/RowEditForm.vue';
import SelectedItemRow from './SelectedItemRow.vue';
import { isNumeric } from 'src/utils';
import { inject } from 'vue';
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { Field } from 'schemas/types';
@@ -90,12 +94,13 @@ export default defineComponent({
sinvDoc: inject('sinvDoc') as SalesInvoice,
};
},
data() {
return {
isExapanded: false,
};
props: {
expandedBatchId: {
type: String as PropType<string | null | undefined>,
default: undefined,
},
},
emits: ['applyPricingRule', 'selectedRow'],
emits: ['applyPricingRule', 'selectedRow', 'setExpandedBatchId'],
computed: {
ratio() {
return [0.1, 0.9, 0.8, 0.8, 0.8, 0.8, 0.2];
@@ -66,6 +66,7 @@
</p>
</div>
<p
v-if="itemVisibility !== 'ERP Sync Items'"
class="
w-6
h-6
@@ -114,6 +115,10 @@ export default defineComponent({
itemQtyMap: {
type: Object,
},
itemVisibility: {
type: String,
default: 'Inventory Items',
},
},
methods: {
getExtractedWords(item: string) {
@@ -54,7 +54,7 @@
:key="df.fieldname"
size="large"
:df="df"
:value="row[df.fieldname]"
:value="(row as POSItem)[df.fieldname as keyof POSItem]"
:readOnly="true"
/>
</Row>
@@ -113,7 +113,7 @@
:key="df.fieldname"
size="large"
:df="df"
:value="row[df.fieldname]"
:value="(row as POSItem)[df.fieldname as keyof POSItem]"
:readOnly="true"
/>
</Row>
@@ -137,13 +137,20 @@ export default defineComponent({
props: {
items: Array,
itemQtyMap: Object,
itemVisibility: {
type: String,
default: 'Inventory Items',
},
},
computed: {
ratio() {
return [1, 1, 0.6, 0.7];
if (this.itemVisibility === 'ERP Sync Items') {
return [1, 1.5, 0.8];
}
return [1, 1, 1, 0.7];
},
tableFields() {
return [
const fields = [
{
fieldname: 'name',
fieldtype: 'Data',
@@ -158,13 +165,6 @@ export default defineComponent({
fieldtype: 'Currency',
readOnly: true,
},
{
fieldname: 'availableQty',
label: t`Qty`,
placeholder: 'Available Qty',
fieldtype: 'Float',
readOnly: true,
},
{
fieldname: 'unit',
label: t`Unit`,
@@ -174,6 +174,18 @@ export default defineComponent({
readOnly: true,
},
] as Field[];
if (this.itemVisibility !== 'ERP Sync Items') {
fields.splice(2, 0, {
fieldname: 'availableQty',
label: t`Qty`,
placeholder: 'Available Qty',
fieldtype: 'Float',
readOnly: true,
});
}
return fields;
},
firstColumnItems() {
return this.items?.slice(0, Math.ceil(this.items.length / 2));
@@ -3,11 +3,11 @@
<feather-icon
:name="isExapanded ? 'chevron-up' : 'chevron-down'"
class="w-4 h-4 inline-flex dark:text-white"
@click="isExapanded = !isExapanded"
@click="toggleExpand"
/>
</div>
<div class="relative" @click="isExapanded = !isExapanded">
<div class="relative" @click="toggleExpand">
<Link
:df="{
fieldname: 'item',
@@ -245,7 +245,7 @@ import Link from 'src/components/Controls/Link.vue';
import Text from 'src/components/Controls/Text.vue';
import { inject } from 'vue';
import { fyo } from 'src/initFyo';
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { Money } from 'pesa';
import { validateSerialNumberCount } from 'src/utils/pos';
@@ -256,8 +256,18 @@ export default defineComponent({
props: {
row: { type: SalesInvoiceItem, required: true },
batchAdded: { type: Boolean, default: false },
expandedBatchId: {
type: String as PropType<string | null | undefined>,
default: undefined,
},
},
emits: ['toggleModal', 'runSinvFormulas', 'selectedRow', 'applyPricingRule'],
emits: [
'toggleModal',
'runSinvFormulas',
'selectedRow',
'applyPricingRule',
'setExpandedBatchId',
],
setup() {
return {
@@ -278,11 +288,17 @@ export default defineComponent({
};
},
watch: {
expandedBatchId(newVal) {
if (newVal !== this.row.name) {
this.isExapanded = false;
}
},
'row.batch': {
async handler(newBatch) {
if (newBatch) {
this.availableQtyInBatch = await this.getAvailableQtyInBatch();
this.isExapanded = true;
this.$emit('setExpandedBatchId', this.row.name);
}
},
immediate: true,
@@ -300,6 +316,15 @@ export default defineComponent({
},
},
methods: {
toggleExpand() {
if (this.isExapanded) {
this.isExapanded = false;
this.$emit('setExpandedBatchId', undefined);
} else {
this.isExapanded = true;
this.$emit('setExpandedBatchId', this.row.name);
}
},
handleOpenKeyboard(row: SalesInvoiceItem, field: string) {
if (this.isReadOnly) {
return;
@@ -41,6 +41,10 @@
>
<ModernPOSSelectedItemRow
:row="(row as SalesInvoiceItem)"
:expanded-batch-id="expandedBatchId"
@set-expanded-batch-id="
(rowName) => $emit('setExpandedBatchId', rowName)
"
@selected-row="selectedItemRow"
@run-sinv-formulas="runSinvFormulas"
@apply-pricing-rule="$emit('applyPricingRule')"
@@ -59,7 +63,7 @@ import RowEditForm from 'src/pages/CommonForm/RowEditForm.vue';
import ModernPOSSelectedItemRow from './ModernPOSSelectedItemRow.vue';
import { isNumeric } from 'src/utils';
import { t } from 'fyo';
import { inject, defineComponent } from 'vue';
import { inject, defineComponent, PropType } from 'vue';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { Field } from 'schemas/types';
@@ -79,12 +83,18 @@ export default defineComponent({
sinvDoc: inject('sinvDoc') as SalesInvoice,
};
},
data() {
return {
isExapanded: false,
};
props: {
expandedBatchId: {
type: String as PropType<string | null | undefined>,
default: undefined,
},
},
emits: ['toggleModal', 'selectedRow', 'applyPricingRule'],
emits: [
'toggleModal',
'selectedRow',
'applyPricingRule',
'setExpandedBatchId',
],
computed: {
ratio() {
return [0.1, 0.8, 0.4, 0.8, 0.8, 0.3];
+1 -1
View File
@@ -10,7 +10,7 @@ export type ItemGroupMap = Record<string, string>;
export type DiscountType = 'percent' | 'amount';
export type ItemVisibility = 'Inventory Items' | 'Non-Inventory Items'
export type ItemVisibility = 'Inventory Items' | 'Non-Inventory Items' | 'ERP Sync Items'
export const modalNames = [
'Keyboard',
+28
View File
@@ -8,6 +8,7 @@ import { Doc } from 'fyo/model/doc';
import { isPesa } from 'fyo/utils';
import { Invoice } from 'models/baseModels/Invoice/Invoice';
import { Party } from 'models/baseModels/Party/Party';
import { LoyaltyProgram } from 'models/baseModels/LoyaltyProgram/LoyaltyProgram';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { getBgTextColorClass } from 'src/utils/colors';
@@ -77,6 +78,9 @@ export default defineComponent({
ReturnIssued: this.t`Return Issued`,
Unpaid: this.t`Unpaid`,
PartlyPaid: this.t`Partly Paid`,
Expired: this.t`Expired`,
Active: this.t`Active`,
Maxed: this.t`Maxed`,
}[this.status];
},
color(): UIColors {
@@ -99,6 +103,9 @@ const statusColorMap: Record<Status, UIColors> = {
ReturnIssued: 'gray',
Unpaid: 'red',
PartlyPaid: 'yellow',
Expired: 'red',
Active: 'green',
Maxed: 'orange',
};
function getStatus(doc: Doc) {
@@ -110,6 +117,27 @@ function getStatus(doc: Doc) {
return 'NotSaved';
}
if (doc instanceof LoyaltyProgram) {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
const maximumUse = doc.maximumUse as number;
const used = doc.used as number;
if (maximumUse > 0 && used >= maximumUse) {
return 'Maxed';
}
if (doc.toDate && doc.toDate instanceof Date) {
const toDate = new Date(doc.toDate);
toDate.setHours(0, 0, 0, 0);
if (toDate <= currentDate) {
return 'Expired';
}
}
return 'Active';
}
if (doc instanceof Party && doc.outstandingAmount?.isZero() !== true) {
return 'Outstanding';
}
+15
View File
@@ -134,6 +134,7 @@
v-if="tableView"
:items="items"
:item-qty-map="itemQuantityMap as ItemQtyMap"
:item-visibility="itemVisibility"
@add-item="(item) => emitEvent('addItem', item)"
/>
@@ -141,6 +142,7 @@
v-else
:items="items"
:item-qty-map="itemQuantityMap as ItemQtyMap"
:item-visibility="itemVisibility"
@add-item="(item) => emitEvent('addItem', item)"
/>
@@ -184,6 +186,10 @@
/>
<SelectedItemTable
:expanded-batch-id="expandedBatchId"
@set-expanded-batch-id="
(rowName) => $emit('setExpandedBatchId', rowName)
"
@apply-pricing-rule="emitEvent('applyPricingRule')"
@selected-row="(row) => $emit('selectedRow', row)"
/>
@@ -483,6 +489,10 @@ export default defineComponent({
type: Array as PropType<POSItem[] | undefined>,
default: () => [],
},
itemVisibility: {
type: String,
default: 'Inventory Items',
},
profile: {
type: Object as PropType<POSProfile>,
required: false,
@@ -496,8 +506,13 @@ export default defineComponent({
type: String,
default: '',
},
expandedBatchId: {
type: String as PropType<string | null | undefined>,
default: undefined,
},
},
emits: [
'setExpandedBatchId',
'addItem',
'toggleView',
'toggleModal',
+16 -8
View File
@@ -128,6 +128,22 @@ export default defineComponent({
return;
}
const loyaltyProgramDoc = await this.fyo.db.getAll(
ModelNameEnum.LoyaltyProgram,
{
fields: ['conversionFactor', 'toDate'],
filters: { name: partyData.loyaltyProgram as string },
}
);
const toDate = loyaltyProgramDoc[0]?.toDate as Date;
const today = new Date();
today.setHours(0, 0, 0, 0);
if (toDate && new Date(toDate).getTime() < today.getTime()) {
throw new Error(t`Loyalty program has expired and cannot be applied`);
}
if (this.loyaltyPoints >= newValue) {
this.sinvDoc.loyaltyPoints = newValue;
} else {
@@ -138,14 +154,6 @@ export default defineComponent({
);
}
const loyaltyProgramDoc = await this.fyo.db.getAll(
ModelNameEnum.LoyaltyProgram,
{
fields: ['conversionFactor'],
filters: { name: partyData.loyaltyProgram as string },
}
);
const loyaltyPoint =
newValue * ((loyaltyProgramDoc[0]?.conversionFactor as number) || 0);
+15
View File
@@ -119,6 +119,10 @@
/>
<ModernPOSSelectedItemTable
:expanded-batch-id="expandedBatchId"
@set-expanded-batch-id="
(rowName) => $emit('setExpandedBatchId', rowName)
"
@selected-row="selectedRow"
@apply-pricing-rule="emitEvent('applyPricingRule')"
@toggle-modal="emitEvent('toggleModal', 'Keyboard')"
@@ -345,6 +349,7 @@
v-if="tableView"
:items="items"
:item-qty-map="itemQuantityMap as ItemQtyMap"
:item-visibility="itemVisibility"
@add-item="(item:string) => emitEvent('addItem', item)"
/>
@@ -352,6 +357,7 @@
v-else
:items="items"
:item-qty-map="itemQuantityMap as ItemQtyMap"
:item-visibility="itemVisibility"
@add-item="(item:string) => emitEvent('addItem', item)"
/>
@@ -489,6 +495,10 @@ export default defineComponent({
type: Array as PropType<POSItem[] | undefined>,
default: () => [],
},
itemVisibility: {
type: String,
default: 'Inventory Items',
},
profile: {
type: Object as PropType<POSProfile>,
required: false,
@@ -502,8 +512,13 @@ export default defineComponent({
type: String,
default: '',
},
expandedBatchId: {
type: String as PropType<string | null | undefined>,
default: undefined,
},
},
emits: [
'setExpandedBatchId',
'addItem',
'toggleView',
'toggleModal',
+38 -3
View File
@@ -27,6 +27,7 @@
:selected-item-group="selectedItemGroup"
:is-pos-shift-open="isPosShiftOpen"
:items="(items as [] as POSItem[])"
:item-visibility="itemVisibility"
:sinv-doc="(sinvDoc as SalesInvoice)"
:disable-pay-button="disablePayButton"
:open-payment-modal="openPaymentModal"
@@ -43,6 +44,8 @@
:open-return-sales-invoice-modal="openReturnSalesInvoiceModal"
:open-batch-selection-modal="openBatchSelectionModal"
:selected-item-for-batch="selectedItemForBatch"
:expanded-batch-id="expandedBatchId"
@set-expanded-batch-id="setExpandedBatchId"
@add-item="addItem"
@toggle-view="toggleView"
@set-sinv-doc="setSinvDoc"
@@ -83,6 +86,7 @@
:selected-item-group="selectedItemGroup"
:is-pos-shift-open="isPosShiftOpen"
:items="(items as [] as POSItem[])"
:item-visibility="itemVisibility"
:sinv-doc="(sinvDoc as SalesInvoice)"
:disable-pay-button="disablePayButton"
:open-payment-modal="openPaymentModal"
@@ -100,6 +104,8 @@
:open-return-sales-invoice-modal="openReturnSalesInvoiceModal"
:open-batch-selection-modal="openBatchSelectionModal"
:selected-item-for-batch="selectedItemForBatch"
:expanded-batch-id="expandedBatchId"
@set-expanded-batch-id="setExpandedBatchId"
@add-item="addItem"
@toggle-view="toggleView"
@set-sinv-doc="setSinvDoc"
@@ -166,7 +172,9 @@ import {
removeFreeItems,
getItemRateFromPriceList,
getItemVisibility,
isLoyaltyProgramExpiredAndMaxed,
} from 'models/helpers';
import { ItemVisibility } from 'src/components/POS/types';
import {
POSItem,
ItemQtyMap,
@@ -262,6 +270,8 @@ export default defineComponent({
quickQtyKeyUpHandler: null as ((e: KeyboardEvent) => void) | null,
selectedItemForBatch: '' as string,
pendingBatchItem: null as { item: POSItem; quantity: number } | null,
expandedBatchId: undefined as string | null | undefined,
itemVisibilityValue: 'Inventory Items' as ItemVisibility,
};
},
computed: {
@@ -271,6 +281,9 @@ export default defineComponent({
return !!fyo.singles.AccountingSettings?.enableDiscounting;
},
isPosShiftOpen: () => !!fyo.singles.POSSettings?.isShiftOpen,
itemVisibility() {
return this.itemVisibilityValue;
},
disablePayButton(): boolean {
if (!this.sinvDoc.items?.length || !this.sinvDoc.party) {
return true;
@@ -295,6 +308,7 @@ export default defineComponent({
async mounted() {
await this.setItems();
await this.loadPOSProfile();
this.itemVisibilityValue = await getItemVisibility(this.fyo);
},
async activated() {
toggleSidebar(false);
@@ -317,6 +331,9 @@ export default defineComponent({
setQuickQtySelectedRow(row: SalesInvoiceItem) {
this.quickQtyRow = row;
},
setExpandedBatchId(rowName: string | null) {
this.expandedBatchId = rowName;
},
addQuickQtyListeners() {
this.quickQtyKeyDownHandler = (e: KeyboardEvent) =>
this.onQuickQtyKeyDown(e);
@@ -495,7 +512,21 @@ export default defineComponent({
filters: { name: value },
});
this.loyaltyProgram = party[0]?.loyaltyProgram as string;
const loyaltyProgramName = party[0]?.loyaltyProgram as string;
if (loyaltyProgramName) {
const isExpiredAndMaxed = await isLoyaltyProgramExpiredAndMaxed(
this.fyo,
loyaltyProgramName
);
if (isExpiredAndMaxed) {
this.loyaltyProgram = loyaltyProgramName;
this.loyaltyPoints = 0;
return;
}
}
this.loyaltyProgram = loyaltyProgramName;
this.loyaltyPoints = party[0]?.loyaltyPoints as number;
},
@@ -644,7 +675,8 @@ export default defineComponent({
this.fyo.singles.AccountingSettings?.enablePriceList &&
this.loyaltyPoints &&
this.sinvDoc.party &&
this.sinvDoc.items?.length
this.sinvDoc.items?.length &&
this.loyaltyProgram
) {
this.toggleModal('LoyaltyProgram', true);
}
@@ -694,8 +726,11 @@ export default defineComponent({
if (itemVisibility === 'Inventory Items') {
filters.trackItem = true;
} else {
} else if (itemVisibility === 'ERP Sync Items') {
filters.datafromErp = true;
} else if (itemVisibility === 'Non-Inventory Items') {
filters.trackItem = false;
filters.datafromErp = false;
}
if (this.selectedItemGroup) {
+3 -1
View File
@@ -76,7 +76,9 @@
<div
class="relative group"
:class="{
hidden: !fyo.singles.AccountingSettings?.enableLoyaltyProgram,
hidden:
!fyo.singles.AccountingSettings?.enableLoyaltyProgram ||
!loyaltyProgram,
}"
>
<div
+14 -1
View File
@@ -192,6 +192,9 @@ export async function syncDocumentsFromERPNext(fyo: Fyo, shouldThrow = false) {
}
const docName = (doc.fbooksDocName || doc.name) as string;
if (getDocTypeName(doc) === ModelNameEnum.Item) {
doc.datafromErp = true;
}
try {
if ((doc.fbooksDocName as string) || (doc.name as string)) {
@@ -279,6 +282,10 @@ async function createNewDocument(
token: string,
deviceID: string
) {
if (getDocTypeName(doc) === ModelNameEnum.Item) {
doc.datafromErp = true;
}
const newDoc = fyo.doc.getNewDoc(getDocTypeName(doc), doc);
await performPreSync(fyo, doc);
await appendDocValues(newDoc as DocValueMap, doc);
@@ -508,8 +515,10 @@ async function updateExistingDocument(
token: string,
deviceID: string
) {
const docType = getDocTypeName(doc);
const existingDoc = await fyo.doc.getDoc(
getDocTypeName(doc),
docType,
(doc.fbooksDocName as string) || (doc.name as string)
);
@@ -602,6 +611,10 @@ export async function performInitialFullSync(fyo: Fyo) {
for (const doc of docsByType[docType]) {
const docName = (doc.fbooksDocName as string) || (doc.name as string);
try {
if (docType === ModelNameEnum.Item) {
doc.datafromErp = true;
}
const isDocExists = await fyo.db.exists(
docType,
(doc.fbooksDocName as string) || (doc.name as string)
+4 -4
View File
@@ -3835,10 +3835,10 @@ lodash.uniqby@4.5.0:
lodash._baseiteratee "~4.7.0"
lodash._baseuniq "~4.6.0"
lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.23:
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
log-symbols@^4.1.0:
version "4.1.0"