2
0
mirror of https://github.com/frappe/books.git synced 2025-01-03 07:12:21 +00:00

feat: multiple payment methods in pos

This commit is contained in:
akshayitzme 2024-12-27 16:31:23 +05:30
parent 7cbe5c6840
commit 7f704fb8d9
10 changed files with 170 additions and 104 deletions

View File

@ -32,16 +32,19 @@ async function execute(dm: DatabaseManager) {
const paymentMethods = [
{
name: 'Cash',
type: 'Cash',
account: accountsMap[AccountTypeEnum.Cash]?.[0],
...defaults,
},
{
name: 'Bank',
type: 'Bank',
account: accountsMap[AccountTypeEnum.Bank]?.[0],
...defaults,
},
{
name: 'Transfer',
type: 'Bank',
account: accountsMap[AccountTypeEnum.Bank]?.[0],
...defaults,
},

View File

@ -9,7 +9,6 @@ import {
FormulaMap,
HiddenMap,
ListViewSettings,
RequiredMap,
ValidationMap,
} from 'fyo/model/types';
import { NotFoundError, ValidationError } from 'fyo/utils/errors';
@ -44,6 +43,10 @@ export class Payment extends Transactional {
for?: PaymentFor[];
_accountsMap?: AccountTypeMap;
async paymentMethodDoc() {
return (await this.loadAndGetLink('paymentMethod')) as PaymentMethod;
}
async change({ changed }: ChangeArg) {
if (changed === 'for') {
this.updateAmountOnReferenceUpdate();
@ -112,6 +115,7 @@ export class Payment extends Transactional {
this.validateAccounts();
this.validateTotalReferenceAmount();
await this.validateReferences();
await this.validateReferencesAreSet();
}
async validateFor() {
@ -225,6 +229,22 @@ export class Payment extends Transactional {
);
}
async validateReferencesAreSet() {
const type = (await this.paymentMethodDoc()).type;
if (type !== 'Bank') {
return;
}
if (!this.clearanceDate) {
throw new ValidationError(t`Clearance Date not set.`);
}
if (!this.referenceId) {
throw new ValidationError(t`Reference Id not set.`);
}
}
async getTaxSummary() {
const taxes: Record<
string,
@ -561,15 +581,13 @@ export class Payment extends Transactional {
);
}
if (this.paymentMethod === 'Cash') {
const paymentMethodDoc = await this.paymentMethodDoc();
if (paymentMethodDoc.type === 'Cash') {
return accountsMap[AccountTypeEnum.Cash]?.[0] ?? null;
}
if (this.paymentMethod !== 'Cash') {
return accountsMap[AccountTypeEnum.Bank]?.[0] ?? null;
}
return null;
return accountsMap[AccountTypeEnum.Bank]?.[0] ?? null;
},
dependsOn: ['paymentMethod', 'paymentType', 'party'],
},
@ -584,23 +602,17 @@ export class Payment extends Transactional {
);
}
const paymentMethodDoc = (await this.loadAndGetLink(
'paymentMethod'
)) as PaymentMethod;
const paymentMethodDoc = await this.paymentMethodDoc();
if (paymentMethodDoc.account) {
return paymentMethodDoc.get('account');
}
if (this.paymentMethod === 'Cash') {
if (paymentMethodDoc.type === 'Cash') {
return accountsMap[AccountTypeEnum.Cash]?.[0] ?? null;
}
if (this.paymentMethod !== 'Cash') {
return accountsMap[AccountTypeEnum.Bank]?.[0] ?? null;
}
return null;
return accountsMap[AccountTypeEnum.Bank]?.[0] ?? null;
},
dependsOn: ['paymentMethod', 'paymentType', 'party'],
},
@ -683,14 +695,7 @@ export class Payment extends Transactional {
},
};
required: RequiredMap = {
referenceId: () => this.paymentMethod !== 'Cash',
clearanceDate: () => this.paymentMethod !== 'Cash',
};
hidden: HiddenMap = {
referenceId: () => this.paymentMethod === 'Cash',
clearanceDate: () => this.paymentMethod === 'Cash',
amountPaid: () => this.writeoff?.isZero() ?? true,
attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)),

View File

@ -1,7 +1,16 @@
import { Doc } from 'fyo/model/doc';
import { Account } from '../Account/Account';
import { ListViewSettings } from 'fyo/model/types';
import { PaymentMethodType } from 'models/types';
export class PaymentMethod extends Doc {
name?: string;
account?: Account;
type?: PaymentMethodType;
static getListViewSettings(): ListViewSettings {
return {
columns: ['name', 'type'],
};
}
}

View File

@ -69,3 +69,5 @@ export enum ModelNameEnum {
}
export type ModelName = keyof typeof ModelNameEnum;
export type PaymentMethodType= 'Cash' | 'Bank'

View File

@ -8,6 +8,22 @@
"label": "Name",
"fieldtype": "Data"
},
{
"fieldname": "type",
"label": "Type",
"fieldtype": "Select",
"required": true,
"options": [
{
"value": "Cash",
"label": "Cash"
},
{
"value": "Bank",
"label": "Bank"
}
]
},
{
"fieldname": "account",
"label": "Account",

View File

@ -25,7 +25,8 @@ export type PosEmits =
| 'addItem'
| 'toggleView'
| 'toggleModal'
| 'setCashAmount'
| 'setPaidAmount'
| 'setPaymentMethod'
| 'setCouponsCount'
| 'routeToSinvList'
| 'applyPricingRule'

View File

@ -43,9 +43,11 @@
<PaymentModal
:open-modal="openPaymentModal"
@toggle-modal="emitEvent('toggleModal', 'Payment')"
@set-cash-amount="(amount) => emitEvent('setCashAmount', amount)"
@set-paid-amount="(amount: Money) => emitEvent('setPaidAmount', amount)"
@set-payment-method="
(paymentMethod) => emitEvent('setPaymentMethod', paymentMethod)
"
@set-transfer-ref-no="(ref) => emitEvent('setTransferRefNo', ref)"
@set-transfer-amount="(amount) => emitEvent('setTransferAmount', amount)"
@set-transfer-clearance-date="
(date) => emitEvent('setTransferClearanceDate', date)
"
@ -338,7 +340,7 @@ export default defineComponent({
FloatingLabelCurrencyInput,
},
props: {
cashAmount: Money,
paidAmount: Money,
tableView: Boolean,
itemDiscounts: Money,
openAlertModal: Boolean,
@ -390,9 +392,10 @@ export default defineComponent({
'toggleModal',
'setCustomer',
'clearValues',
'setCashAmount',
'setPaidAmount',
'setCouponsCount',
'routeToSinvList',
'setPaymentMethod',
'setTransferRefNo',
'setLoyaltyPoints',
'applyPricingRule',
@ -409,7 +412,10 @@ export default defineComponent({
};
},
methods: {
emitEvent(eventName: PosEmits, ...args: (string | boolean | Item)[]) {
emitEvent(
eventName: PosEmits,
...args: (string | boolean | Item | Money)[]
) {
this.$emit(eventName, ...args);
},
getItem,

View File

@ -43,9 +43,11 @@
<PaymentModal
:open-modal="openPaymentModal"
@toggle-modal="emitEvent('toggleModal', 'Payment')"
@set-cash-amount="(amount) => emitEvent('setCashAmount', amount)"
@set-paid-amount="(amount) => emitEvent('setPaidAmount', amount)"
@set-payment-method="
(paymentMethod) => emitEvent('setPaymentMethod', paymentMethod)
"
@set-transfer-ref-no="(ref) => emitEvent('setTransferRefNo', ref)"
@set-transfer-amount="(amount) => emitEvent('setTransferAmount', amount)"
@set-transfer-clearance-date="
(date) => emitEvent('setTransferClearanceDate', date)
"
@ -349,7 +351,7 @@ export default defineComponent({
ModernPOSSelectedItemTable,
},
props: {
cashAmount: Money,
paidAmount: Money,
tableView: Boolean,
itemDiscounts: Money,
openAlertModal: Boolean,
@ -402,10 +404,11 @@ export default defineComponent({
'toggleModal',
'setCustomer',
'clearValues',
'setCashAmount',
'setPaidAmount',
'setCouponsCount',
'routeToSinvList',
'setLoyaltyPoints',
'setPaymentMethod',
'setTransferRefNo',
'applyPricingRule',
'saveInvoiceAction',
@ -425,7 +428,10 @@ export default defineComponent({
};
},
methods: {
emitEvent(eventName: PosEmits, ...args: (string | boolean | Item)[]) {
emitEvent(
eventName: PosEmits,
...args: (string | boolean | Item | Money)[]
) {
this.$emit(eventName, ...args);
},
selectedRow(row: SalesInvoiceItem, field: string) {

View File

@ -20,7 +20,6 @@
:default-customer="defaultCustomer"
:is-pos-shift-open="isPosShiftOpen"
:items="(items as [] as POSItem[])"
:cash-amount="(cashAmount as Money)"
:sinv-doc="(sinvDoc as SalesInvoice)"
:disable-pay-button="disablePayButton"
:open-payment-modal="openPaymentModal"
@ -39,7 +38,8 @@
@clear-values="clearValues"
@set-customer="setCustomer"
@toggle-modal="toggleModal"
@set-cash-amount="setCashAmount"
@set-paid-amount="setPaidAmount"
@set-payment-method="setPaymentMethod"
@set-coupons-count="setCouponsCount"
@route-to-sinv-list="routeToSinvList"
@set-loyalty-points="setLoyaltyPoints"
@ -61,7 +61,6 @@
:default-customer="defaultCustomer"
:is-pos-shift-open="isPosShiftOpen"
:items="(items as [] as POSItem[])"
:cash-amount="(cashAmount as Money)"
:sinv-doc="(sinvDoc as SalesInvoice)"
:disable-pay-button="disablePayButton"
:open-payment-modal="openPaymentModal"
@ -81,7 +80,8 @@
@clear-values="clearValues"
@set-customer="setCustomer"
@toggle-modal="toggleModal"
@set-cash-amount="setCashAmount"
@set-paid-amount="setPaidAmount"
@set-payment-method="setPaymentMethod"
@set-coupons-count="setCouponsCount"
@route-to-sinv-list="routeToSinvList"
@apply-pricing-rule="applyPricingRule"
@ -155,7 +155,8 @@ export default defineComponent({
sinvDoc: computed(() => this.sinvDoc),
coupons: computed(() => this.coupons),
itemQtyMap: computed(() => this.itemQtyMap),
cashAmount: computed(() => this.cashAmount),
paidAmount: computed(() => this.paidAmount),
paymentMethod: computed(() => this.paymentMethod),
transferRefNo: computed(() => this.transferRefNo),
itemDiscounts: computed(() => this.itemDiscounts),
transferAmount: computed(() => this.transferAmount),
@ -188,7 +189,7 @@ export default defineComponent({
openAppliedCouponsModal: false,
totalQuantity: 0,
cashAmount: fyo.pesa(0),
paidAmount: fyo.pesa(0),
itemDiscounts: fyo.pesa(0),
transferAmount: fyo.pesa(0),
totalTaxedAmount: fyo.pesa(0),
@ -202,6 +203,7 @@ export default defineComponent({
appliedCoupons: [] as AppliedCouponCodes[],
itemSearchTerm: '',
paymentMethod: undefined as string | undefined,
transferRefNo: undefined as string | undefined,
defaultCustomer: undefined as string | undefined,
transferClearanceDate: undefined as Date | undefined,
@ -396,8 +398,11 @@ export default defineComponent({
toggleView() {
this.tableView = !this.tableView;
},
setCashAmount(amount: Money) {
this.cashAmount = amount;
setPaidAmount(amount: Money) {
this.paidAmount = amount;
},
setPaymentMethod(method: string) {
this.paymentMethod = method;
},
setDefaultCustomer() {
this.defaultCustomer = this.fyo.singles.Defaults?.posCustomer ?? '';
@ -579,22 +584,26 @@ export default defineComponent({
},
async makePayment() {
this.paymentDoc = this.sinvDoc.getPayment() as Payment;
const paymentMethod = this.cashAmount.isZero() ? 'Transfer' : 'Cash';
const paymentMethod = this.paymentMethod;
await this.paymentDoc.set('paymentMethod', paymentMethod);
if (paymentMethod === 'Transfer') {
const paymentMethodDoc = await this.paymentDoc.loadAndGetLink(
'paymentMethod'
);
if (paymentMethodDoc?.type !== 'Cash') {
await this.paymentDoc.setMultiple({
amount: this.transferAmount as Money,
amount: this.paidAmount as Money,
referenceId: this.transferRefNo,
clearanceDate: this.transferClearanceDate,
});
}
if (paymentMethod === 'Cash') {
if (paymentMethodDoc?.type === 'Cash') {
await this.paymentDoc.setMultiple({
paymentAccount: this.defaultPOSCashAccount,
amount: this.cashAmount as Money,
amount: this.paidAmount as Money,
});
}
@ -688,7 +697,7 @@ export default defineComponent({
this.setSinvDoc();
this.itemSerialNumbers = {};
this.cashAmount = fyo.pesa(0);
this.paidAmount = fyo.pesa(0);
this.transferAmount = fyo.pesa(0);
await this.setItems();

View File

@ -1,43 +1,24 @@
<template>
<Modal class="w-2/6 ml-auto mr-3.5" :set-close-listener="false">
<div v-if="sinvDoc.fieldMap" class="px-4 py-6 grid" style="height: 95vh">
<Currency
:df="fyo.fieldMap.PaymentFor.amount"
:read-only="!transferAmount.isZero()"
:border="true"
:text-right="true"
:value="paidAmount"
@change="(amount:Money)=> $emit('setPaidAmount', amount)"
/>
<div class="grid grid-cols-2 gap-6">
<Currency
:df="fyo.fieldMap.PaymentFor.amount"
:read-only="!transferAmount.isZero()"
:border="true"
:text-right="true"
:value="cashAmount"
@change="(amount:Money)=> $emit('setCashAmount', amount)"
/>
<Button
v-for="method in paymentMethods"
:key="method"
class="w-full py-5 bg-teal-500"
@click="setCashOrTransferAmount"
@click="setPaymentMethodAndAmount(method, paidAmount)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Cash` }}
</p>
</slot>
</Button>
<Currency
:df="fyo.fieldMap.PaymentFor.amount"
:read-only="!cashAmount.isZero()"
:border="true"
:text-right="true"
:value="transferAmount"
@change="(value:Money)=> $emit('setTransferAmount', value)"
/>
<Button
class="w-full py-5 bg-teal-500"
@click="setCashOrTransferAmount('Transfer')"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Transfer` }}
{{ t`${method}` }}
</p>
</slot>
</Button>
@ -45,7 +26,7 @@
<div class="mt-8 grid grid-cols-2 gap-6">
<Data
v-show="!transferAmount.isZero()"
v-show="!isPaymentMethodIsCash"
:df="fyo.fieldMap.Payment.referenceId"
:show-label="true"
:border="true"
@ -55,7 +36,7 @@
/>
<Date
v-show="!transferAmount.isZero()"
v-show="!isPaymentMethodIsCash"
:df="fyo.fieldMap.Payment.clearanceDate"
:show-label="true"
:border="true"
@ -149,7 +130,7 @@
/>
</div>
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto">
<div class="grid grid-cols-2 gap-4 fixed bottom-8" style="width: 25rem">
<div class="col-span-2">
<Button
class="w-full bg-red-500 dark:bg-red-700"
@ -207,6 +188,8 @@ import { Money } from 'pesa';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { defineComponent, inject } from 'vue';
import { fyo } from 'src/initFyo';
import { isPesa } from 'fyo/utils';
import { ModelNameEnum } from 'models/types';
export default defineComponent({
name: 'PaymentModal',
@ -219,15 +202,16 @@ export default defineComponent({
},
emits: [
'createTransaction',
'setCashAmount',
'setTransferAmount',
'setPaidAmount',
'setPaymentMethod',
'setTransferClearanceDate',
'setTransferRefNo',
'toggleModal',
],
setup() {
return {
cashAmount: inject('cashAmount') as Money,
paidAmount: inject('paidAmount') as Money,
paymentMethod: inject('paymentMethod') as string,
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
itemDiscounts: inject('itemDiscounts') as Money,
transferAmount: inject('transferAmount') as Money,
@ -237,34 +221,46 @@ export default defineComponent({
totalTaxedAmount: inject('totalTaxedAmount') as Money,
};
},
data() {
return {
paymentMethods: [] as string[],
};
},
computed: {
isPaymentMethodIsCash(): boolean {
return this.paymentMethod === 'Cash';
},
balanceAmount(): Money {
const grandTotal = this.sinvDoc?.grandTotal ?? fyo.pesa(0);
if (this.cashAmount.isZero()) {
if (isPesa(this.paidAmount) && this.paidAmount.isZero()) {
return grandTotal.sub(this.transferAmount);
}
return grandTotal.sub(this.cashAmount);
return grandTotal.sub(this.paidAmount);
},
paidChange(): Money {
const grandTotal = this.sinvDoc?.grandTotal ?? fyo.pesa(0);
if (this.cashAmount.isZero()) {
if (this.fyo.pesa(this.paidAmount.float).isZero()) {
return this.transferAmount.sub(grandTotal);
}
return this.cashAmount.sub(grandTotal);
return this.fyo.pesa(this.paidAmount.float).sub(grandTotal);
},
showBalanceAmount(): boolean {
if (
this.cashAmount.eq(fyo.pesa(0)) &&
this.fyo.pesa(this.paidAmount.float).eq(fyo.pesa(0)) &&
this.transferAmount.eq(fyo.pesa(0))
) {
return false;
}
if (this.cashAmount.gte(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
if (
this.fyo
.pesa(this.paidAmount.float)
.gte(this.sinvDoc?.grandTotal ?? fyo.pesa(0))
) {
return false;
}
@ -276,13 +272,17 @@ export default defineComponent({
},
showPaidChange(): boolean {
if (
this.cashAmount.eq(fyo.pesa(0)) &&
this.fyo.pesa(this.paidAmount.float).eq(fyo.pesa(0)) &&
this.transferAmount.eq(fyo.pesa(0))
) {
return false;
}
if (this.cashAmount.gt(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
if (
this.fyo
.pesa(this.paidAmount.float)
.gt(this.sinvDoc?.grandTotal ?? fyo.pesa(0))
) {
return true;
}
@ -294,15 +294,14 @@ export default defineComponent({
},
disableSubmitButton(): boolean {
if (
!this.sinvDoc.grandTotal?.isZero() &&
this.transferAmount.isZero() &&
this.cashAmount.isZero()
(this.sinvDoc.grandTotal?.float as number) < 1 &&
this.fyo.pesa(this.paidAmount.float).isZero()
) {
return true;
}
if (
this.cashAmount.isZero() &&
this.paymentMethod !== 'Cash' &&
(!this.transferRefNo || !this.transferClearanceDate)
) {
return true;
@ -310,15 +309,25 @@ export default defineComponent({
return false;
},
},
async activated() {
await this.setPaymentMethods();
},
methods: {
setCashOrTransferAmount(paymentMethod = 'Cash') {
if (paymentMethod === 'Transfer') {
this.$emit('setCashAmount', fyo.pesa(0));
this.$emit('setTransferAmount', this.sinvDoc?.grandTotal);
return;
setPaymentMethodAndAmount(paymentMethod?: string, amount?: Money) {
if (paymentMethod) {
this.$emit('setPaymentMethod', paymentMethod);
}
this.$emit('setTransferAmount', fyo.pesa(0));
this.$emit('setCashAmount', this.sinvDoc?.grandTotal);
if (amount) {
this.$emit('setPaidAmount', this.sinvDoc.grandTotal?.float);
}
},
async setPaymentMethods() {
this.paymentMethods = (
(await this.fyo.db.getAll(ModelNameEnum.PaymentMethod, {
fields: ['name'],
})) as { name: string }[]
).map((d) => d.name);
},
submitTransaction() {
this.$emit('createTransaction');