2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 02:49:03 +00:00

Merge pull request #1043 from AbleKSaju/feat-quantity-alerts

feat: alert for exceeding available quantity
This commit is contained in:
Akshay 2024-12-10 11:14:17 +05:30 committed by GitHub
commit 6361df6e50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 125 additions and 48 deletions

View File

@ -843,7 +843,7 @@ export abstract class Invoice extends Transactional {
loyaltyProgram: () => !this.loyaltyProgram,
loyaltyPoints: () => !this.redeemLoyaltyPoints || this.isReturn,
redeemLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn,
coupons: () => !this.coupons?.length,
coupons: () => this.isSubmitted && !this.coupons?.length,
priceList: () =>
!this.fyo.singles.AccountingSettings?.enablePriceList ||
(!this.canEdit && !this.priceList),

View File

@ -28,6 +28,7 @@ import { Money } from 'pesa';
import { Party } from './baseModels/Party/Party';
import { PricingRule } from './baseModels/PricingRule/PricingRule';
import { Router } from 'vue-router';
import { Item } from 'models/baseModels/Item/Item';
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
import { SalesQuote } from './baseModels/SalesQuote/SalesQuote';
import { StockMovement } from './inventory/StockMovement';
@ -39,7 +40,7 @@ import { safeParseFloat } from 'utils/index';
import { PriceList } from './baseModels/PriceList/PriceList';
import { InvoiceItem } from './baseModels/InvoiceItem/InvoiceItem';
import { SalesInvoiceItem } from './baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { ItemQtyMap } from 'src/components/POS/types';
import { ItemQtyMap, POSItem } from 'src/components/POS/types';
import { ValuationMethod } from './inventory/types';
import {
getRawStockLedgerEntries,
@ -838,6 +839,40 @@ export async function removeLoyaltyPoint(doc: Doc) {
await party.updateLoyaltyPoints();
}
export async function validateQty(
sinvDoc: SalesInvoice,
item: POSItem | Item | undefined,
existingItems: InvoiceItem[]
) {
if (!item) {
return;
}
let itemName = item.name as string;
const itemQtyMap = await getItemQtyMap(sinvDoc);
if (item instanceof SalesInvoiceItem) {
itemName = item.item as string;
}
if (!itemQtyMap[itemName] || itemQtyMap[itemName].availableQty === 0) {
throw new ValidationError(t`Item ${itemName} has Zero Quantity`);
}
if (
(existingItems && !itemQtyMap[itemName]) ||
itemQtyMap[itemName].availableQty < (existingItems[0]?.quantity as number)
) {
existingItems[0].quantity = itemQtyMap[itemName].availableQty;
throw new ValidationError(
t`Item ${itemName} only has ${itemQtyMap[itemName].availableQty} Quantity`
);
}
return;
}
export async function getPricingRulesOfCoupons(
doc: SalesInvoice,
couponName?: string,

View File

@ -35,7 +35,7 @@
>
<Row
v-if="items"
v-for="row in items as any"
v-for="row in items as POSItem[]"
:ratio="ratio"
:border="true"
class="

View File

@ -275,6 +275,10 @@ import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoic
import { Money } from 'pesa';
import { DiscountType } from '../types';
import { validateSerialNumberCount } from 'src/utils/pos';
import { validateQty } from 'models/helpers';
import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { showToast } from 'src/utils/interactive';
export default defineComponent({
name: 'SelectedItemRow',
@ -359,6 +363,28 @@ export default defineComponent({
async setQuantity(quantity: number) {
this.row.set('quantity', quantity);
const existingItems =
(this.row.parentdoc as SalesInvoice).items?.filter(
(invoiceItem: InvoiceItem) =>
invoiceItem.item === this.row.item && !invoiceItem.isFreeItem
) ?? [];
try {
await validateQty(
this.row.parentdoc as SalesInvoice,
this.row,
existingItems
);
} catch (error) {
this.row.set('quantity', existingItems[0].stockNotTransferred);
return showToast({
type: 'error',
message: this.t`${error as string}`,
duration: 'short',
});
}
if (!this.row.isFreeItem) {
this.$emit('applyPricingRule');
this.$emit('runSinvFormulas');

View File

@ -320,6 +320,8 @@ import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { ValidationError } from 'fyo/utils/errors';
import { showToast } from 'src/utils/interactive';
import { validateQty } from 'models/helpers';
import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
export default defineComponent({
name: 'KeyboardModal',
@ -459,6 +461,19 @@ export default defineComponent({
}
if (this.selectedItemField === 'quantity') {
const existingItems =
this.sinvDoc.items?.filter(
(invoiceItem: InvoiceItem) =>
invoiceItem.item === this.selectedItemRow?.item &&
!invoiceItem.isFreeItem
) ?? [];
await validateQty(
this.sinvDoc,
this.selectedItemRow,
existingItems
);
this.$emit('applyPricingRule');
}
}

View File

@ -112,6 +112,7 @@ import { Shipment } from 'models/inventory/Shipment';
import { routeTo, toggleSidebar } from 'src/utils/ui';
import PageHeader from 'src/components/PageHeader.vue';
import { Payment } from 'models/baseModels/Payment/Payment';
import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
@ -124,11 +125,12 @@ import {
validateIsPosSettingsSet,
} from 'src/utils/pos';
import {
validateQty,
getItemQtyMap,
getPricingRule,
removeFreeItems,
getAddedLPWithGrandTotal,
getItemRateFromPriceList,
getItemQtyMap,
} from 'models/helpers';
import {
POSItem,
@ -398,68 +400,67 @@ export default defineComponent({
return;
}
if (
!this.itemQtyMap[item.name as string] ||
this.itemQtyMap[item.name as string].availableQty === 0
) {
showToast({
type: 'error',
message: t`Item ${item.name as string} has Zero Quantity`,
duration: 'short',
});
return;
}
const existingItems =
this.sinvDoc.items?.filter(
(invoiceItem) =>
invoiceItem.item === item.name && !invoiceItem.isFreeItem
) ?? [];
if (item.hasBatch) {
for (const invItem of existingItems) {
const itemQty = invItem.quantity ?? 0;
const qtyInBatch =
this.itemQtyMap[invItem.item as string][invItem.batch as string] ??
0;
try {
if (item.hasBatch) {
for (const invItem of existingItems) {
const itemQty = invItem.quantity ?? 0;
const qtyInBatch =
this.itemQtyMap[invItem.item as string][
invItem.batch as string
] ?? 0;
if (itemQty < qtyInBatch) {
invItem.quantity = (invItem.quantity as number) + 1;
invItem.rate = item.rate as Money;
if (itemQty < qtyInBatch) {
invItem.quantity = (invItem.quantity as number) + 1;
invItem.rate = item.rate as Money;
await this.applyPricingRule();
await this.sinvDoc.runFormulas();
await this.applyPricingRule();
await this.sinvDoc.runFormulas();
await validateQty(
this.sinvDoc as SalesInvoice,
item,
existingItems as InvoiceItem[]
);
return;
return;
}
}
}
try {
await this.sinvDoc.append('items', {
rate: item.rate as Money,
item: item.name,
});
} catch (error) {
showToast({
type: 'error',
message: t`${error as string}`,
});
return;
}
return;
}
if (existingItems.length) {
if (!this.sinvDoc.priceList) {
existingItems[0].rate = item.rate as Money;
}
if (existingItems.length) {
if (!this.sinvDoc.priceList) {
existingItems[0].rate = item.rate as Money;
existingItems[0].quantity = (existingItems[0].quantity as number) + 1;
await this.applyPricingRule();
await this.sinvDoc.runFormulas();
await validateQty(
this.sinvDoc as SalesInvoice,
item,
existingItems as InvoiceItem[]
);
return;
}
existingItems[0].quantity = (existingItems[0].quantity as number) + 1;
await this.applyPricingRule();
await this.sinvDoc.runFormulas();
return;
} catch (error) {
return showToast({
type: 'error',
message: t`${error as string}`,
});
}
await this.sinvDoc.append('items', {

View File

@ -33,7 +33,7 @@
style="height: 65vh; width: 60vh"
>
<Row
v-for="row in savedInvoices as any"
v-for="row in savedInvoices as SalesInvoice[]"
:key="row.name"
:ratio="ratio"
:border="true"