2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 03:19:01 +00:00

Merge pull request #1036 from AbleKSaju/feat-pricelist

feat: PriceList functionality in POS
This commit is contained in:
Akshay 2024-11-29 10:13:51 +05:30 committed by GitHub
commit 08284b758f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 242 additions and 45 deletions

View File

@ -48,7 +48,7 @@ import { AppliedCouponCodes } from '../AppliedCouponCodes/AppliedCouponCodes';
import { CouponCode } from '../CouponCode/CouponCode'; import { CouponCode } from '../CouponCode/CouponCode';
import { SalesInvoice } from '../SalesInvoice/SalesInvoice'; import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
import { SalesInvoiceItem } from '../SalesInvoiceItem/SalesInvoiceItem'; import { SalesInvoiceItem } from '../SalesInvoiceItem/SalesInvoiceItem';
import { PriceListItem } from '../PriceList/PriceListItem'; import { PricingRuleItem } from '../PricingRuleItem/PricingRuleItem';
export type TaxDetail = { export type TaxDetail = {
account: string; account: string;
@ -1332,7 +1332,7 @@ export abstract class Invoice extends Transactional {
item: item.item as string, item: item.item as string,
unit: item.unit as string, unit: item.unit as string,
}, },
})) as PriceListItem[]; })) as PricingRuleItem[];
return docs.map((doc) => doc.parent) as string[]; return docs.map((doc) => doc.parent) as string[];
} }

View File

@ -17,9 +17,9 @@ import { safeParseFloat } from 'utils/index';
import { Invoice } from '../Invoice/Invoice'; import { Invoice } from '../Invoice/Invoice';
import { Item } from '../Item/Item'; import { Item } from '../Item/Item';
import { StockTransfer } from 'models/inventory/StockTransfer'; import { StockTransfer } from 'models/inventory/StockTransfer';
import { PriceList } from '../PriceList/PriceList';
import { isPesa } from 'fyo/utils'; import { isPesa } from 'fyo/utils';
import { PricingRule } from '../PricingRule/PricingRule'; import { PricingRule } from '../PricingRule/PricingRule';
import { getItemRateFromPriceList } from 'models/helpers';
export abstract class InvoiceItem extends Doc { export abstract class InvoiceItem extends Doc {
item?: string; item?: string;
@ -629,7 +629,10 @@ async function getItemRate(doc: InvoiceItem): Promise<Money | undefined> {
let priceListRate: Money | undefined; let priceListRate: Money | undefined;
if (doc.fyo.singles.AccountingSettings?.enablePriceList) { if (doc.fyo.singles.AccountingSettings?.enablePriceList) {
priceListRate = await getItemRateFromPriceList(doc); priceListRate = await getItemRateFromPriceList(
doc,
doc.parentdoc?.priceList as string
);
} }
if (priceListRate) { if (priceListRate) {
@ -675,43 +678,6 @@ async function getItemRateFromPricingRule(
return pricingRuleDoc.discountRate; return pricingRuleDoc.discountRate;
} }
async function getItemRateFromPriceList(
doc: InvoiceItem
): Promise<Money | undefined> {
const priceListName = doc.parentdoc?.priceList;
const item = doc.item;
if (!priceListName || !item) {
return;
}
const priceList = await doc.fyo.doc.getDoc(
ModelNameEnum.PriceList,
priceListName
);
if (!(priceList instanceof PriceList)) {
return;
}
const unit = doc.unit;
const transferUnit = doc.transferUnit;
const plItem = priceList.priceListItem?.find((pli) => {
if (pli.item !== item) {
return false;
}
if (transferUnit && pli.unit !== transferUnit) {
return false;
} else if (unit && pli.unit !== unit) {
return false;
}
return true;
});
return plItem?.rate;
}
function getDiscountedTotalBeforeTaxation( function getDiscountedTotalBeforeTaxation(
rate: Money, rate: Money,
quantity: number, quantity: number,

View File

@ -33,6 +33,9 @@ import { ValidationError } from 'fyo/utils/errors';
import { isPesa } from 'fyo/utils'; import { isPesa } from 'fyo/utils';
import { numberSeriesDefaultsMap } from './baseModels/Defaults/Defaults'; import { numberSeriesDefaultsMap } from './baseModels/Defaults/Defaults';
import { safeParseFloat } from 'utils/index'; import { safeParseFloat } from 'utils/index';
import { PriceList } from './baseModels/PriceList/PriceList';
import { InvoiceItem } from './baseModels/InvoiceItem/InvoiceItem';
import { SalesInvoiceItem } from './baseModels/SalesInvoiceItem/SalesInvoiceItem';
export function getQuoteActions( export function getQuoteActions(
fyo: Fyo, fyo: Fyo,
@ -947,6 +950,43 @@ export async function getPricingRule(
return pricingRules; return pricingRules;
} }
export async function getItemRateFromPriceList(
doc: InvoiceItem | SalesInvoiceItem,
priceListName: string
): Promise<Money | undefined> {
const item = doc.item;
if (!priceListName || !item) {
return;
}
const priceList = await doc.fyo.doc.getDoc(
ModelNameEnum.PriceList,
priceListName
);
if (!(priceList instanceof PriceList)) {
return;
}
const unit = doc.unit;
const transferUnit = doc.transferUnit;
const plItem = priceList.priceListItem?.find((pli) => {
if (pli.item !== item) {
return false;
}
if (transferUnit && pli.unit !== transferUnit) {
return false;
} else if (unit && pli.unit !== unit) {
return false;
}
return true;
});
return plItem?.rate;
}
export function filterPricingRules( export function filterPricingRules(
pricingRuleDocsForItem: PricingRule[], pricingRuleDocsForItem: PricingRule[],
sinvDate: Date, sinvDate: Date,

View File

@ -15,7 +15,8 @@ export type ModalName =
| 'LoyaltyProgram' | 'LoyaltyProgram'
| 'SavedInvoice' | 'SavedInvoice'
| 'Alert' | 'Alert'
| 'CouponCode'; | 'CouponCode'
| 'PriceList';
export type PosEmits = export type PosEmits =
| 'addItem' | 'addItem'

View File

@ -34,6 +34,11 @@
@set-coupons-count="(count) => emitEvent('setCouponsCount', count)" @set-coupons-count="(count) => emitEvent('setCouponsCount', count)"
/> />
<PriceListModal
:open-modal="openPriceListModal"
@toggle-modal="emitEvent('toggleModal', 'PriceList')"
/>
<PaymentModal <PaymentModal
:open-modal="openPaymentModal" :open-modal="openPaymentModal"
@toggle-modal="emitEvent('toggleModal', 'Payment')" @toggle-modal="emitEvent('toggleModal', 'Payment')"
@ -287,6 +292,7 @@ import AlertModal from './AlertModal.vue';
import PaymentModal from './PaymentModal.vue'; import PaymentModal from './PaymentModal.vue';
import Button from 'src/components/Button.vue'; import Button from 'src/components/Button.vue';
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import PriceListModal from './PriceListModal.vue';
import { Item } from 'models/baseModels/Item/Item'; import { Item } from 'models/baseModels/Item/Item';
import Link from 'src/components/Controls/Link.vue'; import Link from 'src/components/Controls/Link.vue';
import CouponCodeModal from './CouponCodeModal.vue'; import CouponCodeModal from './CouponCodeModal.vue';
@ -318,8 +324,9 @@ export default defineComponent({
ItemsTable, ItemsTable,
PaymentModal, PaymentModal,
MultiLabelLink, MultiLabelLink,
POSQuickActions, PriceListModal,
CouponCodeModal, CouponCodeModal,
POSQuickActions,
OpenPOSShiftModal, OpenPOSShiftModal,
SelectedItemTable, SelectedItemTable,
SavedInvoiceModal, SavedInvoiceModal,
@ -336,6 +343,7 @@ export default defineComponent({
isPosShiftOpen: Boolean, isPosShiftOpen: Boolean,
disablePayButton: Boolean, disablePayButton: Boolean,
openPaymentModal: Boolean, openPaymentModal: Boolean,
openPriceListModal: Boolean,
openCouponCodeModal: Boolean, openCouponCodeModal: Boolean,
openShiftCloseModal: Boolean, openShiftCloseModal: Boolean,
openSavedInvoiceModal: Boolean, openSavedInvoiceModal: Boolean,

View File

@ -34,6 +34,11 @@
@set-coupons-count="(count) => emitEvent('setCouponsCount', count)" @set-coupons-count="(count) => emitEvent('setCouponsCount', count)"
/> />
<PriceListModal
:open-modal="openPriceListModal"
@toggle-modal="emitEvent('toggleModal', 'PriceList')"
/>
<PaymentModal <PaymentModal
:open-modal="openPaymentModal" :open-modal="openPaymentModal"
@toggle-modal="emitEvent('toggleModal', 'Payment')" @toggle-modal="emitEvent('toggleModal', 'Payment')"
@ -297,6 +302,7 @@ import AlertModal from './AlertModal.vue';
import PaymentModal from './PaymentModal.vue'; import PaymentModal from './PaymentModal.vue';
import Button from 'src/components/Button.vue'; import Button from 'src/components/Button.vue';
import KeyboardModal from './KeyboardModal.vue'; import KeyboardModal from './KeyboardModal.vue';
import PriceListModal from './PriceListModal.vue';
import { Item } from 'models/baseModels/Item/Item'; import { Item } from 'models/baseModels/Item/Item';
import Link from 'src/components/Controls/Link.vue'; import Link from 'src/components/Controls/Link.vue';
import CouponCodeModal from './CouponCodeModal.vue'; import CouponCodeModal from './CouponCodeModal.vue';
@ -327,6 +333,7 @@ export default defineComponent({
PaymentModal, PaymentModal,
KeyboardModal, KeyboardModal,
MultiLabelLink, MultiLabelLink,
PriceListModal,
POSQuickActions, POSQuickActions,
CouponCodeModal, CouponCodeModal,
OpenPOSShiftModal, OpenPOSShiftModal,
@ -348,6 +355,7 @@ export default defineComponent({
disablePayButton: Boolean, disablePayButton: Boolean,
openPaymentModal: Boolean, openPaymentModal: Boolean,
openKeyboardModal: Boolean, openKeyboardModal: Boolean,
openPriceListModal: Boolean,
openCouponCodeModal: Boolean, openCouponCodeModal: Boolean,
openShiftCloseModal: Boolean, openShiftCloseModal: Boolean,
openSavedInvoiceModal: Boolean, openSavedInvoiceModal: Boolean,

View File

@ -26,6 +26,7 @@
:open-payment-modal="openPaymentModal" :open-payment-modal="openPaymentModal"
:item-discounts="(itemDiscounts as Money)" :item-discounts="(itemDiscounts as Money)"
:coupons="(coupons as AppliedCouponCodes)" :coupons="(coupons as AppliedCouponCodes)"
:open-price-list-modal="openPriceListModal"
:applied-coupons-count="appliedCouponsCount" :applied-coupons-count="appliedCouponsCount"
:open-shift-close-modal="openShiftCloseModal" :open-shift-close-modal="openShiftCloseModal"
:open-coupon-code-modal="openCouponCodeModal" :open-coupon-code-modal="openCouponCodeModal"
@ -66,6 +67,7 @@
:open-keyboard-modal="openKeyboardModal" :open-keyboard-modal="openKeyboardModal"
:item-discounts="(itemDiscounts as Money)" :item-discounts="(itemDiscounts as Money)"
:coupons="(coupons as AppliedCouponCodes)" :coupons="(coupons as AppliedCouponCodes)"
:open-price-list-modal="openPriceListModal"
:applied-coupons-count="appliedCouponsCount" :applied-coupons-count="appliedCouponsCount"
:open-shift-close-modal="openShiftCloseModal" :open-shift-close-modal="openShiftCloseModal"
:open-coupon-code-modal="openCouponCodeModal" :open-coupon-code-modal="openCouponCodeModal"
@ -124,6 +126,7 @@ import {
getPricingRule, getPricingRule,
removeFreeItems, removeFreeItems,
getAddedLPWithGrandTotal, getAddedLPWithGrandTotal,
getItemRateFromPriceList,
} from 'models/helpers'; } from 'models/helpers';
import { import {
POSItem, POSItem,
@ -165,6 +168,7 @@ export default defineComponent({
openAlertModal: false, openAlertModal: false,
openPaymentModal: false, openPaymentModal: false,
openKeyboardModal: false, openKeyboardModal: false,
openPriceListModal: false,
openCouponCodeModal: false, openCouponCodeModal: false,
openShiftCloseModal: false, openShiftCloseModal: false,
openSavedInvoiceModal: false, openSavedInvoiceModal: false,
@ -440,7 +444,10 @@ export default defineComponent({
} }
if (existingItems.length) { if (existingItems.length) {
if (!this.sinvDoc.priceList) {
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();
@ -454,6 +461,17 @@ export default defineComponent({
item: item.name, item: item.name,
}); });
if (this.sinvDoc.priceList) {
let itemData = this.sinvDoc.items?.filter(
(val) => val.item == item.name
) as SalesInvoiceItem[];
itemData[0].rate = await getItemRateFromPriceList(
itemData[0],
this.sinvDoc.priceList
);
}
await this.applyPricingRule(); await this.applyPricingRule();
await this.sinvDoc.runFormulas(); await this.sinvDoc.runFormulas();
}, },

View File

@ -139,7 +139,6 @@
<div <div
class="p-0.5 rounded-md bg-gray-100" class="p-0.5 rounded-md bg-gray-100"
:class="{ :class="{
'bg-gray-100': loyaltyPoints,
'dark:bg-gray-600 cursor-not-allowed': 'dark:bg-gray-600 cursor-not-allowed':
!sinvDoc?.party || !sinvDoc?.items?.length, !sinvDoc?.party || !sinvDoc?.items?.length,
}" }"
@ -249,6 +248,54 @@
{{ appliedCouponsCount }} {{ appliedCouponsCount }}
</div> </div>
</div> </div>
<div
class="relative group"
:class="{
hidden: !fyo.singles.AccountingSettings?.enablePriceList,
}"
>
<div
class="p-1 rounded-md bg-gray-100"
@click="$emit('toggleModal', 'PriceList')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="23px"
viewBox="0 -960 960 960"
width="24px"
fill="#000"
>
<path
d="M180.31-164q-27.01 0-45.66-18.65Q116-201.3 116-228.31v-503.38q0-27.01 18.65-45.66Q153.3-796 180.31-796h599.38q27.01 0 45.66 18.65Q844-758.7 844-731.69v503.38q0 27.01-18.65 45.66Q806.7-164 779.69-164H180.31Zm0-52h599.38q4.62 0 8.46-3.85 3.85-3.84 3.85-8.46v-503.38q0-4.62-3.85-8.46-3.84-3.85-8.46-3.85H180.31q-4.62 0-8.46 3.85-3.85 3.84-3.85 8.46v503.38q0 4.62 3.85 8.46 3.84 3.85 8.46 3.85ZM221-297h172v-52H221v52Zm361-77.23L737.77-530 701-566.77l-119 119-51-51L494.23-462 582-374.23ZM221-454h172v-52H221v52Zm0-156h172v-52H221v52Zm-53 394v-528 528Z"
/>
</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
"
>
Price List
</span>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -0,0 +1,109 @@
<template>
<Modal class="h-auto w-96" :set-close-listener="false">
<p class="text-center font-semibold py-3">{{ t`Apply Price List` }}</p>
<div class="px-10">
<hr class="dark:border-gray-800" />
<div class="flex justify-center pt-10">
<div class="flex justify-between w-full mb-20">
<div class="w-full">
<Link
v-if="sinvDoc.fieldMap"
class="flex-shrink-0 w-full"
:border="true"
:value="priceList"
:df="sinvDoc.fieldMap.priceList"
@change="(value) => (priceList = value)"
/>
</div>
<div class="w-10 flex justify-end items-center">
<feather-icon
name="trash"
class="w-5 text-xl text-red-500"
@click="removePriceList"
/>
</div>
</div>
</div>
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto mb-2">
<div class="col-span-2">
<Button
class="w-full bg-green-500 dark:bg-green-700"
style="padding: 1.35rem"
@click="setPriceList"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Save` }}
</p>
</slot>
</Button>
</div>
</div>
<div class="row-start-6 grid 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="$emit('toggleModal', 'PriceList')"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Cancel` }}
</p>
</slot>
</Button>
</div>
</div>
</div>
</Modal>
</template>
<script lang="ts">
import { t } from 'fyo';
import Modal from 'src/components/Modal.vue';
import { defineComponent, inject } from 'vue';
import Button from 'src/components/Button.vue';
import { showToast } from 'src/utils/interactive';
import Link from 'src/components/Controls/Link.vue';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
export default defineComponent({
name: 'PriceListModal',
components: {
Link,
Modal,
Button,
},
emits: ['toggleModal'],
setup() {
return {
sinvDoc: inject('sinvDoc') as SalesInvoice,
};
},
data() {
return {
priceList: '',
};
},
methods: {
async removePriceList() {
this.priceList = '';
await this.setPriceList();
},
async setPriceList() {
try {
await this.sinvDoc.set('priceList', this.priceList);
this.$emit('toggleModal', 'PriceList');
} catch (error) {
showToast({
type: 'error',
message: t`${error as string}`,
});
}
},
},
});
</script>