mirror of
https://github.com/frappe/books.git
synced 2026-08-24 02:24:17 -05:00
Merge pull request #1342 from Gadha2311/batch-selection
feat: enable selecting batches for batched items
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
getReturnQtyTotal,
|
||||
getReturnLoyaltyPoints,
|
||||
getItemQtyMap,
|
||||
getItemVisibility,
|
||||
} from 'models/helpers';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
import { validateBatch } from 'models/inventory/helpers';
|
||||
@@ -1329,17 +1330,17 @@ export abstract class Invoice extends Transactional {
|
||||
linkedEntries = await getLinkedEntries(sinvDoc);
|
||||
}
|
||||
|
||||
const itemVisibility = this.fyo.singles.POSSettings?.itemVisibility;
|
||||
const itemVisibility = await getItemVisibility(this.fyo);
|
||||
|
||||
if (!this.stockNotTransferred && itemVisibility === 'Inventory Items') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schemaName = this.stockTransferSchemaName;
|
||||
|
||||
const defaults = (this.fyo.singles.Defaults as Defaults) ?? {};
|
||||
let terms;
|
||||
let numberSeries;
|
||||
|
||||
if (this.isSales) {
|
||||
terms = defaults.shipmentTerms ?? '';
|
||||
numberSeries = defaults.shipmentNumberSeries ?? undefined;
|
||||
@@ -1367,6 +1368,7 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
|
||||
const transfer = this.fyo.doc.getNewDoc(schemaName, data) as StockTransfer;
|
||||
|
||||
for (const row of this.items ?? []) {
|
||||
if (!row.item) {
|
||||
continue;
|
||||
@@ -1398,6 +1400,7 @@ export abstract class Invoice extends Transactional {
|
||||
} else {
|
||||
quantity = row.quantity;
|
||||
}
|
||||
|
||||
const item = row.item;
|
||||
const batch = row.batch || null;
|
||||
const description = row.description;
|
||||
|
||||
@@ -27,6 +27,7 @@ export class Item extends Doc {
|
||||
for?: 'Purchases' | 'Sales' | 'Both';
|
||||
hasBatch?: boolean;
|
||||
itemGroup?: string;
|
||||
hsnCode?: number;
|
||||
hasSerialNumber?: boolean;
|
||||
uomConversions: UOMConversionItem[] = [];
|
||||
|
||||
|
||||
+16
-2
@@ -40,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, ItemVisibility, POSItem } from 'src/components/POS/types';
|
||||
import { ValuationMethod } from './inventory/types';
|
||||
import {
|
||||
getRawStockLedgerEntries,
|
||||
@@ -113,6 +113,20 @@ export async function getItemQtyMap(doc: SalesInvoice): Promise<ItemQtyMap> {
|
||||
return itemQtyMap;
|
||||
}
|
||||
|
||||
export async function getItemVisibility(fyo: Fyo): Promise<ItemVisibility> {
|
||||
const posProfileName = fyo.singles.POSSettings?.posProfile as string;
|
||||
|
||||
if (posProfileName) {
|
||||
const posProfile = await fyo.doc.getDoc(
|
||||
ModelNameEnum.POSProfile,
|
||||
posProfileName
|
||||
);
|
||||
return posProfile?.itemVisibility as ItemVisibility;
|
||||
}
|
||||
|
||||
return fyo.singles.POSSettings?.itemVisibility as ItemVisibility;
|
||||
}
|
||||
|
||||
export function getStockTransferActions(
|
||||
fyo: Fyo,
|
||||
schemaName: ModelNameEnum.Shipment | ModelNameEnum.PurchaseReceipt
|
||||
@@ -1000,7 +1014,7 @@ export async function removeLoyaltyPoint(doc: Doc) {
|
||||
|
||||
export async function validateQty(
|
||||
sinvDoc: SalesInvoice,
|
||||
item: Item | SalesInvoiceItem | undefined,
|
||||
item: Item | SalesInvoiceItem | POSItem | undefined,
|
||||
existingItems: InvoiceItem[]
|
||||
) {
|
||||
if (!item) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Money } from 'pesa';
|
||||
import { StockLedgerEntry } from './StockLedgerEntry';
|
||||
import { SMDetails, SMIDetails, SMTransferDetails } from './types';
|
||||
import { getSerialNumbers } from './helpers';
|
||||
import { getItemVisibility } from 'models/helpers';
|
||||
|
||||
export class StockManager {
|
||||
/**
|
||||
@@ -86,13 +87,13 @@ export class StockManager {
|
||||
|
||||
async #validate(details: SMIDetails) {
|
||||
this.#validateRate(details);
|
||||
this.#validateQuantity(details);
|
||||
await this.#validateQuantity(details);
|
||||
this.#validateLocation(details);
|
||||
await this.#validateStockAvailability(details);
|
||||
}
|
||||
|
||||
#validateQuantity(details: SMIDetails) {
|
||||
const itemVisibility = this.fyo.singles.POSSettings?.itemVisibility;
|
||||
async #validateQuantity(details: SMIDetails) {
|
||||
const itemVisibility = await getItemVisibility(this.fyo);
|
||||
if (itemVisibility !== 'Inventory Items') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ export interface BaseField {
|
||||
tab?: string; // UI Facing config, for grouping by tabs
|
||||
abstract?: string; // Used to mark the location of a field in an Abstract schema
|
||||
isCustom?: boolean; // Whether the field is a custom field
|
||||
filters?: Record<string, string>;
|
||||
getOptions?: () => Promise<{ label: string; value: string }[]>;
|
||||
}
|
||||
|
||||
export type SelectOption = { value: string; label: string };
|
||||
|
||||
@@ -189,6 +189,9 @@ export default {
|
||||
return getCreateFiltersFromListViewFilters(filters);
|
||||
},
|
||||
async getFilters() {
|
||||
if (this.df.filters) {
|
||||
return this.df.filters;
|
||||
}
|
||||
const { schemaName, fieldname } = this.df;
|
||||
const getFilters = fyo.models[schemaName]?.filters?.[fieldname];
|
||||
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
fieldtype: 'Link',
|
||||
target: 'Batch',
|
||||
label: t`Batch`,
|
||||
filters: { item: row.item as string},
|
||||
}"
|
||||
:value="row.batch"
|
||||
:border="true"
|
||||
@@ -241,7 +242,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="showAvlQuantityInBatch()" class="px-5 pt-6 col-span-2">
|
||||
<div v-if="showAvlQuantityInBatch" class="px-5 pt-6 col-span-2">
|
||||
<Float
|
||||
:df="{
|
||||
fieldname: 'availableQtyInBatch',
|
||||
@@ -289,17 +290,19 @@ 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 { getItemVisibility, validateQty } from 'models/helpers';
|
||||
import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { POSProfile } from 'models/baseModels/POSProfile/PosProfile';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SelectedItemRow',
|
||||
components: { Currency, Data, Float, Int, Link, Text },
|
||||
props: {
|
||||
row: { type: SalesInvoiceItem, required: true },
|
||||
batchAdded: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['runSinvFormulas', 'applyPricingRule', 'selectedRow'],
|
||||
setup() {
|
||||
@@ -315,12 +318,23 @@ export default defineComponent({
|
||||
isExapanded: false,
|
||||
batches: [] as string[],
|
||||
availableQtyInBatch: 0,
|
||||
|
||||
itemVisibility: '',
|
||||
defaultRate: this.row.rate as Money,
|
||||
profileDiscountSetting: null as boolean | null,
|
||||
profileRateSetting: null as boolean | null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'row.batch': {
|
||||
async handler(newBatch) {
|
||||
if (newBatch) {
|
||||
this.availableQtyInBatch = await this.getAvailableQtyInBatch();
|
||||
this.isExapanded = true;
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isUOMConversionEnabled(): boolean {
|
||||
return !!fyo.singles.InventorySettings?.enableUomConversions;
|
||||
@@ -331,6 +345,13 @@ export default defineComponent({
|
||||
isReadOnly() {
|
||||
return this.row.isFreeItem;
|
||||
},
|
||||
showAvlQuantityInBatch() {
|
||||
return (
|
||||
this.row.links?.item &&
|
||||
this.row.links?.item.hasBatch &&
|
||||
this.itemVisibility
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
@@ -349,11 +370,14 @@ export default defineComponent({
|
||||
this.profileRateSetting =
|
||||
!!profile?.canChangeRate ||
|
||||
!!this.fyo.singles.POSSettings?.canChangeRate;
|
||||
|
||||
this.itemVisibility = await getItemVisibility(this.fyo);
|
||||
} else {
|
||||
this.profileDiscountSetting =
|
||||
!!this.fyo.singles.POSSettings?.canEditDiscount;
|
||||
|
||||
this.profileRateSetting = !!this.fyo.singles.POSSettings?.canChangeRate;
|
||||
this.itemVisibility = await getItemVisibility(this.fyo);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -402,15 +426,6 @@ export default defineComponent({
|
||||
|
||||
return transferQty;
|
||||
},
|
||||
showAvlQuantityInBatch() {
|
||||
const itemVisibility = this.fyo.singles.POSSettings?.itemVisibility;
|
||||
|
||||
return (
|
||||
this.row.links?.item &&
|
||||
this.row.links?.item.hasBatch &&
|
||||
itemVisibility === 'Inventory Items'
|
||||
);
|
||||
},
|
||||
|
||||
isDiscountsReadOnly(isValidDiscount: boolean) {
|
||||
const canEditDiscount = this.profileDiscountSetting;
|
||||
@@ -419,7 +434,7 @@ export default defineComponent({
|
||||
},
|
||||
async setBatch(batch: string) {
|
||||
this.row.set('batch', batch);
|
||||
this.availableQtyInBatch = await this.getAvailableQtyInBatch();
|
||||
await this.getAvailableQtyInBatch();
|
||||
},
|
||||
setSerialNumber(serialNumber: string) {
|
||||
if (!serialNumber) {
|
||||
|
||||
@@ -10,6 +10,8 @@ export type ItemGroupMap = Record<string, string>;
|
||||
|
||||
export type DiscountType = 'percent' | 'amount';
|
||||
|
||||
export type ItemVisibility = 'Inventory Items' | 'Non-Inventory Items'
|
||||
|
||||
export const modalNames = [
|
||||
'Keyboard',
|
||||
'Payment',
|
||||
@@ -21,6 +23,7 @@ export const modalNames = [
|
||||
'PriceList',
|
||||
'ItemEnquiry',
|
||||
'ReturnSalesInvoice',
|
||||
'BatchSelection',
|
||||
] as const;
|
||||
|
||||
export type ModalName = typeof modalNames[number];
|
||||
@@ -44,13 +47,16 @@ export type PosEmits =
|
||||
| 'selectedReturnInvoice'
|
||||
| 'saveAndContinue'
|
||||
| 'handlePaymentAction'
|
||||
| 'setTransferClearanceDate';
|
||||
| 'setTransferClearanceDate'
|
||||
| 'batchSelected';
|
||||
|
||||
export interface POSItem {
|
||||
id?: number;
|
||||
image?: string;
|
||||
name: string;
|
||||
rate: Money;
|
||||
item?: string;
|
||||
batch?: string;
|
||||
availableQty: number;
|
||||
unit: string;
|
||||
hasBatch: boolean;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<Modal class="h-auto w-96" :set-close-listener="false">
|
||||
<p class="text-center font-semibold py-3 text-gray-800 dark:text-gray-200">
|
||||
{{ t`Select the Batch` }}
|
||||
</p>
|
||||
|
||||
<div class="px-10 pt-6">
|
||||
<Link
|
||||
:df="{
|
||||
fieldname: 'batch',
|
||||
fieldtype: 'Link',
|
||||
target: 'Batch',
|
||||
label: t`Batch`,
|
||||
required: true,
|
||||
getOptions: getBatchOptions,
|
||||
filters: { item: itemCode },
|
||||
}"
|
||||
:value="selectedBatch"
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
@change="(value: string) => selectedBatch = value"
|
||||
/>
|
||||
|
||||
<div class="mt-8 mb-6 grid grid-cols-2 gap-4">
|
||||
<Button
|
||||
class="w-full bg-green-500 dark:bg-green-700"
|
||||
style="padding: 1.35rem"
|
||||
:disabled="!selectedBatch"
|
||||
@click="submitSelection"
|
||||
>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Select` }}
|
||||
</p>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
class="w-full bg-red-500 dark:bg-red-700"
|
||||
style="padding: 1.35rem"
|
||||
@click="closeModal"
|
||||
>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { t } from 'fyo';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Link from 'src/components/Controls/Link.vue';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BatchSelectionModal',
|
||||
components: {
|
||||
Modal,
|
||||
Button,
|
||||
Link,
|
||||
},
|
||||
props: {
|
||||
itemCode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['toggleModal', 'batchSelected'],
|
||||
data() {
|
||||
return {
|
||||
selectedBatch: '' as string,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async getBatchOptions() {
|
||||
if (!this.itemCode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const batches = (await fyo.db.getAll(ModelNameEnum.Batch, {
|
||||
filters: { item: this.itemCode },
|
||||
fields: ['name'],
|
||||
})) as { name: string; itemCode: string }[];
|
||||
|
||||
return batches.map((b) => ({ label: b.name, value: b.name }));
|
||||
} catch (error) {
|
||||
showToast({ type: 'error', message: t`Failed to load batches` });
|
||||
return [];
|
||||
}
|
||||
},
|
||||
submitSelection() {
|
||||
this.$emit('batchSelected', this.selectedBatch);
|
||||
this.$emit('toggleModal', 'BatchSelection');
|
||||
this.selectedBatch = '';
|
||||
},
|
||||
closeModal() {
|
||||
this.$emit('toggleModal', 'BatchSelection');
|
||||
this.selectedBatch = '';
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -19,6 +19,13 @@
|
||||
@set-loyalty-points="(points) => emitEvent('setLoyaltyPoints', points)"
|
||||
/>
|
||||
|
||||
<BatchSelectionModal
|
||||
:open-modal="openBatchSelectionModal"
|
||||
:item-code="selectedItemForBatch"
|
||||
@toggle-modal="emitEvent('toggleModal', 'BatchSelection')"
|
||||
@batch-selected="(batch) => emitEvent('batchSelected', batch)"
|
||||
/>
|
||||
|
||||
<SavedInvoiceModal
|
||||
:open-modal="openSavedInvoiceModal"
|
||||
:modal-status="openSavedInvoiceModal"
|
||||
@@ -104,9 +111,7 @@
|
||||
:border="true"
|
||||
:value="itemSearchTerm"
|
||||
:show-clear-button="true"
|
||||
@keyup.enter="(item) =>
|
||||
emitEvent('handleItemSearch', item.target.value as string, true)
|
||||
"
|
||||
@keyup.enter="(event: KeyboardEvent) => emitEvent('handleItemSearch', (event.target as HTMLInputElement).value, true)"
|
||||
@change="(item: string) => emitEvent('handleItemSearch', item)"
|
||||
/>
|
||||
|
||||
@@ -395,6 +400,7 @@ import SelectedItemTable from 'src/components/POS/Classic/SelectedItemTable.vue'
|
||||
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
|
||||
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
|
||||
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
|
||||
import BatchSelectionModal from 'src/pages/POS/BatchSelectionModal.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ClassicPOS',
|
||||
@@ -418,6 +424,7 @@ export default defineComponent({
|
||||
FloatingLabelFloatInput,
|
||||
ReturnSalesInvoiceModal,
|
||||
FloatingLabelCurrencyInput,
|
||||
BatchSelectionModal,
|
||||
},
|
||||
props: {
|
||||
paidAmount: Money,
|
||||
@@ -435,6 +442,7 @@ export default defineComponent({
|
||||
openLoyaltyProgramModal: Boolean,
|
||||
openAppliedCouponsModal: Boolean,
|
||||
openReturnSalesInvoiceModal: Boolean,
|
||||
openBatchSelectionModal: Boolean,
|
||||
totalQuantity: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
@@ -480,6 +488,14 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
batchAddedItems: {
|
||||
type: Array as () => string[],
|
||||
default: () => [],
|
||||
},
|
||||
selectedItemForBatch: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'addItem',
|
||||
@@ -505,6 +521,7 @@ export default defineComponent({
|
||||
'saveAndContinue',
|
||||
'handlePaymentAction',
|
||||
'selectedRow',
|
||||
'batchSelected',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
|
||||
+104
-24
@@ -40,6 +40,8 @@
|
||||
:open-loyalty-program-modal="openLoyaltyProgramModal"
|
||||
:open-applied-coupons-modal="openAppliedCouponsModal"
|
||||
:open-return-sales-invoice-modal="openReturnSalesInvoiceModal"
|
||||
:open-batch-selection-modal="openBatchSelectionModal"
|
||||
:selected-item-for-batch="selectedItemForBatch"
|
||||
@add-item="addItem"
|
||||
@toggle-view="toggleView"
|
||||
@set-sinv-doc="setSinvDoc"
|
||||
@@ -64,6 +66,7 @@
|
||||
@save-and-continue="handleSaveAndContinue"
|
||||
@handle-payment-action="handlePaymentAction"
|
||||
@selected-row="setQuickQtySelectedRow"
|
||||
@batch-selected="handleBatchSelected"
|
||||
/>
|
||||
<ModernPOS
|
||||
v-else
|
||||
@@ -155,6 +158,7 @@ import {
|
||||
getPricingRule,
|
||||
removeFreeItems,
|
||||
getItemRateFromPriceList,
|
||||
getItemVisibility,
|
||||
} from 'models/helpers';
|
||||
import {
|
||||
POSItem,
|
||||
@@ -214,6 +218,7 @@ export default defineComponent({
|
||||
openLoyaltyProgramModal: false,
|
||||
openAppliedCouponsModal: false,
|
||||
openReturnSalesInvoiceModal: false,
|
||||
openBatchSelectionModal: false,
|
||||
|
||||
totalQuantity: 0,
|
||||
paidAmount: fyo.pesa(0),
|
||||
@@ -242,13 +247,13 @@ export default defineComponent({
|
||||
itemQtyMap: {} as ItemQtyMap,
|
||||
coupons: {} as AppliedCouponCodes,
|
||||
itemSerialNumbers: {} as ItemSerialNumbers,
|
||||
|
||||
// Quick Quantity via holding 'Q'
|
||||
quickQtyActive: false,
|
||||
quickQtyBuffer: '' as string,
|
||||
quickQtyRow: null as SalesInvoiceItem | null,
|
||||
quickQtyKeyDownHandler: null as ((e: KeyboardEvent) => void) | null,
|
||||
quickQtyKeyUpHandler: null as ((e: KeyboardEvent) => void) | null,
|
||||
selectedItemForBatch: '' as string,
|
||||
pendingBatchItem: null as { item: POSItem; quantity: number } | null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -416,7 +421,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// Determine target row: prefer explicitly selected row; else fallback to last non-free item
|
||||
let row: SalesInvoiceItem | null = this.quickQtyRow;
|
||||
let row = this.quickQtyRow as SalesInvoiceItem | null;
|
||||
if (!row || !(this.sinvDoc.items || []).includes(row)) {
|
||||
const items = (this.sinvDoc.items || []).filter((r) => !r.isFreeItem);
|
||||
row = items.length
|
||||
@@ -443,11 +448,11 @@ export default defineComponent({
|
||||
try {
|
||||
await row.set('quantity', qty);
|
||||
|
||||
const existingItems =
|
||||
(this.sinvDoc.items || []).filter(
|
||||
(invoiceItem: InvoiceItem) =>
|
||||
invoiceItem.item === row!.item && !invoiceItem.isFreeItem
|
||||
) || [];
|
||||
const existingItems = (this.sinvDoc.items || []).filter(
|
||||
(invoiceItem) =>
|
||||
(invoiceItem as InvoiceItem).item === row!.item &&
|
||||
!(invoiceItem as InvoiceItem).isFreeItem
|
||||
) as InvoiceItem[];
|
||||
|
||||
await validateQty(
|
||||
this.sinvDoc as SalesInvoice,
|
||||
@@ -673,9 +678,7 @@ export default defineComponent({
|
||||
},
|
||||
async setItems() {
|
||||
const filters: Record<string, boolean | string> = {};
|
||||
const itemVisibility =
|
||||
this.posProfile?.itemVisibility ??
|
||||
this.fyo.singles.POSSettings?.itemVisibility;
|
||||
const itemVisibility = await getItemVisibility(this.fyo);
|
||||
|
||||
const hideUnavailable =
|
||||
this.posProfile?.hideUnavailableItems ??
|
||||
@@ -833,7 +836,7 @@ export default defineComponent({
|
||||
);
|
||||
}
|
||||
},
|
||||
async addItem(item: POSItem | Item | undefined, quantity?: number) {
|
||||
async addItem(item: POSItem | undefined, quantity?: number) {
|
||||
try {
|
||||
await this.sinvDoc.runFormulas();
|
||||
this.validateInvoice();
|
||||
@@ -842,15 +845,23 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.hasBatch) {
|
||||
this.selectedItemForBatch = item.name;
|
||||
this.pendingBatchItem = { item, quantity: quantity ?? 1 };
|
||||
|
||||
this.toggleModal('BatchSelection', true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isInventoryItem = await this.fyo.getValue(
|
||||
ModelNameEnum.Item,
|
||||
item.name as string,
|
||||
item.name,
|
||||
'trackItem'
|
||||
);
|
||||
|
||||
if (isInventoryItem) {
|
||||
const availableQty =
|
||||
this.itemQtyMap[item.name as string]?.availableQty ?? 0;
|
||||
const availableQty = this.itemQtyMap[item.name]?.availableQty ?? 0;
|
||||
if (availableQty <= 0) {
|
||||
throw new ValidationError(
|
||||
t`Item is out of stock (quantity is zero)`
|
||||
@@ -866,13 +877,13 @@ export default defineComponent({
|
||||
|
||||
await validateQty(
|
||||
this.sinvDoc as SalesInvoice,
|
||||
item as Item,
|
||||
item,
|
||||
existingItems as InvoiceItem[]
|
||||
);
|
||||
|
||||
const itemsHsncode = (await this.fyo.getValue(
|
||||
'Item',
|
||||
item?.name as string,
|
||||
item?.name,
|
||||
'hsnCode'
|
||||
)) as number;
|
||||
|
||||
@@ -903,7 +914,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
await this.sinvDoc.append('items', {
|
||||
rate: item.rate as Money,
|
||||
rate: item.rate,
|
||||
item: item.name,
|
||||
quantity: addQty,
|
||||
hsnCode: itemsHsncode,
|
||||
@@ -915,14 +926,13 @@ export default defineComponent({
|
||||
|
||||
if (existingItems.length) {
|
||||
if (!this.sinvDoc.priceList) {
|
||||
existingItems[0].rate = item.rate as Money;
|
||||
existingItems[0].rate = item.rate;
|
||||
}
|
||||
|
||||
const currentQty = existingItems[0].quantity ?? 0;
|
||||
const addQty = quantity ?? 1;
|
||||
if (isInventoryItem) {
|
||||
const availableQty =
|
||||
this.itemQtyMap[item.name as string]?.availableQty ?? 0;
|
||||
const availableQty = this.itemQtyMap[item.name]?.availableQty ?? 0;
|
||||
if (currentQty + addQty > availableQty) {
|
||||
throw new ValidationError(
|
||||
'Cannot add more than the available quantity'
|
||||
@@ -936,7 +946,7 @@ export default defineComponent({
|
||||
if (isInventoryItem) {
|
||||
await validateQty(
|
||||
this.sinvDoc as SalesInvoice,
|
||||
item as Item,
|
||||
item,
|
||||
existingItems as InvoiceItem[]
|
||||
);
|
||||
}
|
||||
@@ -944,7 +954,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
await this.sinvDoc.append('items', {
|
||||
rate: item.rate as Money,
|
||||
rate: item.rate,
|
||||
item: item.name,
|
||||
quantity: quantity ? quantity : 1,
|
||||
hsnCode: itemsHsncode,
|
||||
@@ -971,13 +981,83 @@ export default defineComponent({
|
||||
});
|
||||
}
|
||||
},
|
||||
async handleBatchSelected(batchName: string) {
|
||||
if (!this.pendingBatchItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { item, quantity } = this.pendingBatchItem;
|
||||
this.pendingBatchItem = null;
|
||||
|
||||
try {
|
||||
const itemDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Item,
|
||||
item.name
|
||||
)) as Item;
|
||||
let availableQty = 0;
|
||||
if (itemDoc.trackItem) {
|
||||
availableQty =
|
||||
(await fyo.db.getStockQuantity(
|
||||
item.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
batchName
|
||||
)) ?? 0;
|
||||
|
||||
const itemIndex = this.items.findIndex((i) => i.name === item.name);
|
||||
if (itemIndex !== -1) {
|
||||
this.items[itemIndex].availableQty = availableQty ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
const existingItems =
|
||||
this.sinvDoc.items?.filter(
|
||||
(invoiceItem) =>
|
||||
invoiceItem.item === item.name &&
|
||||
invoiceItem.batch === batchName &&
|
||||
!invoiceItem.isFreeItem
|
||||
) ?? [];
|
||||
|
||||
await validateQty(
|
||||
this.sinvDoc as SalesInvoice,
|
||||
itemDoc,
|
||||
existingItems as InvoiceItem[]
|
||||
);
|
||||
|
||||
if (existingItems.length) {
|
||||
const currentQty = existingItems[0].quantity ?? 0;
|
||||
const addQty = quantity ?? 1;
|
||||
await existingItems[0].set('quantity', currentQty + addQty);
|
||||
} else {
|
||||
await this.sinvDoc.append('items', {
|
||||
rate: item.rate as Money,
|
||||
item: item.name,
|
||||
quantity: quantity ?? 1,
|
||||
hsnCode: itemDoc.hsnCode,
|
||||
batch: batchName,
|
||||
});
|
||||
}
|
||||
|
||||
await this.applyPricingRule();
|
||||
await this.sinvDoc.runFormulas();
|
||||
|
||||
await this.setItemQtyMap();
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async createTransaction(shouldPrint = false, isPay = false) {
|
||||
try {
|
||||
this.sinvDoc.date = new Date();
|
||||
await this.validate();
|
||||
await this.submitSinvDoc();
|
||||
|
||||
const itemVisibility = this.fyo.singles.POSSettings?.itemVisibility;
|
||||
const itemVisibility = await getItemVisibility(this.fyo);
|
||||
|
||||
if (
|
||||
this.sinvDoc.stockNotTransferred &&
|
||||
|
||||
@@ -329,7 +329,7 @@ export default defineComponent({
|
||||
return false;
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
async mounted() {
|
||||
await this.setPaymentMethods();
|
||||
},
|
||||
methods: {
|
||||
|
||||
Reference in New Issue
Block a user