2
0
mirror of https://github.com/frappe/books.git synced 2025-02-02 20:18:26 +00:00

Merge pull request #1000 from AbleKSaju/feat-newPos-UI

feat: POS modern UI
This commit is contained in:
Akshay 2024-11-27 12:12:49 +05:30 committed by GitHub
commit 8d74236d37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 2799 additions and 674 deletions

View File

@ -1,5 +1,7 @@
import { Fyo, t } from 'fyo'; import {
import { Doc } from 'fyo/model/doc'; AccountRootType,
AccountRootTypeEnum,
} from './baseModels/Account/types';
import { import {
Action, Action,
ColumnConfig, ColumnConfig,
@ -7,31 +9,30 @@ import {
LeadStatus, LeadStatus,
RenderData, RenderData,
} from 'fyo/model/types'; } from 'fyo/model/types';
import { Fyo, t } from 'fyo';
import { InvoiceStatus, ModelNameEnum } from './types';
import { ApplicablePricingRules } from './baseModels/Invoice/types';
import { AppliedCouponCodes } from './baseModels/AppliedCouponCodes/AppliedCouponCodes';
import { CollectionRulesItems } from './baseModels/CollectionRulesItems/CollectionRulesItems';
import { CouponCode } from './baseModels/CouponCode/CouponCode';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Money } from 'pesa'; import { Doc } from 'fyo/model/doc';
import { safeParseFloat } from 'utils/index';
import { Router } from 'vue-router';
import {
AccountRootType,
AccountRootTypeEnum,
} from './baseModels/Account/types';
import { numberSeriesDefaultsMap } from './baseModels/Defaults/Defaults';
import { Invoice } from './baseModels/Invoice/Invoice'; import { Invoice } from './baseModels/Invoice/Invoice';
import { Lead } from './baseModels/Lead/Lead';
import { LoyaltyProgram } from './baseModels/LoyaltyProgram/LoyaltyProgram';
import { Money } from 'pesa';
import { Party } from './baseModels/Party/Party';
import { PricingRule } from './baseModels/PricingRule/PricingRule';
import { Router } from 'vue-router';
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
import { SalesQuote } from './baseModels/SalesQuote/SalesQuote'; import { SalesQuote } from './baseModels/SalesQuote/SalesQuote';
import { StockMovement } from './inventory/StockMovement'; import { StockMovement } from './inventory/StockMovement';
import { StockTransfer } from './inventory/StockTransfer'; import { StockTransfer } from './inventory/StockTransfer';
import { InvoiceStatus, ModelNameEnum } from './types';
import { Lead } from './baseModels/Lead/Lead';
import { PricingRule } from './baseModels/PricingRule/PricingRule';
import { ApplicablePricingRules } from './baseModels/Invoice/types';
import { LoyaltyProgram } from './baseModels/LoyaltyProgram/LoyaltyProgram';
import { CollectionRulesItems } from './baseModels/CollectionRulesItems/CollectionRulesItems';
import { isPesa } from 'fyo/utils';
import { Party } from './baseModels/Party/Party';
import { CouponCode } from './baseModels/CouponCode/CouponCode';
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
import { AppliedCouponCodes } from './baseModels/AppliedCouponCodes/AppliedCouponCodes';
import { ValidationError } from 'fyo/utils/errors'; import { ValidationError } from 'fyo/utils/errors';
import { isPesa } from 'fyo/utils';
import { numberSeriesDefaultsMap } from './baseModels/Defaults/Defaults';
import { safeParseFloat } from 'utils/index';
export function getQuoteActions( export function getQuoteActions(
fyo: Fyo, fyo: Fyo,
@ -763,6 +764,19 @@ export function getLoyaltyProgramTier(
return loyaltyProgramTier; return loyaltyProgramTier;
} }
export async function updatePricingRuleItem(doc: SalesInvoice) {
const pricingRule = (await getPricingRule(doc)) as ApplicablePricingRules[];
let appliedPricingRuleCount = doc?.pricingRuleDetail?.length;
if (appliedPricingRuleCount !== pricingRule?.length) {
appliedPricingRuleCount = pricingRule?.length;
await doc?.appendPricingRuleDetail(pricingRule);
await doc?.applyProductDiscount();
}
}
export async function removeLoyaltyPoint(doc: Doc) { export async function removeLoyaltyPoint(doc: Doc) {
if (!doc.loyaltyProgram) { if (!doc.loyaltyProgram) {
return; return;

View File

@ -9,6 +9,7 @@ export class POSSettings extends Doc {
inventory?: string; inventory?: string;
cashAccount?: string; cashAccount?: string;
writeOffAccount?: string; writeOffAccount?: string;
posUI?: 'Classic' | 'Modern';
static filters: FiltersMap = { static filters: FiltersMap = {
cashAccount: () => ({ cashAccount: () => ({

View File

@ -31,6 +31,24 @@
"create": true, "create": true,
"default": "Write Off", "default": "Write Off",
"section": "Default" "section": "Default"
},
{
"fieldname": "posUI",
"label": "Pos Ui",
"fieldtype": "Select",
"options": [
{
"value": "Classic",
"label": "Classic"
},
{
"value": "Modern",
"label": "Modern"
}
],
"default": "Classic",
"required": true,
"section": "Default"
} }
] ]
} }

View File

@ -1,16 +1,15 @@
<template> <template>
<div <div
class=" class="
px-2
w-36
flex flex
items-center items-center
border border
w-36
rounded rounded
px-2
bg-gray-50 bg-gray-50
dark:bg-gray-890 dark:border-gray-800 dark:bg-gray-890 dark:focus-within:bg-gray-900
focus-within:bg-gray-100 focus-within:bg-gray-100
dark:focus-within:bg-gray-900
" "
> >
<input <input

View File

@ -15,7 +15,7 @@
:tabindex="isReadOnly ? '-1' : '0'" :tabindex="isReadOnly ? '-1' : '0'"
@blur="onBlur" @blur="onBlur"
@focus="onFocus" @focus="onFocus"
@input="(e) => $emit('input', e)" @input="(e:Event) => $emit('input', e)"
/> />
<div <div
v-show="!showInput" v-show="!showInput"
@ -47,6 +47,17 @@ export default defineComponent({
currencySymbol: '', currencySymbol: '',
}; };
}, },
props: {
focusInput: Boolean,
},
created() {
if (this.focusInput) {
this.showInput = true;
nextTick(() => {
this.focus();
});
}
},
computed: { computed: {
formattedValue() { formattedValue() {
const value = this.parse(this.value); const value = this.parse(this.value);

View File

@ -1,51 +1,61 @@
<template> <template>
<div <div
class=" class="
flex flex-col
gap-4 gap-4
p-4 py-2
w-full
flex flex-col
items-center items-center
mt-4
px-2
rounded-t-md rounded-t-md
text-black text-black
w-full overflow-y-auto
custom-scroll custom-scroll-thumb2
" "
style="height: 80vh" style="height: 83vh"
> >
<!-- Items Grid --> <!-- Items Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 w-full"> <div
class="
gap-2
w-full
grid grid-cols-1
md:grid-cols-2
lg:grid-cols-3
xl:grid-cols-4
"
>
<div <div
class=" class="
border border-gray-300
dark:border-gray-800
p-1 p-1
border border-gray-300
flex flex-col flex flex-col
text-sm text-center text-sm text-center
dark:border-gray-800
" "
@click="handleChange(item as POSItem)" @click="handleChange(item as POSItem)"
v-for="item in items as POSItem[]" v-for="item in items as POSItem[]"
:key="item.name" :key="item.name"
> >
<div class="self-center w-32 h-32 rounded-lg mb-1"> <div class="self-center w-32 h-32 p-1 rounded-lg">
<div class="relative"> <div class="relative w-full h-full p-2">
<img <img
v-if="item.image" v-if="item.image"
:src="item.image" :src="item.image"
alt="" alt=""
class="rounded-lg w-32 h-32 object-cover" class="rounded-lg w-full h-full object-cover"
/> />
<div <div
v-else v-else
class=" class="
rounded-lg rounded-lg
w-32 w-full
h-32 h-full
flex
bg-gray-100 bg-gray-100
dark:bg-gray-850 flex
justify-center justify-center
items-center items-center
dark:bg-gray-850
" "
> >
<p class="text-4xl font-semibold text-gray-400 select-none"> <p class="text-4xl font-semibold text-gray-400 select-none">
@ -53,7 +63,17 @@
</p> </p>
</div> </div>
<p <p
class="absolute top-1 right-1 rounded-full p-1" class="
absolute
top-1
right-1
rounded-full
w-6
h-6
flex
justify-center
items-center
"
:class=" :class="
item.availableQty > 0 item.availableQty > 0
? 'bg-green-100 text-green-900' ? 'bg-green-100 text-green-900'
@ -79,8 +99,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { fyo } from 'src/initFyo'; import { POSItem } from '../types';
import { POSItem } from './types';
export default defineComponent({ export default defineComponent({
name: 'ItemsGrid', name: 'ItemsGrid',

View File

@ -29,7 +29,10 @@
</div> </div>
</Row> </Row>
<div class="overflow-y-auto" style="height: 80vh"> <div
class="overflow-y-auto custom-scroll custom-scroll-thumb2"
style="height: 70vh"
>
<Row <Row
v-if="items" v-if="items"
v-for="row in items as any" v-for="row in items as any"
@ -64,12 +67,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import FormControl from '../Controls/FormControl.vue'; import FormControl from 'src/components/Controls/FormControl.vue';
import Row from 'src/components/Row.vue'; import Row from 'src/components/Row.vue';
import { isNumeric } from 'src/utils'; import { isNumeric } from 'src/utils';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { Field } from 'schemas/types'; import { Field } from 'schemas/types';
import { POSItem } from './types'; import { POSItem } from '../types';
export default defineComponent({ export default defineComponent({
name: 'ItemsTable', name: 'ItemsTable',

View File

@ -262,18 +262,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Currency from '../Controls/Currency.vue'; import Currency from 'src/components/Controls/Currency.vue';
import Data from '../Controls/Data.vue'; import Data from 'src/components/Controls/Data.vue';
import Float from '../Controls/Float.vue'; import Float from 'src/components/Controls/Float.vue';
import Int from '../Controls/Int.vue'; import Int from 'src/components/Controls/Int.vue';
import Link from '../Controls/Link.vue'; import Link from 'src/components/Controls/Link.vue';
import Text from '../Controls/Text.vue'; import Text from 'src/components/Controls/Text.vue';
import { inject } from 'vue'; import { inject } from 'vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem'; import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { DiscountType } from './types'; import { DiscountType } from '../types';
import { validateSerialNumberCount } from 'src/utils/pos'; import { validateSerialNumberCount } from 'src/utils/pos';
import { getPricingRule } from 'models/helpers'; import { getPricingRule } from 'models/helpers';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice'; import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';

View File

@ -11,7 +11,7 @@
w-full w-full
flex flex
items-center items-center
mt-4 mt-2
" "
> >
<div <div
@ -60,10 +60,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import FormContainer from '../FormContainer.vue'; import FormContainer from 'src/components/FormContainer.vue';
import FormControl from '../Controls/FormControl.vue'; import FormControl from 'src/components/Controls/FormControl.vue';
import Link from '../Controls/Link.vue'; import Link from 'src/components/Controls/Link.vue';
import Row from '../Row.vue'; import Row from 'src/components/Row.vue';
import RowEditForm from 'src/pages/CommonForm/RowEditForm.vue'; import RowEditForm from 'src/pages/CommonForm/RowEditForm.vue';
import SelectedItemRow from './SelectedItemRow.vue'; import SelectedItemRow from './SelectedItemRow.vue';
import { isNumeric } from 'src/utils'; import { isNumeric } from 'src/utils';

View File

@ -0,0 +1,131 @@
<template>
<div
class="
flex flex-col
items-center
gap-4
my-3
px-4
py-2
rounded-t-md
text-black
w-full
overflow-y-auto
custom-scroll custom-scroll-thumb2
"
style="height: 80vh"
>
<!-- Items Grid -->
<div
class="
gap-2
w-full
grid grid-cols-1
sm:grid-cols-2
md:grid-cols-4
lg:grid-cols-6
xl:grid-cols-7'
"
>
<div
class="
p-1
border border-gray-300
dark:border-gray-800
flex flex-col
text-sm text-center
"
@click="handleChange(item as POSItem)"
v-for="item in items as POSItem[]"
:key="item.name"
>
<div class="self-center w-32 h-32 p-1 rounded-lg">
<div class="relative w-full h-full p-2">
<img
v-if="item.image"
:src="item.image"
alt=""
class="rounded-lg w-full h-full object-cover"
/>
<div
v-else
class="
rounded-lg
bg-gray-100
w-full
h-full
flex
justify-center
items-center
dark:bg-gray-850
"
>
<p class="text-4xl font-semibold text-gray-400 select-none">
{{ getExtractedWords(item.name) }}
</p>
</div>
<p
class="
w-6
h-6
top-1
right-1
absolute
rounded-full
flex
justify-center
items-center
"
:class="
item.availableQty > 0
? 'bg-green-100 text-green-900'
: 'bg-red-100 text-red-900'
"
>
{{ item.availableQty }}
</p>
</div>
</div>
<h3 class="text-lg font-medium dark:text-white">{{ item.name }}</h3>
<p class="text-lg font-medium dark:text-white">
{{
item.rate ? fyo.currencySymbols[item.rate.getCurrency()] : undefined
}}
{{ item.rate }}
</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { POSItem } from '../types';
export default defineComponent({
name: 'ModernPOSItemsGrid',
emits: ['addItem', 'updateValues'],
props: {
items: {
type: Array,
},
itemQtyMap: {
type: Object,
},
},
methods: {
getExtractedWords(item: string) {
const initials = item.split(' ').map((word) => {
return word[0].toUpperCase();
});
return initials.join('');
},
handleChange(value: POSItem) {
this.$emit('addItem', value);
this.$emit('updateValues');
},
},
});
</script>

View File

@ -0,0 +1,192 @@
<template>
<div class="flex gap-2">
<div
class="w-1/2 overflow-y-auto custom-scroll custom-scroll-thumb2"
style="height: 81vh"
>
<Row
:ratio="ratio"
class="
mt-2
px-2
w-full
flex
items-center
border
rounded-t-md
text-gray-600
dark:border-gray-800 dark:text-gray-400
"
>
<div
v-for="df in tableFields"
:key="df.fieldname"
class="flex items-center p-2 text-lg"
:class="{
'ms-auto': isNumeric(df as Field),
}"
>
{{ df.label }}
</div>
</Row>
<Row
v-for="row in firstColumnItems as POSItem[]"
:key="row.id"
:ratio="ratio"
:border="true"
class="
px-2
w-full
border-b border-x
flex
items-center
justify-center
group
h-row-mid
hover:bg-gray-25
dark:border-gray-800 dark:bg-gray-890
"
@click="handleChange(row)"
>
<FormControl
v-for="df in tableFields"
:key="df.fieldname"
size="large"
:df="df"
:value="row[df.fieldname]"
:readOnly="true"
/>
</Row>
</div>
<div
class="w-1/2 overflow-y-auto custom-scroll custom-scroll-thumb2"
style="height: calc(80vh - 20rem)"
>
<Row
:ratio="ratio"
class="
mt-2
px-2
w-full
flex
items-center
border
rounded-t-md
text-gray-600
dark:border-gray-800 dark:text-gray-400
"
>
<div
v-for="df in tableFields"
:key="df.fieldname"
class="flex items-center p-2 text-lg"
:class="{
'ms-auto': isNumeric(df as Field),
}"
>
{{ df.label }}
</div>
</Row>
<Row
v-for="row in secondColumnItems as POSItem[]"
:key="row.id"
:ratio="ratio"
:border="true"
class="
px-2
w-full
border-b border-x
flex
items-center
justify-center
group
h-row-mid
hover:bg-gray-25
dark:bg-gray-890 dark:border-gray-800
"
@click="handleChange(row)"
>
<FormControl
v-for="df in tableFields"
:key="df.fieldname"
size="large"
:df="df"
:value="row[df.fieldname]"
:readOnly="true"
/>
</Row>
</div>
</div>
</template>
<script lang="ts">
import FormControl from 'src/components/Controls/FormControl.vue';
import Row from 'src/components/Row.vue';
import { isNumeric } from 'src/utils';
import { defineComponent } from 'vue';
import { Field } from 'schemas/types';
import { POSItem } from '../types';
export default defineComponent({
name: 'ModernPOSItemsTable',
components: { FormControl, Row },
emits: ['addItem', 'updateValues'],
props: {
items: Array,
itemQtyMap: Object,
},
computed: {
ratio() {
return [1, 1, 0.6, 0.7];
},
tableFields() {
return [
{
fieldname: 'name',
fieldtype: 'Data',
label: 'Item',
placeholder: 'Item',
readOnly: true,
},
{
fieldname: 'rate',
label: 'Rate',
placeholder: 'Rate',
fieldtype: 'Currency',
readOnly: true,
},
{
fieldname: 'availableQty',
label: 'Qty',
placeholder: 'Available Qty',
fieldtype: 'Float',
readOnly: true,
},
{
fieldname: 'unit',
label: 'Unit',
placeholder: 'Unit',
fieldtype: 'Data',
target: 'UOM',
readOnly: true,
},
] as Field[];
},
firstColumnItems() {
return this.items?.slice(0, Math.ceil(this.items.length / 2));
},
secondColumnItems() {
return this.items?.slice(Math.ceil(this.items.length / 2));
},
},
methods: {
handleChange(value: POSItem) {
this.$emit('addItem', value);
this.$emit('updateValues');
},
isNumeric,
},
});
</script>

View File

@ -0,0 +1,339 @@
<template>
<div>
<feather-icon
:name="isExapanded ? 'chevron-up' : 'chevron-down'"
class="w-4 h-4 inline-flex"
@click="isExapanded = !isExapanded"
/>
</div>
<div class="relative" @click="isExapanded = !isExapanded">
<Link
:df="{
fieldname: 'item',
fieldtype: 'Data',
label: 'item',
}"
:class="row.isFreeItem ? 'mt-2' : ''"
size="small"
:border="false"
:value="row.item"
:read-only="true"
/>
<p
v-if="row.isFreeItem"
class="absolute flex top-0 font-medium text-xs ml-2 text-green-800"
style="font-size: 0.6rem"
>
{{ row.pricingRule }}
</p>
</div>
<Int
:df="{
fieldname: 'quantity',
fieldtype: 'Int',
label: 'Quantity',
}"
size="small"
:border="false"
:value="row.quantity"
:read-only="true"
/>
<Currency
:df="{
fieldtype: 'Currency',
fieldname: 'rate',
label: 'rate',
}"
size="small"
:border="false"
:value="row.rate"
:read-only="true"
/>
<Currency
:df="{
fieldtype: 'Currency',
fieldname: 'amount',
label: 'Amount',
}"
size="small"
:border="false"
:value="row.amount"
:read-only="true"
/>
<div class="flex justify-center">
<feather-icon
name="trash"
class="w-4 text-xl text-red-500"
@click="removeAddedItem(row)"
/>
</div>
<div></div>
<template v-if="isExapanded">
<div class="rounded-md grid grid-cols-4 my-3" style="width: 27vw">
<div class="px-4 col-span-2">
<Float
:df="{
fieldname: 'quantity',
fieldtype: 'Float',
label: 'Quantity',
}"
@click="handleOpenKeyboard(row, 'quantity')"
size="medium"
:min="0"
:border="true"
:show-label="true"
:value="row.quantity"
:read-only="isReadOnly"
/>
</div>
<div class="px-4 col-span-2">
<Link
v-if="isUOMConversionEnabled"
:df="{
fieldname: 'transferUnit',
fieldtype: 'Link',
target: 'UOM',
label: t`Transfer Unit`,
}"
size="medium"
:show-label="true"
:border="true"
:value="row.transferUnit"
:read-only="isReadOnly"
/>
</div>
<div class="px-4 pt-6 col-span-2">
<Int
v-if="isUOMConversionEnabled"
:df="{
fieldtype: 'Int',
fieldname: 'transferQuantity',
label: 'Transfer Quantity',
}"
@click="!isReadOnly && handleOpenKeyboard(row, 'transferQuantity')"
size="medium"
:border="true"
:show-label="true"
:value="row.transferQuantity"
:read-only="isReadOnly"
/>
</div>
<div class="px-4 pt-6 col-span-2">
<Currency
:df="{
fieldtype: 'Currency',
fieldname: 'rate',
label: 'Rate',
}"
@click="!isReadOnly && handleOpenKeyboard(row, 'rate')"
size="medium"
:show-label="true"
:border="true"
:value="row.rate"
:read-only="isReadOnly"
/>
</div>
<div class="px-4 col-span-2 mt-5">
<Currency
v-if="isDiscountingEnabled"
:df="{
fieldtype: 'Currency',
fieldname: 'discountAmount',
label: 'Discount Amount',
}"
@click="handleOpenKeyboard(row, 'itemDiscountAmount')"
class="col-span-2"
size="medium"
:show-label="true"
:border="true"
:value="row.itemDiscountAmount"
:read-only="row.itemDiscountPercent as number > 0 || isReadOnly"
/>
</div>
<div class="px-4 col-span-2 mt-5">
<Float
v-if="isDiscountingEnabled"
:df="{
fieldtype: 'Float',
fieldname: 'itemDiscountPercent',
label: 'Discount Percent',
}"
@click="handleOpenKeyboard(row, 'itemDiscountPercent')"
size="medium"
:show-label="true"
:border="true"
:value="row.itemDiscountPercent"
:read-only="!row.itemDiscountAmount?.isZero() || isReadOnly"
/>
</div>
<div
v-if="row.links?.item && row.links?.item.hasBatch"
class="px-4 pt-6 col-span-2"
>
<Link
:df="{
fieldname: 'batch',
fieldtype: 'Link',
target: 'Batch',
label: t`Batch`,
}"
size="medium"
:value="row.batch"
:border="true"
:show-label="true"
:read-only="false"
@change="(value:string) => setBatch(value)"
/>
</div>
<div
v-if="row.links?.item && row.links?.item.hasBatch"
class="px-4 pt-6 col-span-2"
>
<Float
:df="{
fieldname: 'availableQtyInBatch',
fieldtype: 'Float',
label: t`Qty in Batch`,
}"
size="medium"
:min="0"
:value="availableQtyInBatch"
:show-label="true"
:border="true"
:read-only="true"
:text-right="true"
/>
</div>
<div v-if="hasSerialNumber" class="px-4 pt-6 col-span-4">
<Text
:df="{
label: t`Serial Number`,
fieldtype: 'Text',
fieldname: 'serialNumber',
}"
:value="row.serialNumber"
:show-label="true"
:border="true"
:required="hasSerialNumber"
@change="(value:string)=> setSerialNumber(value)"
/>
</div>
</div>
</template>
</template>
<script lang="ts">
import Currency from 'src/components/Controls/Currency.vue';
import Data from 'src/components/Controls/Data.vue';
import Float from 'src/components/Controls/Float.vue';
import Int from 'src/components/Controls/Int.vue';
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 { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { Money } from 'pesa';
import { validateSerialNumberCount } from 'src/utils/pos';
import { updatePricingRuleItem } from 'models/helpers';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
export default defineComponent({
name: 'ModernPOSSelectedItemRow',
components: { Currency, Data, Float, Int, Link, Text },
props: {
row: { type: SalesInvoiceItem, required: true },
},
emits: ['toggleModal', 'runSinvFormulas', 'selectedRow'],
setup() {
return {
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
itemSerialNumbers: inject('itemSerialNumbers') as {
[item: string]: string;
},
};
},
data() {
return {
isExapanded: false,
batches: [] as string[],
availableQtyInBatch: 0,
defaultRate: this.row.rate as Money,
};
},
computed: {
isUOMConversionEnabled(): boolean {
return !!fyo.singles.InventorySettings?.enableUomConversions;
},
hasSerialNumber(): boolean {
return !!(this.row.links?.item && this.row.links?.item.hasSerialNumber);
},
isReadOnly() {
return this.row.isFreeItem;
},
},
methods: {
handleOpenKeyboard(row: SalesInvoiceItem, field: string) {
if (this.isReadOnly) {
return;
}
this.$emit('selectedRow', row, field);
this.$emit('toggleModal', 'Keyboard');
},
async getAvailableQtyInBatch(): Promise<number> {
if (!this.row.batch) {
return 0;
}
return (
(await fyo.db.getStockQuantity(
this.row.item as string,
undefined,
undefined,
undefined,
this.row.batch
)) ?? 0
);
},
async setBatch(batch: string) {
this.row.set('batch', batch);
this.availableQtyInBatch = await this.getAvailableQtyInBatch();
},
setSerialNumber(serialNumber: string) {
if (!serialNumber) {
return;
}
this.itemSerialNumbers[this.row.item as string] = serialNumber;
validateSerialNumberCount(
serialNumber,
this.row.quantity ?? 0,
this.row.item!
);
},
async removeAddedItem(row: SalesInvoiceItem) {
this.row.parentdoc?.remove('items', row?.idx as number);
if (!row.isFreeItem) {
await updatePricingRuleItem(this.row.parentdoc as SalesInvoice);
}
},
},
});
</script>

View File

@ -0,0 +1,150 @@
<template>
<Row
:ratio="ratio"
class="
w-full
px-2
mt-2
border
rounded-t
text-gray-600
dark:border-gray-800 dark:text-gray-400
"
>
<div
v-if="tableFields"
v-for="df in tableFields"
:key="df.fieldname"
class="text-lg flex m-2"
:class="{
'ms-auto': isNumeric(df as Field),
}"
>
{{ df.label }}
</div>
</Row>
<div
class="overflow-auto custom-scroll custom-scroll-thumb1"
style="height: calc(90vh - 25rem)"
>
<Row
v-for="row in sinvDoc.items"
:ratio="ratio"
class="
p-2
border
w-full
hover:bg-gray-25
dark:border-gray-800 dark:bg-gray-890
"
>
<ModernPOSSelectedItemRow
:row="(row as SalesInvoiceItem)"
@selected-row="selectedItemRow"
@run-sinv-formulas="runSinvFormulas"
@toggle-modal="handleToggleModal"
/>
</Row>
</div>
</template>
<script lang="ts">
import FormContainer from 'src/components/FormContainer.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import Link from 'src/components/Controls/Link.vue';
import Row from 'src/components/Row.vue';
import RowEditForm from 'src/pages/CommonForm/RowEditForm.vue';
import ModernPOSSelectedItemRow from './ModernPOSSelectedItemRow.vue';
import { isNumeric } from 'src/utils';
import { inject, defineComponent } from 'vue';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { Field } from 'schemas/types';
export default defineComponent({
name: 'ModernPOSSelectedItemTable',
components: {
FormContainer,
FormControl,
Link,
Row,
RowEditForm,
ModernPOSSelectedItemRow,
},
setup() {
return {
sinvDoc: inject('sinvDoc') as SalesInvoice,
};
},
data() {
return {
isExapanded: false,
};
},
emits: ['toggleModal', 'selectedRow'],
computed: {
ratio() {
return [0.1, 0.8, 0.4, 0.8, 0.8, 0.3];
},
tableFields() {
return [
{
fieldname: 'toggler',
fieldtype: 'Link',
label: ' ',
},
{
fieldname: 'item',
fieldtype: 'Link',
label: 'Item',
placeholder: 'Item',
required: true,
schemaName: 'Item',
},
{
fieldname: 'quantity',
label: 'Quantity',
placeholder: 'Quantity',
fieldtype: 'Int',
required: true,
schemaName: '',
},
{
fieldname: 'rate',
label: 'Rate',
placeholder: 'Rate',
fieldtype: 'Currency',
required: true,
schemaName: '',
},
{
fieldname: 'amount',
label: 'Amount',
placeholder: 'Amount',
fieldtype: 'Currency',
required: true,
schemaName: '',
},
{
fieldname: 'removeItem',
fieldtype: 'Link',
label: ' ',
},
];
},
},
methods: {
handleToggleModal(modal: string) {
this.$emit('toggleModal', modal);
},
async runSinvFormulas() {
await this.sinvDoc.runFormulas();
},
selectedItemRow(row: SalesInvoiceItem, field: string) {
this.$emit('selectedRow', row, field);
},
isNumeric,
},
});
</script>

View File

@ -9,15 +9,16 @@ export type ItemSerialNumbers = { [item: string]: string };
export type DiscountType = 'percent' | 'amount'; export type DiscountType = 'percent' | 'amount';
export type ModalName = export type ModalName =
| 'ShiftOpen' | 'Keyboard'
| 'ShiftClose'
| 'Payment' | 'Payment'
| 'ShiftClose'
| 'LoyaltyProgram' | 'LoyaltyProgram'
| 'SavedInvoice' | 'SavedInvoice'
| 'RouteToInvoiceList' | 'Alert'
| 'CouponCode'; | 'CouponCode';
export interface POSItem { export interface POSItem {
id?: number;
image?: string; image?: string;
name: string; name: string;
rate: Money; rate: Money;

View File

@ -10,7 +10,7 @@
<div class="flex col-span-2 gap-5"> <div class="flex col-span-2 gap-5">
<Button <Button
class="py-5 w-full bg-red-500 dark:bg-red-700" class="py-5 w-full bg-red-500 dark:bg-red-700"
@click="$emit('toggleModal', 'RouteToInvoiceList')" @click="$emit('toggleModal', 'Alert')"
> >
<slot> <slot>
<p class="uppercase text-lg text-white font-semibold"> <p class="uppercase text-lg text-white font-semibold">
@ -23,7 +23,7 @@
class="w-full py-5 bg-green-500 dark:bg-green-700" class="w-full py-5 bg-green-500 dark:bg-green-700"
@click=" @click="
routeTo('/list/SalesInvoice'); routeTo('/list/SalesInvoice');
$emit('toggleModal', 'RouteToInvoiceList'); $emit('toggleModal', 'Alert');
" "
> >
<slot> <slot>
@ -49,7 +49,7 @@ export default defineComponent({
Modal, Modal,
Button, Button,
}, },
emits: ['toggleModal', 'selectedInvoiceName'], emits: ['toggleModal'],
methods: { methods: {
routeTo, routeTo,
}, },

View File

@ -0,0 +1,443 @@
<template>
<div>
<OpenPOSShiftModal
v-if="!isPosShiftOpen"
:open-modal="!isPosShiftOpen"
@toggle-modal="toggleModal"
/>
<ClosePOSShiftModal
:open-modal="openShiftCloseModal"
@toggle-modal="toggleModal"
/>
<LoyaltyProgramModal
:open-modal="openLoyaltyProgramModal"
:loyalty-points="loyaltyPoints"
:loyalty-program="loyaltyProgram"
@set-loyalty-points="emitSetLoyaltyPoints"
@toggle-modal="toggleModal"
/>
<SavedInvoiceModal
:open-modal="openSavedInvoiceModal"
:modal-status="openSavedInvoiceModal"
@selected-invoice-name="emitSelectedInvoice"
@toggle-modal="toggleModal"
/>
<CouponCodeModal
:open-modal="openCouponCodeModal"
@toggle-modal="toggleModal"
@set-coupons-count="emitCouponsCount"
/>
<PaymentModal
:open-modal="openPaymentModal"
@toggle-modal="toggleModal"
@set-cash-amount="emitSetCashAmount"
@set-coupons-count="emitCouponsCount"
@set-transfer-ref-no="setTransferRefNo"
@set-transfer-amount="emitSetTransferAmount"
@create-transaction="emitCreateTransaction"
@set-transfer-clearance-date="setTransferClearanceDate"
/>
<AlertModal :open-modal="openAlertModal" @toggle-modal="toggleModal" />
<div
class="bg-gray-25 dark:bg-gray-875 grid grid-cols-12 gap-2 p-4"
style="height: calc(100vh - var(--h-row-largest))"
>
<div
class="
col-span-5
bg-white
border
rounded-md
dark:border-gray-800 dark:bg-gray-850
"
>
<div class="rounded-md p-4 col-span-5">
<div class="flex gap-x-2">
<!-- Item Search -->
<Link
:class="
fyo.singles.InventorySettings?.enableBarcodes
? 'flex-shrink-0 w-2/3'
: 'w-full'
"
:df="{
label: t`Search an Item`,
fieldtype: 'Link',
fieldname: 'item',
target: 'Item',
}"
:border="true"
:value="itemSearchTerm"
@keyup.enter="
async () => await selectItem(await getItem(itemSearchTerm))
"
@change="(item: string) =>itemSearchTerm= item"
/>
<Barcode
v-if="fyo.singles.InventorySettings?.enableBarcodes"
class="w-1/3"
@item-selected="
async (name: string) => {
await selectItem(await getItem(name));
}
"
/>
</div>
<ItemsTable
v-if="tableView"
:items="items"
:item-qty-map="itemQuantityMap as ItemQtyMap"
@add-item="selectItem"
/>
<ItemsGrid
v-else
:items="items"
:item-qty-map="itemQuantityMap as ItemQtyMap"
@add-item="selectItem"
/>
<div class="flex fixed bottom-0 p-1 mb-7 gap-x-3">
<POSQuickActions
:sinv-doc="sinvDoc"
:loyalty-points="loyaltyPoints"
:loyalty-program="loyaltyProgram"
:applied-coupons-count="appliedCouponsCount"
@toggle-view="toggleView"
@toggle-modal="toggleModal"
@emit-route-to-sinv-list="emitRouteToSinvList"
/>
</div>
</div>
</div>
<div class="col-span-7">
<div class="flex flex-col gap-3" style="height: calc(100vh - 6rem)">
<div
class="
p-4
bg-white
border
rounded-md
grow
h-full
dark:border-gray-800 dark:bg-gray-850
"
>
<!-- Customer Search -->
<MultiLabelLink
v-if="sinvDoc?.fieldMap"
class="flex-shrink-0"
secondary-link="phone"
:border="true"
:value="sinvDoc?.party"
:df="sinvDoc?.fieldMap.party"
@change="(value:string) => $emit('setCustomer',value)"
/>
<SelectedItemTable />
</div>
<div
class="
p-4
bg-white
border
rounded-md
dark:border-gray-800 dark:bg-gray-850
"
>
<div class="w-full grid grid-cols-2 gap-y-2 gap-x-3">
<div class="">
<div class="grid grid-cols-2 gap-2">
<FloatingLabelFloatInput
:df="{
label: t`Total Quantity`,
fieldtype: 'Int',
fieldname: 'totalQuantity',
minvalue: 0,
maxvalue: 1000,
}"
size="large"
:value="totalQuantity"
:read-only="true"
:text-right="true"
/>
<FloatingLabelCurrencyInput
:df="{
label: t`Add'l Discounts`,
fieldtype: 'Int',
fieldname: 'additionalDiscount',
minvalue: 0,
}"
size="large"
:value="additionalDiscounts"
:read-only="true"
:text-right="true"
@change="(amount:Money)=> additionalDiscounts= amount"
/>
</div>
<div class="mt-4 grid grid-cols-2 gap-2">
<FloatingLabelCurrencyInput
:df="{
label: t`Item Discounts`,
fieldtype: 'Currency',
fieldname: 'itemDiscounts',
}"
size="large"
:value="itemDiscounts"
:read-only="true"
:text-right="true"
/>
<FloatingLabelCurrencyInput
v-if="sinvDoc?.fieldMap"
:df="sinvDoc?.fieldMap.grandTotal"
size="large"
:value="sinvDoc?.grandTotal"
:read-only="true"
:text-right="true"
/>
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<Button
class="w-full bg-violet-500 dark:bg-violet-700 py-6"
:disabled="!sinvDoc?.party || !sinvDoc?.items?.length"
@click="$emit('saveInvoiceAction')"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Save` }}
</p>
</slot>
</Button>
<Button
class="w-full mt-4 bg-blue-500 dark:bg-blue-700 py-6"
@click="toggleModal('SavedInvoice', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`held` }}
</p>
</slot>
</Button>
</div>
<div class="w-full">
<Button
class="w-full bg-red-500 dark:bg-red-700 py-6"
:disabled="!sinvDoc?.items?.length"
@click="() => $emit('clearValues')"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Cancel` }}
</p>
</slot>
</Button>
<Button
class="mt-4 w-full bg-green-500 dark:bg-green-700 py-6"
:disabled="disablePayButton"
@click="toggleModal('Payment', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Pay` }}
</p>
</slot>
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Money } from 'pesa';
import { fyo } from 'src/initFyo';
import { getItem } from 'src/utils/pos';
import AlertModal from './AlertModal.vue';
import PaymentModal from './PaymentModal.vue';
import Button from 'src/components/Button.vue';
import { defineComponent, PropType } from 'vue';
import { Item } from 'models/baseModels/Item/Item';
import Link from 'src/components/Controls/Link.vue';
import CouponCodeModal from './CouponCodeModal.vue';
import POSQuickActions from './POSQuickActions.vue';
import { ModalName } from 'src/components/POS/types';
import SavedInvoiceModal from './SavedInvoiceModal.vue';
import OpenPOSShiftModal from './OpenPOSShiftModal.vue';
import ClosePOSShiftModal from './ClosePOSShiftModal.vue';
import Barcode from 'src/components/Controls/Barcode.vue';
import { Payment } from 'models/baseModels/Payment/Payment';
import LoyaltyProgramModal from './LoyaltyProgramModal.vue';
import ItemsGrid from 'src/components/POS/Classic/ItemsGrid.vue';
import ItemsTable from 'src/components/POS/Classic/ItemsTable.vue';
import MultiLabelLink from 'src/components/Controls/MultiLabelLink.vue';
import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
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 {
ItemQtyMap,
ItemSerialNumbers,
POSItem,
} from 'src/components/POS/types';
export default defineComponent({
name: 'ClassicPOS',
components: {
Link,
Button,
Barcode,
ItemsGrid,
AlertModal,
ItemsTable,
PaymentModal,
MultiLabelLink,
CouponCodeModal,
POSQuickActions,
OpenPOSShiftModal,
SavedInvoiceModal,
SelectedItemTable,
ClosePOSShiftModal,
LoyaltyProgramModal,
FloatingLabelFloatInput,
FloatingLabelCurrencyInput,
},
props: {
cashAmount: Money,
itemDiscounts: Money,
openAlertModal: Boolean,
disablePayButton: Boolean,
openPaymentModal: Boolean,
openCouponCodeModal: Boolean,
openShiftCloseModal: Boolean,
openSavedInvoiceModal: Boolean,
openLoyaltyProgramModal: Boolean,
openAppliedCouponsModal: Boolean,
loyaltyPoints: {
type: Number,
default: 0,
},
loyaltyProgram: {
type: String,
default: '',
},
appliedCouponsCount: {
type: Number,
default: 0,
},
sinvDoc: {
type: Object as PropType<SalesInvoice | undefined>,
default: undefined,
},
itemQuantityMap: {
type: Object as PropType<ItemQtyMap>,
default: () => ({}),
},
coupons: {
type: Object as PropType<AppliedCouponCodes>,
default: () => ({}),
},
items: {
type: Array as PropType<POSItem[] | undefined>,
default: () => [],
},
},
emits: [
'addItem',
'toggleModal',
'setCustomer',
'clearValues',
'setCashAmount',
'setCouponsCount',
'routeToSinvList',
'setLoyaltyPoints',
'saveInvoiceAction',
'createTransaction',
'setTransferAmount',
'selectedInvoiceName',
],
data() {
return {
tableView: true,
totalQuantity: 0,
totalTaxedAmount: fyo.pesa(0),
additionalDiscounts: fyo.pesa(0),
paymentDoc: {} as Payment,
itemSerialNumbers: {} as ItemSerialNumbers,
itemSearchTerm: '',
transferRefNo: undefined as string | undefined,
transferClearanceDate: undefined as Date | undefined,
};
},
computed: {
isPosShiftOpen: () => !!fyo.singles.POSShift?.isShiftOpen,
},
methods: {
setTransferRefNo(ref: string) {
this.transferRefNo = ref;
},
emitRouteToSinvList() {
this.$emit('routeToSinvList');
},
toggleView() {
this.tableView = !this.tableView;
},
emitSetCashAmount(amount: Money) {
this.$emit('setCashAmount', amount);
},
setTransferClearanceDate(date: Date) {
this.transferClearanceDate = date;
},
emitCouponsCount(value: number) {
this.$emit('setCouponsCount', value);
},
emitSetLoyaltyPoints(value: string) {
this.$emit('setLoyaltyPoints', value);
},
emitSelectedInvoice(doc: InvoiceItem) {
this.$emit('selectedInvoiceName', doc);
},
toggleModal(modal: ModalName, value: boolean) {
this.$emit('toggleModal', modal, value);
},
emitCreateTransaction(shouldPrint = false) {
this.$emit('createTransaction', shouldPrint);
},
emitSetTransferAmount(amount: Money = fyo.pesa(0)) {
this.$emit('setTransferAmount', amount);
},
selectItem(item: POSItem | Item | undefined) {
this.$emit('addItem', item);
},
openCouponModal() {
if (this.sinvDoc?.party && this.sinvDoc?.items?.length) {
this.toggleModal('CouponCode', true);
}
},
getItem,
},
});
</script>

View File

@ -0,0 +1,494 @@
<template>
<Modal class="h-auto" :set-close-listener="false">
<div class="px-5" style="width: 30vw">
<p class="text-center font-semibold py-3">Keyboard</p>
<hr class="dark:border-gray-800" />
<div class="mx-6 my-3">
<component
:is="selectedItemRow?.fieldMap[selectedItemField!].fieldtype"
ref="dynamicInput"
:df="{
fieldname: selectedItemRow?.fieldMap[selectedItemField!].fieldname as string,
fieldtype: selectedItemRow?.fieldMap[selectedItemField!].fieldtype,
label: selectedItemRow?.fieldMap[selectedItemField!].label as string,
}"
class="mb-3"
:border="true"
:show-label="true"
:value="selectedValue"
:focus-input="true"
@change="(value: number) => handleInput(value.toString())"
/>
<div
id="keypad"
class="text-4xl grid grid-cols-4 gap-3 rounded font-bold py-4"
>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('7')"
>
7
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('8')"
>
8
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('9')"
>
9
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="deleteLast()"
>
Del
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('4')"
>
4
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('5')"
>
5
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('6')"
>
6
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('-')"
>
-
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('1')"
>
1
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('2')"
>
2
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('3')"
>
3
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('+')"
>
+
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('.')"
>
</button>
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="appendValue('0')"
>
0
</button>
<div class="grid col-span-2">
<button
class="
py-2.5
bg-gray-100
text-2xl
border-transparent
rounded-lg
transition-colors
duration-200
hover:bg-gray-200
dark:bg-gray-875 dark:hover:bg-gray-900
"
@click="reset()"
>
Clear
</button>
</div>
</div>
</div>
<div class="px-5">
<div class="grid row-start-6 grid-cols-2 gap-4 mt-auto mb-3">
<div class="col-span-2">
<Button
class="w-full bg-green-500 dark:bg-green-700"
style="padding: 1.35rem"
@click="saveSelectedItem()"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Save` }}
</p>
</slot>
</Button>
</div>
</div>
<div class="grid row-start-6 grid-cols-2 gap-4 mt-auto mb-8">
<div class="col-span-2">
<Button
class="w-full bg-red-500 dark:bg-red-700"
style="padding: 1.35rem"
@click="closeKeyboardModal()"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Cancel` }}
</p>
</slot>
</Button>
</div>
</div>
</div>
</div>
</Modal>
</template>
<script lang="ts">
import Modal from 'src/components/Modal.vue';
import { ModelNameEnum } from 'models/types';
import { defineComponent, inject } from 'vue';
import Button from 'src/components/Button.vue';
import Float from 'src/components/Controls/Float.vue';
import Currency from 'src/components/Controls/Currency.vue';
import { updatePricingRuleItem } from 'models/helpers';
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';
export default defineComponent({
name: 'KeyboardModal',
components: {
Modal,
Float,
Button,
Currency,
},
props: {
modalStatus: Boolean,
selectedItemRow: SalesInvoiceItem,
selectedItemField: {
type: String,
default: '',
},
},
emits: ['toggleModal'],
setup() {
return {
sinvDoc: inject('sinvDoc') as SalesInvoice,
};
},
data() {
return {
selectedValue: '',
};
},
watch: {
async modalStatus(newVal) {
if (newVal) {
await this.$nextTick();
await this.focusInput();
}
this.updateSelectedValue();
},
},
async mounted() {
this.updateSelectedValue();
await this.focusInput();
},
methods: {
async appendValue(value: string) {
if (value === '-') {
this.selectedValue = this.selectedValue.startsWith('-')
? this.selectedValue
: `-${this.selectedValue}`;
} else if (value === '+') {
this.selectedValue = this.selectedValue.startsWith('-')
? this.selectedValue.slice(1)
: this.selectedValue;
} else {
this.selectedValue =
this.selectedValue === '0' ? value : this.selectedValue + value;
}
await this.focusInput();
},
updateSelectedValue() {
this.selectedValue = '';
if (
this.selectedItemRow?.fieldMap[this.selectedItemField].fieldtype !==
ModelNameEnum.Currency
) {
this.selectedValue = this.selectedItemRow![
this.selectedItemField
] as string;
}
},
handleInput(value: string) {
this.selectedValue = value;
},
async saveSelectedItem() {
try {
if (
this.selectedItemRow?.fieldMap[this.selectedItemField].fieldtype ===
ModelNameEnum.Currency
) {
this.selectedItemRow[this.selectedItemField] = this.fyo.pesa(
Number(this.selectedValue)
);
if (this.selectedItemField === 'rate') {
this.selectedItemRow.setRate = this.fyo.pesa(
Number(this.selectedValue)
);
await this.sinvDoc.runFormulas();
this.$emit('toggleModal', 'Keyboard');
return;
}
if (this.selectedItemField === 'itemDiscountAmount') {
if (this.sinvDoc.grandTotal?.lte(this.selectedValue)) {
this.selectedItemRow.itemDiscountAmount = this.fyo.pesa(
Number(0)
);
throw new ValidationError(
this.fyo.t`Discount Amount (${this.fyo.format(
this.selectedValue,
'Currency'
)}) cannot be greated than Amount (${this.fyo.format(
this.sinvDoc.grandTotal,
'Currency'
)}).`
);
}
this.selectedItemRow.setItemDiscountAmount = true;
this.selectedItemRow.itemDiscountAmount = this.fyo.pesa(
Number(this.selectedValue)
);
}
} else {
this.selectedItemRow![this.selectedItemField] = Number(
this.selectedValue
);
if (this.selectedItemField === 'itemDiscountPercent') {
if (Number(this.selectedValue) > 100) {
await this.selectedItemRow?.set('itemDiscountPercent', 0);
throw new ValidationError(
this.fyo
.t`Discount Percent (${this.selectedValue}) cannot be greater than 100.`
);
}
await this.selectedItemRow?.set('setItemDiscountAmount', false);
await this.selectedItemRow?.set(
'itemDiscountPercent',
this.selectedValue
);
}
if (this.selectedItemField === 'quantity') {
await updatePricingRuleItem(this.sinvDoc);
}
}
await this.sinvDoc.runFormulas();
this.$emit('toggleModal', 'Keyboard');
} catch (error) {
showToast({
type: 'error',
message: this.t`${error as string}`,
});
}
},
async deleteLast() {
this.selectedValue = this.selectedValue?.slice(0, -1);
await this.focusInput();
},
async reset() {
this.selectedValue = '';
await this.focusInput();
},
async focusInput() {
await this.$nextTick();
(this.$refs.dynamicInput as HTMLInputElement)?.focus();
},
async closeKeyboardModal() {
await this.reset();
this.$emit('toggleModal', 'Keyboard');
},
},
});
</script>

View File

@ -107,6 +107,15 @@ export default defineComponent({
methods: { methods: {
async updateLoyaltyPoints(newValue: number) { async updateLoyaltyPoints(newValue: number) {
try { try {
const partyData = await this.fyo.db.get(
ModelNameEnum.Party,
this.sinvDoc.party as string
);
if (!partyData.loyaltyProgram) {
return;
}
if (this.loyaltyPoints >= newValue) { if (this.loyaltyPoints >= newValue) {
this.sinvDoc.loyaltyPoints = newValue; this.sinvDoc.loyaltyPoints = newValue;
} else { } else {
@ -121,7 +130,7 @@ export default defineComponent({
ModelNameEnum.LoyaltyProgram, ModelNameEnum.LoyaltyProgram,
{ {
fields: ['conversionFactor'], fields: ['conversionFactor'],
filters: { name: this.loyaltyProgram }, filters: { name: partyData.loyaltyProgram as string },
} }
); );

462
src/pages/POS/ModernPOS.vue Normal file
View File

@ -0,0 +1,462 @@
<template>
<div>
<OpenPOSShiftModal
v-if="!isPosShiftOpen"
:open-modal="!isPosShiftOpen"
@toggle-modal="toggleModal"
/>
<ClosePOSShiftModal
:open-modal="openShiftCloseModal"
@toggle-modal="toggleModal"
/>
<LoyaltyProgramModal
:open-modal="openLoyaltyProgramModal"
:loyalty-points="loyaltyPoints"
:loyalty-program="loyaltyProgram"
@set-loyalty-points="emitSetLoyaltyPoints"
@toggle-modal="toggleModal"
/>
<SavedInvoiceModal
:open-modal="openSavedInvoiceModal"
:modal-status="openSavedInvoiceModal"
@toggle-modal="toggleModal"
@selected-invoice-name="emitSelectedInvoice"
/>
<CouponCodeModal
:open-modal="openCouponCodeModal"
@toggle-modal="toggleModal"
@set-coupons-count="emitCouponsCount"
/>
<PaymentModal
:open-modal="openPaymentModal"
@toggle-modal="toggleModal"
@set-cash-amount="emitSetCashAmount"
@set-coupons-count="emitCouponsCount"
@set-transfer-ref-no="setTransferRefNo"
@create-transaction="emitCreateTransaction"
@set-transfer-amount="emitSetTransferAmount"
@set-transfer-clearance-date="setTransferClearanceDate"
/>
<AlertModal :open-modal="openAlertModal" @toggle-modal="toggleModal" />
<KeyboardModal
v-if="selectedItemField && selectedItemRow"
:open-modal="openKeyboardModal"
:modal-status="openKeyboardModal"
:selected-item-field="selectedItemField"
:selected-item-row="(selectedItemRow as SalesInvoiceItem)"
@toggle-modal="toggleModal"
/>
<div class="bg-gray-25 dark:bg-gray-875 grid grid-cols-9 gap-3 p-4">
<div class="col-span-3 flex h-auto w-full">
<div class="grid grid-rows-5 w-full gap-3">
<div
class="
p-4
grow
h-full
row-span-5
bg-white
border
rounded-md
dark:bg-gray-850 dark:border-gray-800
"
>
<!-- Customer Search -->
<MultiLabelLink
v-if="sinvDoc?.fieldMap"
class="flex-shrink-0"
secondary-link="phone"
:border="true"
:value="sinvDoc?.party"
:df="sinvDoc?.fieldMap.party"
@change="(value:string) => $emit('setCustomer',value)"
/>
<ModernPOSSelectedItemTable
@selected-row="selectedRow"
@toggle-modal="toggleModal"
/>
</div>
<div
class="
p-4
bg-white
border
rounded-md
dark:bg-gray-850 dark:border-gray-800
"
>
<div class="grid grid-cols-2 gap-2">
<FloatingLabelFloatInput
:df="{
label: t`Total Quantity`,
fieldtype: 'Int',
fieldname: 'totalQuantity',
minvalue: 0,
maxvalue: 1000,
}"
size="large"
:value="totalQuantity"
:read-only="true"
:text-right="true"
/>
<FloatingLabelCurrencyInput
:df="{
label: t`Add'l Discounts`,
fieldtype: 'Int',
fieldname: 'additionalDiscount',
minvalue: 0,
}"
size="large"
:value="additionalDiscounts"
:read-only="true"
:text-right="true"
@change="(amount:Money)=> additionalDiscounts = amount"
/>
</div>
<div class="mt-2 grid grid-cols-2 gap-2">
<FloatingLabelCurrencyInput
:df="{
label: t`Item Discounts`,
fieldtype: 'Currency',
fieldname: 'itemDiscounts',
}"
size="large"
:value="itemDiscounts"
:read-only="true"
:text-right="true"
/>
<FloatingLabelCurrencyInput
v-if="sinvDoc?.fieldMap"
:df="sinvDoc?.fieldMap.grandTotal"
size="large"
:value="sinvDoc?.grandTotal"
:read-only="true"
:text-right="true"
/>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<Button
class="mt-2 w-full bg-violet-500 dark:bg-violet-700 py-5"
:disabled="!sinvDoc?.party || !sinvDoc?.items?.length"
@click="$emit('saveInvoiceAction')"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Save` }}
</p>
</slot>
</Button>
<Button
class="w-full mt-2 bg-blue-500 dark:bg-blue-700 py-5"
@click="toggleModal('SavedInvoice', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`held` }}
</p>
</slot>
</Button>
</div>
<div class="w-full">
<Button
class="mt-2 w-full bg-red-500 dark:bg-red-700 py-5"
:disabled="!sinvDoc?.items?.length"
@click="() => $emit('clearValues')"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Cancel` }}
</p>
</slot>
</Button>
<Button
class="mt-2 w-full bg-green-500 dark:bg-green-700 py-5"
:disabled="disablePayButton"
@click="toggleModal('Payment', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Pay` }}
</p>
</slot>
</Button>
</div>
</div>
</div>
</div>
</div>
<div
class="
bg-white
border
rounded-md
col-span-6
flex flex-col
dark:bg-gray-850 dark:border-gray-800
"
style="height: calc(100vh - 6rem)"
>
<div class="rounded-md p-4 col-span-5 h-full">
<div class="flex gap-x-2">
<!-- Item Search -->
<Link
:class="
fyo.singles.InventorySettings?.enableBarcodes
? 'flex-shrink-0 w-2/3'
: 'w-full'
"
:df="{
label: t`Search an Item`,
fieldtype: 'Link',
fieldname: 'item',
target: 'Item',
}"
:border="true"
:value="itemSearchTerm"
@keyup.enter="
async () => await selectItem(await getItem(itemSearchTerm))
"
@change="(item: string) =>itemSearchTerm= item"
/>
<Barcode
v-if="fyo.singles.InventorySettings?.enableBarcodes"
class="w-1/3"
@item-selected="
async (name: string) => {
await selectItem(await getItem(name));
}
"
/>
</div>
<ModernPOSItemsTable
v-if="tableView"
:items="items"
:item-qty-map="itemQuantityMap as ItemQtyMap"
@add-item="selectItem"
/>
<ModernPOSItemsGrid
v-else
:items="items"
:item-qty-map="itemQuantityMap as ItemQtyMap"
@add-item="selectItem"
/>
</div>
<div class="flex fixed bottom-0 p-1 ml-3 mb-7 gap-x-3">
<POSQuickActions
:sinv-doc="sinvDoc"
:loyalty-points="loyaltyPoints"
:loyalty-program="loyaltyProgram"
:applied-coupons-count="appliedCouponsCount"
@toggle-view="toggleView"
@toggle-modal="toggleModal"
@emit-route-to-sinv-list="emitRouteToSinvList"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Money } from 'pesa';
import { PropType } from 'vue';
import { fyo } from 'src/initFyo';
import { defineComponent } from 'vue';
import { getItem } from 'src/utils/pos';
import AlertModal from './AlertModal.vue';
import PaymentModal from './PaymentModal.vue';
import Button from 'src/components/Button.vue';
import KeyboardModal from './KeyboardModal.vue';
import { Item } from 'models/baseModels/Item/Item';
import Link from 'src/components/Controls/Link.vue';
import CouponCodeModal from './CouponCodeModal.vue';
import POSQuickActions from './POSQuickActions.vue';
import OpenPOSShiftModal from './OpenPOSShiftModal.vue';
import SavedInvoiceModal from './SavedInvoiceModal.vue';
import Barcode from 'src/components/Controls/Barcode.vue';
import ClosePOSShiftModal from './ClosePOSShiftModal.vue';
import { Payment } from 'models/baseModels/Payment/Payment';
import LoyaltyProgramModal from './LoyaltyProgramModal.vue';
import { InvoiceItem } from 'models/baseModels/InvoiceItem/InvoiceItem';
import MultiLabelLink from 'src/components/Controls/MultiLabelLink.vue';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import ModernPOSItemsGrid from 'src/components/POS/Modern/ModernPOSItemsGrid.vue';
import ModernPOSItemsTable from 'src/components/POS/Modern/ModernPOSItemsTable.vue';
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
import ModernPOSSelectedItemTable from 'src/components/POS/Modern/ModernPOSSelectedItemTable.vue';
import {
ItemQtyMap,
ItemSerialNumbers,
ModalName,
POSItem,
} from 'src/components/POS/types';
export default defineComponent({
name: 'ModernPos',
components: {
Link,
Button,
Barcode,
AlertModal,
PaymentModal,
KeyboardModal,
MultiLabelLink,
POSQuickActions,
CouponCodeModal,
OpenPOSShiftModal,
SavedInvoiceModal,
ModernPOSItemsGrid,
ClosePOSShiftModal,
LoyaltyProgramModal,
ModernPOSItemsTable,
FloatingLabelFloatInput,
FloatingLabelCurrencyInput,
ModernPOSSelectedItemTable,
},
props: {
cashAmount: Money,
itemDiscounts: Money,
openAlertModal: Boolean,
disablePayButton: Boolean,
openPaymentModal: Boolean,
openKeyboardModal: Boolean,
openCouponCodeModal: Boolean,
openShiftCloseModal: Boolean,
openSavedInvoiceModal: Boolean,
openLoyaltyProgramModal: Boolean,
openAppliedCouponsModal: Boolean,
loyaltyPoints: {
type: Number,
default: 0,
},
loyaltyProgram: {
type: String,
default: '',
},
appliedCouponsCount: {
type: Number,
default: 0,
},
coupons: {
type: Object as PropType<AppliedCouponCodes>,
default: () => ({}),
},
sinvDoc: {
type: Object as PropType<SalesInvoice | undefined>,
default: undefined,
},
itemQuantityMap: {
type: Object as PropType<ItemQtyMap>,
default: () => ({}),
},
items: {
type: Array as PropType<POSItem[] | undefined>,
default: () => [],
},
},
emits: [
'addItem',
'toggleModal',
'setCustomer',
'clearValues',
'setCashAmount',
'setCouponsCount',
'routeToSinvList',
'setLoyaltyPoints',
'saveInvoiceAction',
'createTransaction',
'setTransferAmount',
'selectedInvoiceName',
],
data() {
return {
tableView: true,
totalQuantity: 0,
totalTaxedAmount: fyo.pesa(0),
additionalDiscounts: fyo.pesa(0),
paymentDoc: {} as Payment,
itemSerialNumbers: {} as ItemSerialNumbers,
selectedItemField: '',
selectedItemRow: {} as SalesInvoiceItem,
itemSearchTerm: '',
transferRefNo: undefined as string | undefined,
transferClearanceDate: undefined as Date | undefined,
};
},
computed: {
isPosShiftOpen: () => !!fyo.singles.POSShift?.isShiftOpen,
},
methods: {
setTransferRefNo(ref: string) {
this.transferRefNo = ref;
},
toggleView() {
this.tableView = !this.tableView;
},
emitSetCashAmount(amount: Money) {
this.$emit('setCashAmount', amount);
},
setTransferClearanceDate(date: Date) {
this.transferClearanceDate = date;
},
emitCouponsCount(value: number) {
this.$emit('setCouponsCount', value);
},
emitRouteToSinvList() {
this.$emit('routeToSinvList');
},
emitSetLoyaltyPoints(value: string) {
this.$emit('setLoyaltyPoints', value);
},
emitSelectedInvoice(doc: InvoiceItem) {
this.$emit('selectedInvoiceName', doc);
},
toggleModal(modal: ModalName, value: boolean) {
this.$emit('toggleModal', modal, value);
},
emitCreateTransaction(shouldPrint = false) {
this.$emit('createTransaction', shouldPrint);
},
selectedRow(row: SalesInvoiceItem, field: string) {
this.selectedItemRow = row;
this.selectedItemField = field;
},
emitSetTransferAmount(amount: Money = fyo.pesa(0)) {
this.$emit('setTransferAmount', amount);
},
selectItem(item: POSItem | Item | undefined) {
this.$emit('addItem', item);
},
openCouponModal() {
if (this.sinvDoc?.party && this.sinvDoc?.items?.length) {
this.toggleModal('CouponCode', true);
}
},
getItem,
},
});
</script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class=""> <div class="flex-col">
<PageHeader :title="t`Point of Sale`"> <PageHeader :title="t`Point of Sale`">
<slot> <slot>
<Button <Button
@ -10,646 +10,179 @@
</Button> </Button>
</slot> </slot>
</PageHeader> </PageHeader>
<ClassicPOS
<OpenPOSShiftModal v-if="fyo.singles.POSSettings?.posUI == 'Classic'"
v-if="!isPosShiftOpen" :item-quantity-qap="itemQtyMap"
:open-modal="!isPosShiftOpen"
@toggle-modal="toggleModal"
/>
<ClosePOSShiftModal
:open-modal="openShiftCloseModal"
@toggle-modal="toggleModal"
/>
<LoyaltyProgramModal
:open-modal="openLoyaltyProgramModal"
:loyalty-points="loyaltyPoints" :loyalty-points="loyaltyPoints"
:loyalty-program="loyaltyProgram" :open-alert-modal="openAlertModal"
@set-loyalty-points="setLoyaltyPoints" :default-customer="defaultCustomer"
@toggle-modal="toggleModal" :items="(items as [] as POSItem[])"
/> :cash-amount="(cashAmount as Money)"
<SavedInvoiceModal :sinv-doc="(sinvDoc as SalesInvoice)"
:open-modal="openSavedInvoiceModal" :disable-pay-button="disablePayButton"
:modal-status="openSavedInvoiceModal" :open-payment-modal="openPaymentModal"
@selected-invoice-name="selectedInvoiceName" :item-discounts="(itemDiscounts as Money)"
@toggle-modal="toggleModal" :coupons="(coupons as AppliedCouponCodes)"
/> :applied-coupons-count="appliedCouponsCount"
:open-shift-close-modal="openShiftCloseModal"
<CouponCodeModal :open-coupon-code-modal="openCouponCodeModal"
:open-modal="openCouponCodeModal" :open-saved-invoice-modal="openSavedInvoiceModal"
@set-coupons-count="setCouponsCount" :open-loyalty-program-modal="openLoyaltyProgramModal"
@toggle-modal="toggleModal" :open-applied-coupons-modal="openAppliedCouponsModal"
/> @add-item="addItem"
@set-sinv-doc="setSinvDoc"
<PaymentModal @clear-values="clearValues"
:open-modal="openPaymentModal" @set-customer="setCustomer"
@create-transaction="createTransaction"
@toggle-modal="toggleModal" @toggle-modal="toggleModal"
@set-cash-amount="setCashAmount" @set-cash-amount="setCashAmount"
@set-transfer-amount="setTransferAmount"
@set-transfer-ref-no="setTransferRefNo"
@set-coupons-count="setCouponsCount" @set-coupons-count="setCouponsCount"
@set-transfer-clearance-date="setTransferClearanceDate" @route-to-sinv-list="routeToSinvList"
@set-loyalty-points="setLoyaltyPoints"
@create-transaction="createTransaction"
@save-invoice-action="saveInvoiceAction"
@set-transfer-amount="setTransferAmount"
@selected-invoice-name="selectedInvoiceName"
/> />
<ModernPOS
<AlertModal v-else
:open-modal="openRouteToInvoiceListModal" :item-quantity-qap="itemQtyMap"
:loyalty-points="loyaltyPoints"
:open-alert-modal="openAlertModal"
:default-customer="defaultCustomer"
:items="(items as [] as POSItem[])"
:cash-amount="(cashAmount as Money)"
:sinv-doc="(sinvDoc as SalesInvoice)"
:disable-pay-button="disablePayButton"
:open-payment-modal="openPaymentModal"
:open-keyboard-modal="openKeyboardModal"
:item-discounts="(itemDiscounts as Money)"
:coupons="(coupons as AppliedCouponCodes)"
:applied-coupons-count="appliedCouponsCount"
:open-shift-close-modal="openShiftCloseModal"
:open-coupon-code-modal="openCouponCodeModal"
:open-saved-invoice-modal="openSavedInvoiceModal"
:open-loyalty-program-modal="openLoyaltyProgramModal"
:open-applied-coupons-modal="openAppliedCouponsModal"
@add-item="addItem"
@set-sinv-doc="setSinvDoc"
@clear-values="clearValues"
@set-customer="setCustomer"
@toggle-modal="toggleModal" @toggle-modal="toggleModal"
@set-cash-amount="setCashAmount"
@set-coupons-count="setCouponsCount"
@route-to-sinv-list="routeToSinvList"
@set-loyalty-points="setLoyaltyPoints"
@create-transaction="createTransaction"
@save-invoice-action="saveInvoiceAction"
@set-transfer-amount="setTransferAmount"
@selected-invoice-name="selectedInvoiceName"
/> />
<div
class="bg-gray-25 dark:bg-gray-875 gap-2 grid grid-cols-12 p-4"
style="height: calc(100vh - var(--h-row-largest))"
>
<div
class="
bg-white
dark:bg-gray-850
border
dark:border-gray-800
col-span-5
rounded-md
"
>
<div class="rounded-md p-4 col-span-5">
<div class="flex gap-x-2">
<!-- Item Search -->
<Link
:class="
fyo.singles.InventorySettings?.enableBarcodes
? 'flex-shrink-0 w-2/3'
: 'w-full'
"
:df="{
label: t`Search an Item`,
fieldtype: 'Link',
fieldname: 'item',
target: 'Item',
}"
:border="true"
:value="itemSearchTerm"
@keyup.enter="
async () => await addItem(await getItem(itemSearchTerm))
"
@change="(item: string) =>itemSearchTerm= item"
/>
<Barcode
v-if="fyo.singles.InventorySettings?.enableBarcodes"
class="w-1/3"
@item-selected="
async (name: string) => {
await addItem(await getItem(name));
}
"
/>
</div>
<ItemsTable
v-if="tableView"
:items="items"
:item-qty-map="itemQtyMap"
@add-item="addItem"
/>
<ItemsGrid
v-else
:items="items"
:item-qty-map="itemQtyMap"
@add-item="addItem"
/>
<div class="flex fixed bottom-0 p-1 mb-7 gap-x-3">
<div class="relative group">
<div class="bg-gray-100 p-1.5 rounded-md" @click="toggleView">
<FeatherIcon
:name="tableView ? 'grid' : 'list'"
class="w-5 h-5 text-black"
/>
</div>
<span
class="
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
mb-2
bg-gray-100
dark:bg-gray-800 dark:text-white
text-black text-xs
rounded-md
p-2
w-20
text-center
opacity-0
group-hover:opacity-100
transition-opacity
duration-300
"
>
{{ tableView ? 'Grid View' : 'List View' }}
</span>
</div>
<div class="relative group">
<div
class="px-1.5 py-1 rounded-md bg-gray-100"
@click="routeToSinvList"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="21"
fill="#000"
>
<path
d="M240-100q-41.92 0-70.96-29.04Q140-158.08 140-199.82V-300h120v-552.31l55.39 47.7 56.15-47.7 56.15 47.7 56.16-47.7 56.15 47.7 56.15-47.7 56.16 47.7 56.15-47.7 56.15 47.7 55.39-47.7V-200q0 41.92-29.04 70.96Q761.92-100 720-100H240Zm480-60q17 0 28.5-11.5T760-200v-560H320v460h360v100q0 17 11.5 28.5T720-160ZM367.69-610v-60h226.92v60H367.69Zm0 120v-60h226.92v60H367.69Zm310-114.62q-14.69 0-25.04-10.34-10.34-10.35-10.34-25.04t10.34-25.04q10.35-10.34 25.04-10.34t25.04 10.34q10.35 10.35 10.35 25.04t-10.35 25.04q-10.35 10.34-25.04 10.34Zm0 120q-14.69 0-25.04-10.34-10.34-10.35-10.34-25.04t10.34-25.04q10.35-10.34 25.04-10.34t25.04 10.34q10.35 10.35 10.35 25.04t-10.35 25.04q-10.35 10.34-25.04 10.34ZM240-160h380v-80H200v40q0 17 11.5 28.5T240-160Zm-40 0v-80 80Z"
/>
</svg>
</div>
<span
class="
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
rounded-md
opacity-0
bg-gray-100
dark:bg-gray-800 dark:text-white
text-black text-xs text-center
mb-2
p-2
w-28
group-hover:opacity-100
transition-opacity
duration-300
"
>
Sales Invoice List
</span>
</div>
<div class="relative group">
<div
class="p-1 rounded-md bg-gray-100"
:class="{
hidden: !fyo.singles.AccountingSettings?.enableLoyaltyProgram,
'bg-gray-100': loyaltyPoints,
'dark:bg-gray-600 cursor-not-allowed':
!loyaltyPoints || !sinvDoc.party || !sinvDoc.items?.length,
}"
@click="
loyaltyPoints && sinvDoc.party && sinvDoc.items?.length
? toggleModal('LoyaltyProgram', true)
: null
"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="23px"
viewBox="0 -960 960 960"
width="25px"
fill="#000"
>
<path
d="M100-180v-600h760v600H100Zm50.26-50.26h659.48v-499.48H150.26v499.48Zm0 0v-499.48 499.48Zm181.64-56.77h50.25v-42.56h48.67q14.37 0 23.6-10.38 9.22-10.38 9.22-24.25v-106.93q0-14.71-9.22-24.88-9.23-10.17-23.6-10.17H298.77v-73.95h164.87v-50.26h-81.49v-42.56H331.9v42.56h-48.41q-14.63 0-24.8 10.38-10.18 10.38-10.18 25v106.27q0 14.62 10.18 23.71 10.17 9.1 24.8 9.1h129.9v76.1H248.51v50.26h83.39v42.56Zm312.97-27.94L705.9-376H583.85l61.02 61.03ZM583.85-574H705.9l-61.03-61.03L583.85-574Z"
/>
</svg>
</div>
<span
class="
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
mb-2
bg-gray-100
dark:bg-gray-800 dark:text-white
text-black text-xs
rounded-md
p-2
w-28
text-center
opacity-0
group-hover:opacity-100
transition-opacity
duration-300
"
>
Loyalty Program
</span>
</div>
<div class="relative group">
<div
class="p-0.5 rounded-md bg-gray-100"
:class="{
hidden: !fyo.singles.AccountingSettings?.enableCouponCode,
'bg-gray-100': loyaltyPoints,
'dark:bg-gray-600 cursor-not-allowed':
!sinvDoc.party || !sinvDoc.items?.length,
}"
@click="openCouponModal()"
>
<svg
fill="#000000"
width="28px"
height="28px"
viewBox="0 0 512.00 512.00"
enable-background="new 0 0 512 512"
version="1.1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
stroke="#000000"
stroke-width="3.312000000000001"
transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"
>
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"
stroke="#CCCCCC"
stroke-width="19.456"
></g>
<g id="SVGRepo_iconCarrier">
<g id="Layer_1"></g>
<g id="Layer_2">
<g>
<path
d="M412.7,134.4H229.6c-2,0-3.9,0.8-5.3,2.2l-27.8,27.8L169.1,137c-1.4-1.4-3.3-2.2-5.3-2.2H99.3c-4.1,0-7.5,3.4-7.5,7.5 v227.4c0,4.1,3.4,7.5,7.5,7.5h64.5c2,0,3.9-0.8,5.3-2.2l27.4-27.4l27.8,27.8c1.4,1.4,3.3,2.2,5.3,2.2h183.1c4.1,0,7.5-3.4,7.5-7.5 V141.9C420.2,137.7,416.8,134.4,412.7,134.4z M405.2,362.6H232.7l-30.9-30.9c-2.9-2.9-7.7-2.9-10.6,0l-30.5,30.5h-53.9V149.8h53.9 l30.5,30.5c2.9,2.9,7.7,2.9,10.6,0l30.9-30.9h172.5V362.6z"
></path>
<path
d="M276.9,235.2c15.4,0,28-12.6,28-28s-12.6-28-28-28s-28,12.6-28,28S261.4,235.2,276.9,235.2z M276.9,194.2 c7.2,0,13,5.8,13,13s-5.8,13-13,13s-13-5.8-13-13S269.7,194.2,276.9,194.2z"
></path>
<path
d="M360,262.4c-15.4,0-28,12.6-28,28s12.6,28,28,28s28-12.6,28-28S375.4,262.4,360,262.4z M360,303.4c-7.2,0-13-5.8-13-13 s5.8-13,13-13s13,5.8,13,13S367.2,303.4,360,303.4z"
></path>
<path
d="M256.6,310.7c1.5,1.5,3.4,2.2,5.3,2.2s3.8-0.7,5.3-2.2l113.1-113.1c2.9-2.9,2.9-7.7,0-10.6c-2.9-2.9-7.7-2.9-10.6,0 L256.6,300.1C253.6,303,253.6,307.7,256.6,310.7z"
></path>
<path
d="M196.5,202.5c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,203.3,198.4,202.5,196.5,202.5z"
></path>
<path
d="M196.5,233.2c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,234,198.4,233.2,196.5,233.2z"
></path>
<path
d="M196.5,263.8c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,264.6,198.4,263.8,196.5,263.8z"
></path>
<path
d="M196.5,294.5c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,295.3,198.4,294.5,196.5,294.5z"
></path>
</g>
</g>
</g>
</svg>
</div>
<span
class="
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
mb-2
bg-gray-100
dark:bg-gray-800 dark:text-white
text-black text-xs
rounded-md
p-2
w-28
text-center
opacity-0
group-hover:opacity-100
transition-opacity
duration-300
"
>
Coupon Code
</span>
<div
v-if="appliedCouponsCount !== 0"
class="
absolute
top-0
right-0
transform
translate-x-1/2
-translate-y-1/2
h-4
w-4
bg-green-400
text-green-900
rounded-full
flex
items-center
justify-center
text-xs
cursor-pointer
border-red-500
p-2
"
>
{{ appliedCouponsCount }}
</div>
</div>
</div>
</div>
</div>
<div class="col-span-7">
<div class="flex flex-col gap-3" style="height: calc(100vh - 6rem)">
<div
class="
bg-white
dark:bg-gray-850
border
dark:border-gray-800
grow
h-full
p-4
rounded-md
"
>
<!-- Customer Search -->
<MultiLabelLink
v-if="sinvDoc.fieldMap"
class="flex-shrink-0"
secondary-link="phone"
:border="true"
:value="sinvDoc.party"
:df="sinvDoc.fieldMap.party"
@change="(value:string) => setCustomer(value)"
/>
<SelectedItemTable />
</div>
<div
class="
bg-white
dark:bg-gray-850
border
dark:border-gray-800
p-4
rounded-md
"
>
<div class="w-full grid grid-cols-2 gap-y-2 gap-x-3">
<div class="">
<div class="grid grid-cols-2 gap-2">
<FloatingLabelFloatInput
:df="{
label: t`Total Quantity`,
fieldtype: 'Int',
fieldname: 'totalQuantity',
minvalue: 0,
maxvalue: 1000,
}"
size="large"
:value="totalQuantity"
:read-only="true"
:text-right="true"
/>
<FloatingLabelCurrencyInput
:df="{
label: t`Add'l Discounts`,
fieldtype: 'Int',
fieldname: 'additionalDiscount',
minvalue: 0,
}"
size="large"
:value="additionalDiscounts"
:read-only="true"
:text-right="true"
@change="(amount:Money)=> additionalDiscounts= amount"
/>
</div>
<div class="mt-4 grid grid-cols-2 gap-2">
<FloatingLabelCurrencyInput
:df="{
label: t`Item Discounts`,
fieldtype: 'Currency',
fieldname: 'itemDiscounts',
}"
size="large"
:value="itemDiscounts"
:read-only="true"
:text-right="true"
/>
<FloatingLabelCurrencyInput
v-if="sinvDoc.fieldMap"
:df="sinvDoc.fieldMap.grandTotal"
size="large"
:value="sinvDoc.grandTotal"
:read-only="true"
:text-right="true"
/>
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<Button
class="w-full bg-violet-500 dark:bg-violet-700 py-6"
:disabled="!sinvDoc.party || !sinvDoc.items?.length"
@click="handleSaveInvoiceAction"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Save` }}
</p>
</slot>
</Button>
<Button
class="w-full mt-4 bg-blue-500 dark:bg-blue-700 py-6"
@click="toggleModal('SavedInvoice', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`held` }}
</p>
</slot>
</Button>
</div>
<div class="w-full">
<Button
class="w-full bg-red-500 dark:bg-red-700 py-6"
:disabled="!sinvDoc.items?.length"
@click="clearValues"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Cancel` }}
</p>
</slot>
</Button>
<Button
class="mt-4 w-full bg-green-500 dark:bg-green-700 py-6"
:disabled="disablePayButton"
@click="toggleModal('Payment', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Pay` }}
</p>
</slot>
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Button from 'src/components/Button.vue';
import ClosePOSShiftModal from './ClosePOSShiftModal.vue';
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
import ItemsTable from 'src/components/POS/ItemsTable.vue';
import Link from 'src/components/Controls/Link.vue';
import OpenPOSShiftModal from './OpenPOSShiftModal.vue';
import PageHeader from 'src/components/PageHeader.vue';
import PaymentModal from './PaymentModal.vue';
import SelectedItemTable from 'src/components/POS/SelectedItemTable.vue';
import { computed, defineComponent } from 'vue';
import { fyo } from 'src/initFyo';
import { routeTo, toggleSidebar } from 'src/utils/ui';
import { ModelNameEnum } from 'models/types';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import ItemsGrid from 'src/components/POS/ItemsGrid.vue';
import { t } from 'fyo'; import { t } from 'fyo';
import { import { Money } from 'pesa';
ItemQtyMap, import { fyo } from 'src/initFyo';
ItemSerialNumbers, import ModernPOS from './ModernPOS.vue';
POSItem, import ClassicPOS from './ClassicPOS.vue';
} from 'src/components/POS/types'; import { ModelNameEnum } from 'models/types';
import Button from 'src/components/Button.vue';
import { computed, defineComponent } from 'vue';
import { showToast } from 'src/utils/interactive';
import { Item } from 'models/baseModels/Item/Item'; import { Item } from 'models/baseModels/Item/Item';
import { ModalName } from 'src/components/POS/types'; import { ModalName } from 'src/components/POS/types';
import { Money } from 'pesa';
import { Payment } from 'models/baseModels/Payment/Payment';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { Shipment } from 'models/inventory/Shipment'; import { Shipment } from 'models/inventory/Shipment';
import { showToast } from 'src/utils/interactive'; import { routeTo, toggleSidebar } from 'src/utils/ui';
import PageHeader from 'src/components/PageHeader.vue';
import { Payment } from 'models/baseModels/Payment/Payment';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes';
import { import {
getItem, validateSinv,
getItemDiscounts,
getItemQtyMap, getItemQtyMap,
getItemDiscounts,
validateShipment,
getTotalQuantity, getTotalQuantity,
getTotalTaxedAmount, getTotalTaxedAmount,
validateIsPosSettingsSet, validateIsPosSettingsSet,
validateShipment,
validateSinv,
} from 'src/utils/pos'; } from 'src/utils/pos';
import Barcode from 'src/components/Controls/Barcode.vue';
import { import {
getAddedLPWithGrandTotal,
getPricingRule, getPricingRule,
removeFreeItems, removeFreeItems,
getAddedLPWithGrandTotal,
} from 'models/helpers'; } from 'models/helpers';
import LoyaltyProgramModal from './LoyaltyprogramModal.vue'; import {
import AlertModal from './AlertModal.vue'; POSItem,
import SavedInvoiceModal from './SavedInvoiceModal.vue'; ItemQtyMap,
import CouponCodeModal from './CouponCodeModal.vue'; ItemSerialNumbers,
import { AppliedCouponCodes } from 'models/baseModels/AppliedCouponCodes/AppliedCouponCodes'; } from 'src/components/POS/types';
import MultiLabelLink from 'src/components/Controls/MultiLabelLink.vue';
export default defineComponent({ export default defineComponent({
name: 'POS', name: 'POS',
components: { components: {
Button, Button,
ClosePOSShiftModal, ModernPOS,
FloatingLabelCurrencyInput,
FloatingLabelFloatInput,
ItemsTable,
ItemsGrid,
Link,
MultiLabelLink,
AlertModal,
OpenPOSShiftModal,
PageHeader, PageHeader,
PaymentModal, ClassicPOS,
LoyaltyProgramModal,
SavedInvoiceModal,
CouponCodeModal,
SelectedItemTable,
Barcode,
}, },
provide() { provide() {
return { return {
cashAmount: computed(() => this.cashAmount),
doc: computed(() => this.sinvDoc), doc: computed(() => this.sinvDoc),
isDiscountingEnabled: computed(() => this.isDiscountingEnabled),
itemDiscounts: computed(() => this.itemDiscounts),
itemQtyMap: computed(() => this.itemQtyMap),
itemSerialNumbers: computed(() => this.itemSerialNumbers),
sinvDoc: computed(() => this.sinvDoc), sinvDoc: computed(() => this.sinvDoc),
appliedCoupons: computed(() => this.sinvDoc.coupons),
coupons: computed(() => this.coupons), coupons: computed(() => this.coupons),
totalTaxedAmount: computed(() => this.totalTaxedAmount), itemQtyMap: computed(() => this.itemQtyMap),
transferAmount: computed(() => this.transferAmount), cashAmount: computed(() => this.cashAmount),
transferClearanceDate: computed(() => this.transferClearanceDate),
transferRefNo: computed(() => this.transferRefNo), transferRefNo: computed(() => this.transferRefNo),
itemDiscounts: computed(() => this.itemDiscounts),
transferAmount: computed(() => this.transferAmount),
appliedCoupons: computed(() => this.sinvDoc.coupons),
totalTaxedAmount: computed(() => this.totalTaxedAmount),
itemSerialNumbers: computed(() => this.itemSerialNumbers),
isDiscountingEnabled: computed(() => this.isDiscountingEnabled),
transferClearanceDate: computed(() => this.transferClearanceDate),
}; };
}, },
data() { data() {
return { return {
items: [] as POSItem[],
tableView: true, tableView: true,
isItemsSeeded: false, items: [] as POSItem[],
openPaymentModal: false,
openLoyaltyProgramModal: false,
openSavedInvoiceModal: false,
openCouponCodeModal: false,
openAppliedCouponsModal: false,
openShiftCloseModal: false,
openShiftOpenModal: false,
openRouteToInvoiceListModal: false,
additionalDiscounts: fyo.pesa(0), openAlertModal: false,
cashAmount: fyo.pesa(0), openPaymentModal: false,
itemDiscounts: fyo.pesa(0), openKeyboardModal: false,
totalTaxedAmount: fyo.pesa(0), openCouponCodeModal: false,
transferAmount: fyo.pesa(0), openShiftCloseModal: false,
openSavedInvoiceModal: false,
openLoyaltyProgramModal: false,
openAppliedCouponsModal: false,
totalQuantity: 0, totalQuantity: 0,
cashAmount: fyo.pesa(0),
itemDiscounts: fyo.pesa(0),
transferAmount: fyo.pesa(0),
totalTaxedAmount: fyo.pesa(0),
additionalDiscounts: fyo.pesa(0),
loyaltyPoints: 0, loyaltyPoints: 0,
appliedLoyaltyPoints: 0, appliedLoyaltyPoints: 0,
loyaltyProgram: '' as string, loyaltyProgram: '' as string,
appliedCoupons: [] as AppliedCouponCodes[],
appliedCouponsCount: 0, appliedCouponsCount: 0,
appliedCoupons: [] as AppliedCouponCodes[],
defaultCustomer: undefined as string | undefined,
itemSearchTerm: '', itemSearchTerm: '',
transferRefNo: undefined as string | undefined, transferRefNo: undefined as string | undefined,
defaultCustomer: undefined as string | undefined,
transferClearanceDate: undefined as Date | undefined, transferClearanceDate: undefined as Date | undefined,
itemQtyMap: {} as ItemQtyMap,
itemSerialNumbers: {} as ItemSerialNumbers,
paymentDoc: {} as Payment, paymentDoc: {} as Payment,
sinvDoc: {} as SalesInvoice, sinvDoc: {} as SalesInvoice,
itemQtyMap: {} as ItemQtyMap,
coupons: {} as AppliedCouponCodes, coupons: {} as AppliedCouponCodes,
itemSerialNumbers: {} as ItemSerialNumbers,
}; };
}, },
computed: { computed: {
@ -659,24 +192,11 @@ export default defineComponent({
return !!fyo.singles.AccountingSettings?.enableDiscounting; return !!fyo.singles.AccountingSettings?.enableDiscounting;
}, },
isPosShiftOpen: () => !!fyo.singles.POSShift?.isShiftOpen, isPosShiftOpen: () => !!fyo.singles.POSShift?.isShiftOpen,
isPaymentAmountSet(): boolean {
if (this.sinvDoc.grandTotal?.isZero()) {
return true;
}
if (this.cashAmount.isZero() && this.transferAmount.isZero()) {
return false;
}
return true;
},
disablePayButton(): boolean { disablePayButton(): boolean {
if (!this.sinvDoc.items?.length) { if (!this.sinvDoc.items?.length || !this.sinvDoc.party) {
return true; return true;
} }
if (!this.sinvDoc.party) {
return true;
}
return false; return false;
}, },
}, },
@ -816,7 +336,6 @@ export default defineComponent({
}, },
async setLoyaltyPoints(value: number) { async setLoyaltyPoints(value: number) {
this.appliedLoyaltyPoints = value; this.appliedLoyaltyPoints = value;
this.sinvDoc.redeemLoyaltyPoints = true; this.sinvDoc.redeemLoyaltyPoints = true;
const totalLotaltyAmount = await getAddedLPWithGrandTotal( const totalLotaltyAmount = await getAddedLPWithGrandTotal(
@ -851,7 +370,6 @@ export default defineComponent({
}, },
async addItem(item: POSItem | Item | undefined) { async addItem(item: POSItem | Item | undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
await this.sinvDoc.runFormulas(); await this.sinvDoc.runFormulas();
if (!item) { if (!item) {
@ -912,8 +430,10 @@ export default defineComponent({
if (existingItems.length) { if (existingItems.length) {
existingItems[0].rate = item.rate as Money; existingItems[0].rate = item.rate as Money;
existingItems[0].quantity = (existingItems[0].quantity as number) + 1; existingItems[0].quantity = (existingItems[0].quantity as number) + 1;
await this.applyPricingRule(); await this.applyPricingRule();
await this.sinvDoc.runFormulas(); await this.sinvDoc.runFormulas();
return; return;
} }
@ -943,6 +463,7 @@ export default defineComponent({
async makePayment() { async makePayment() {
this.paymentDoc = this.sinvDoc.getPayment() as Payment; this.paymentDoc = this.sinvDoc.getPayment() as Payment;
const paymentMethod = this.cashAmount.isZero() ? 'Transfer' : 'Cash'; const paymentMethod = this.cashAmount.isZero() ? 'Transfer' : 'Cash';
await this.paymentDoc.set('paymentMethod', paymentMethod); await this.paymentDoc.set('paymentMethod', paymentMethod);
if (paymentMethod === 'Transfer') { if (paymentMethod === 'Transfer') {
@ -1067,6 +588,7 @@ export default defineComponent({
if (value) { if (value) {
return (this[`open${modal}Modal`] = value); return (this[`open${modal}Modal`] = value);
} }
return (this[`open${modal}Modal`] = !this[`open${modal}Modal`]); return (this[`open${modal}Modal`] = !this[`open${modal}Modal`]);
}, },
updateValues() { updateValues() {
@ -1087,6 +609,7 @@ export default defineComponent({
this.sinvDoc.pricingRuleDetail = undefined; this.sinvDoc.pricingRuleDetail = undefined;
this.sinvDoc.isPricingRuleApplied = false; this.sinvDoc.isPricingRuleApplied = false;
removeFreeItems(this.sinvDoc as SalesInvoice); removeFreeItems(this.sinvDoc as SalesInvoice);
return; return;
} }
@ -1113,16 +636,16 @@ export default defineComponent({
return await routeTo('/list/SalesInvoice'); return await routeTo('/list/SalesInvoice');
} }
this.openRouteToInvoiceListModal = true; this.openAlertModal = true;
}, },
async handleSaveInvoiceAction() { async saveInvoiceAction() {
if (!this.sinvDoc.party && !this.sinvDoc.items?.length) { if (!this.sinvDoc.party && !this.sinvDoc.items?.length) {
return; return;
} }
await this.saveOrder(); await this.saveOrder();
}, },
routeTo, routeTo,
getItem,
}, },
}); });
</script> </script>

View File

@ -0,0 +1,320 @@
<template>
<div class="relative group">
<div class="bg-gray-100 p-1.5 rounded-md" @click="toggleItemsView">
<FeatherIcon
:name="tableView ? 'grid' : 'list'"
class="w-5 h-5 text-black"
/>
</div>
<span
class="
p-2
mb-2
w-20
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
text-center
opacity-0
bg-gray-100
text-black text-xs
rounded-md
transition-opacity
duration-300
group-hover:opacity-100
dark:bg-gray-800 dark:text-white
"
>
{{ tableView ? 'Grid View' : 'List View' }}
</span>
</div>
<div class="relative group">
<div
class="px-1.5 py-1 rounded-md bg-gray-100"
@click="() => $emit('emitRouteToSinvList')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="21"
fill="#000"
>
<path
d="M240-100q-41.92 0-70.96-29.04Q140-158.08 140-199.82V-300h120v-552.31l55.39 47.7 56.15-47.7 56.15 47.7 56.16-47.7 56.15 47.7 56.15-47.7 56.16 47.7 56.15-47.7 56.15 47.7 55.39-47.7V-200q0 41.92-29.04 70.96Q761.92-100 720-100H240Zm480-60q17 0 28.5-11.5T760-200v-560H320v460h360v100q0 17 11.5 28.5T720-160ZM367.69-610v-60h226.92v60H367.69Zm0 120v-60h226.92v60H367.69Zm310-114.62q-14.69 0-25.04-10.34-10.34-10.35-10.34-25.04t10.34-25.04q10.35-10.34 25.04-10.34t25.04 10.34q10.35 10.35 10.35 25.04t-10.35 25.04q-10.35 10.34-25.04 10.34Zm0 120q-14.69 0-25.04-10.34-10.34-10.35-10.34-25.04t10.34-25.04q10.35-10.34 25.04-10.34t25.04 10.34q10.35 10.35 10.35 25.04t-10.35 25.04q-10.35 10.34-25.04 10.34ZM240-160h380v-80H200v40q0 17 11.5 28.5T240-160Zm-40 0v-80 80Z"
/>
</svg>
</div>
<span
class="
mb-2
p-2
w-28
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
rounded-md
opacity-0
bg-gray-100
text-black text-xs text-center
transition-opacity
duration-300
group-hover:opacity-100
dark:bg-gray-800 dark:text-white
"
>
Sales Invoice List
</span>
</div>
<div
class="relative group"
:class="{
hidden: !fyo.singles.AccountingSettings?.enableLoyaltyProgram,
}"
>
<div
class="p-1 rounded-md bg-gray-100"
:class="{
'bg-gray-100': loyaltyPoints,
'dark:bg-gray-600 cursor-not-allowed':
!loyaltyPoints || !sinvDoc?.party || !sinvDoc?.items?.length,
}"
@click="
loyaltyPoints && sinvDoc?.party && sinvDoc?.items?.length
? toggleModal('LoyaltyProgram', true)
: null
"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="23px"
viewBox="0 -960 960 960"
width="25px"
fill="#000"
>
<path
d="M100-180v-600h760v600H100Zm50.26-50.26h659.48v-499.48H150.26v499.48Zm0 0v-499.48 499.48Zm181.64-56.77h50.25v-42.56h48.67q14.37 0 23.6-10.38 9.22-10.38 9.22-24.25v-106.93q0-14.71-9.22-24.88-9.23-10.17-23.6-10.17H298.77v-73.95h164.87v-50.26h-81.49v-42.56H331.9v42.56h-48.41q-14.63 0-24.8 10.38-10.18 10.38-10.18 25v106.27q0 14.62 10.18 23.71 10.17 9.1 24.8 9.1h129.9v76.1H248.51v50.26h83.39v42.56Zm312.97-27.94L705.9-376H583.85l61.02 61.03ZM583.85-574H705.9l-61.03-61.03L583.85-574Z"
/>
</svg>
</div>
<span
class="
mb-2
p-2
w-28
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
bg-gray-100
text-black text-xs
rounded-md
text-center
opacity-0
transition-opacity
duration-300
group-hover:opacity-100
dark:bg-gray-800 dark:text-white
"
>
Loyalty Program
</span>
</div>
<div
class="relative group"
:class="{
hidden: !fyo.singles.AccountingSettings?.enableCouponCode,
}"
>
<div
class="p-0.5 rounded-md bg-gray-100"
:class="{
'bg-gray-100': loyaltyPoints,
'dark:bg-gray-600 cursor-not-allowed':
!sinvDoc?.party || !sinvDoc?.items?.length,
}"
@click="openCouponModal()"
>
<svg
fill="#000000"
width="28px"
height="28px"
viewBox="0 0 512.00 512.00"
enable-background="new 0 0 512 512"
version="1.1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
stroke="#000000"
stroke-width="3.312000000000001"
transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"
>
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"
stroke="#CCCCCC"
stroke-width="19.456"
></g>
<g id="SVGRepo_iconCarrier">
<g id="Layer_1"></g>
<g id="Layer_2">
<g>
<path
d="M412.7,134.4H229.6c-2,0-3.9,0.8-5.3,2.2l-27.8,27.8L169.1,137c-1.4-1.4-3.3-2.2-5.3-2.2H99.3c-4.1,0-7.5,3.4-7.5,7.5 v227.4c0,4.1,3.4,7.5,7.5,7.5h64.5c2,0,3.9-0.8,5.3-2.2l27.4-27.4l27.8,27.8c1.4,1.4,3.3,2.2,5.3,2.2h183.1c4.1,0,7.5-3.4,7.5-7.5 V141.9C420.2,137.7,416.8,134.4,412.7,134.4z M405.2,362.6H232.7l-30.9-30.9c-2.9-2.9-7.7-2.9-10.6,0l-30.5,30.5h-53.9V149.8h53.9 l30.5,30.5c2.9,2.9,7.7,2.9,10.6,0l30.9-30.9h172.5V362.6z"
></path>
<path
d="M276.9,235.2c15.4,0,28-12.6,28-28s-12.6-28-28-28s-28,12.6-28,28S261.4,235.2,276.9,235.2z M276.9,194.2 c7.2,0,13,5.8,13,13s-5.8,13-13,13s-13-5.8-13-13S269.7,194.2,276.9,194.2z"
></path>
<path
d="M360,262.4c-15.4,0-28,12.6-28,28s12.6,28,28,28s28-12.6,28-28S375.4,262.4,360,262.4z M360,303.4c-7.2,0-13-5.8-13-13 s5.8-13,13-13s13,5.8,13,13S367.2,303.4,360,303.4z"
></path>
<path
d="M256.6,310.7c1.5,1.5,3.4,2.2,5.3,2.2s3.8-0.7,5.3-2.2l113.1-113.1c2.9-2.9,2.9-7.7,0-10.6c-2.9-2.9-7.7-2.9-10.6,0 L256.6,300.1C253.6,303,253.6,307.7,256.6,310.7z"
></path>
<path
d="M196.5,202.5c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,203.3,198.4,202.5,196.5,202.5z"
></path>
<path
d="M196.5,233.2c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,234,198.4,233.2,196.5,233.2z"
></path>
<path
d="M196.5,263.8c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,264.6,198.4,263.8,196.5,263.8z"
></path>
<path
d="M196.5,294.5c-2,0-3.9,0.8-5.3,2.2c-1.4,1.4-2.2,3.3-2.2,5.3c0,2,0.8,3.9,2.2,5.3c1.4,1.4,3.3,2.2,5.3,2.2 c2,0,3.9-0.8,5.3-2.2c1.4-1.4,2.2-3.3,2.2-5.3c0-2-0.8-3.9-2.2-5.3C200.4,295.3,198.4,294.5,196.5,294.5z"
></path>
</g>
</g>
</g>
</svg>
</div>
<span
class="
mb-2
p-2
w-28
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
bg-gray-100
text-black text-xs
rounded-md
text-center
opacity-0
transition-opacity
duration-300
group-hover:opacity-100
dark:bg-gray-800 dark:text-white
"
>
Coupon Code
</span>
<div
v-if="appliedCouponsCount !== 0"
class="
h-4
w-4
p-2
absolute
top-0
right-0
transform
translate-x-1/2
-translate-y-1/2
bg-green-400
text-green-900
border-red-500
rounded-full
flex
items-center
justify-center
text-xs
cursor-pointer
"
>
{{ appliedCouponsCount }}
</div>
</div>
</template>
<script lang="ts">
import { fyo } from 'src/initFyo';
import { defineComponent, PropType } from 'vue';
import { ModalName } from 'src/components/POS/types';
import { Payment } from 'models/baseModels/Payment/Payment';
import { ItemSerialNumbers } from 'src/components/POS/types';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
export default defineComponent({
name: 'POSQuickActions',
props: {
openAlertModal: Boolean,
loyaltyPoints: {
type: Number,
default: 0,
},
loyaltyProgram: {
type: String,
default: '',
},
appliedCouponsCount: {
type: Number,
default: 0,
},
sinvDoc: {
type: Object as PropType<SalesInvoice | undefined>,
default: undefined,
},
},
emits: ['toggleView', 'toggleModal', 'emitRouteToSinvList'],
data() {
return {
tableView: true,
totalQuantity: 0,
totalTaxedAmount: fyo.pesa(0),
additionalDiscounts: fyo.pesa(0),
paymentDoc: {} as Payment,
itemSerialNumbers: {} as ItemSerialNumbers,
itemSearchTerm: '',
transferRefNo: undefined as string | undefined,
transferClearanceDate: undefined as Date | undefined,
};
},
computed: {
isPosShiftOpen: () => !!fyo.singles.POSShift?.isShiftOpen,
},
methods: {
setTransferRefNo(ref: string) {
this.transferRefNo = ref;
},
toggleItemsView() {
this.tableView = !this.tableView;
this.$emit('toggleView', !this.tableView);
},
toggleModal(modal: ModalName, value: boolean) {
this.$emit('toggleModal', modal, value);
},
openCouponModal() {
if (this.sinvDoc?.party && this.sinvDoc?.items?.length) {
this.toggleModal('CouponCode', true);
}
},
},
});
</script>

View File

@ -152,7 +152,7 @@
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto"> <div class="row-start-6 grid grid-cols-2 gap-4 mt-auto">
<div class="col-span-2"> <div class="col-span-2">
<Button <Button
class="w-full bg-red-500" class="w-full bg-red-500 dark:bg-red-700"
style="padding: 1.35rem" style="padding: 1.35rem"
@click="$emit('toggleModal', 'Payment')" @click="$emit('toggleModal', 'Payment')"
> >
@ -166,7 +166,7 @@
<div class="col-span-1"> <div class="col-span-1">
<Button <Button
class="w-full bg-blue-500" class="w-full bg-blue-500 dark:bg-blue-700"
style="padding: 1.35rem" style="padding: 1.35rem"
:disabled="disableSubmitButton" :disabled="disableSubmitButton"
@click="submitTransaction()" @click="submitTransaction()"
@ -180,7 +180,7 @@
</div> </div>
<div class="col-span-1"> <div class="col-span-1">
<Button <Button
class="w-full bg-green-500" class="w-full bg-green-500 dark:bg-green-700"
style="padding: 1.35rem" style="padding: 1.35rem"
:disabled="disableSubmitButton" :disabled="disableSubmitButton"
@click="$emit('createTransaction', true)" @click="$emit('createTransaction', true)"

View File

@ -102,16 +102,12 @@ function getInventorySidebar(): SidebarRoot[] {
} }
function getPOSSidebar() { function getPOSSidebar() {
const isPOSEnabled = !!fyo.singles.InventorySettings?.enablePointOfSale;
if (!isPOSEnabled) {
return [];
}
return { return {
label: t`POS`, label: t`POS`,
name: 'pos', name: 'pos',
route: '/pos', route: '/pos',
icon: 'pos', icon: 'pos',
hidden: () => !fyo.singles.InventorySettings?.enablePointOfSale,
}; };
} }