2
0
mirror of https://github.com/frappe/books.git synced 2025-02-02 12:08:27 +00:00

Merge pull request #1079 from AbleKSaju/feat-return-invoice

feat: return invoice functionality in POS
This commit is contained in:
Akshay 2025-01-14 16:55:32 +05:30 committed by GitHub
commit 0e120bf315
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 350 additions and 39 deletions

View File

@ -50,6 +50,7 @@ import { CouponCode } from '../CouponCode/CouponCode';
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
import { SalesInvoiceItem } from '../SalesInvoiceItem/SalesInvoiceItem';
import { PricingRuleItem } from '../PricingRuleItem/PricingRuleItem';
import { getLinkedEntries } from 'src/utils/doc';
export type TaxDetail = {
account: string;
@ -982,6 +983,17 @@ export abstract class Invoice extends Transactional {
return null;
}
let linkedEntries;
if (this.returnAgainst) {
const sinvDoc = (await this.fyo.doc.getDoc(
ModelNameEnum.SalesInvoice,
this.returnAgainst
)) as SalesInvoice;
linkedEntries = await getLinkedEntries(sinvDoc);
}
if (!this.stockNotTransferred) {
return null;
}
@ -1005,6 +1017,7 @@ export abstract class Invoice extends Transactional {
terms,
numberSeries,
backReference: this.name,
returnAgainst: linkedEntries ? linkedEntries.Shipment![0] : '',
};
let location = this.autoStockTransferLocation;

View File

@ -55,8 +55,7 @@
"label": "Is POS Shift Open",
"fieldtype": "Check",
"default": false,
"hidden": true,
"section": "Default"
"hidden": true
},
{
"fieldname": "weightEnabledBarcode",

View File

@ -17,6 +17,7 @@ export const modalNames = [
'Alert',
'CouponCode',
'PriceList',
'ReturnSalesInvoice',
] as const;
export type ModalName = typeof modalNames[number];
@ -35,6 +36,7 @@ export type PosEmits =
| 'setTransferAmount'
| 'createTransaction'
| 'selectedInvoiceName'
| 'selectedReturnInvoice'
| 'setTransferClearanceDate';
export interface POSItem {

View File

@ -56,6 +56,13 @@
"
/>
<ReturnSalesInvoiceModal
:open-modal="openReturnSalesInvoiceModal"
:modal-status="openReturnSalesInvoiceModal"
@selected-return-invoice="(value:any) => emitEvent('selectedReturnInvoice', value)"
@toggle-modal="emitEvent('toggleModal', 'ReturnSalesInvoice')"
/>
<AlertModal
:open-modal="openAlertModal"
@toggle-modal="emitEvent('toggleModal', 'Alert')"
@ -180,7 +187,7 @@
<div
class="
p-4
p-3
bg-white
border
rounded-md
@ -188,7 +195,7 @@
"
>
<div class="w-full grid grid-cols-2 gap-y-2 gap-x-3">
<div class="">
<div class="flex flex-col justify-end">
<div class="grid grid-cols-2 gap-2">
<FloatingLabelFloatInput
:df="{
@ -242,10 +249,11 @@
/>
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-full">
<div class="w-full">
<div class="w-full flex gap-2">
<Button
class="w-full bg-violet-500 dark:bg-violet-700 py-6"
class="w-full bg-violet-500 dark:bg-violet-700"
:class="`${isReturnInvoiceEnabledReturn ? 'py-5' : 'py-6'}`"
:disabled="!sinvDoc?.party || !sinvDoc?.items?.length"
@click="$emit('saveInvoiceAction')"
>
@ -255,21 +263,9 @@
</p>
</slot>
</Button>
<Button
class="w-full mt-4 bg-blue-500 dark:bg-blue-700 py-6"
@click="emitEvent('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"
class="w-full bg-red-500 dark:bg-red-700"
:class="`${isReturnInvoiceEnabledReturn ? 'py-5' : 'py-6'}`"
:disabled="!sinvDoc?.items?.length"
@click="() => $emit('clearValues')"
>
@ -279,19 +275,62 @@
</p>
</slot>
</Button>
</div>
<div
class="w-full flex gap-2"
:class="`${isReturnInvoiceEnabledReturn ? 'mt-2' : 'mt-4'}`"
>
<Button
class="w-full bg-blue-500 dark:bg-blue-700"
:class="`${isReturnInvoiceEnabledReturn ? 'py-5' : 'py-6'}`"
@click="emitEvent('toggleModal', 'SavedInvoice', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`held` }}
</p>
</slot>
</Button>
<Button
class="mt-4 w-full bg-green-500 dark:bg-green-700 py-6"
v-if="isReturnInvoiceEnabledReturn"
class="w-full bg-orange-500 dark:bg-orange-700 py-5"
@click="
emitEvent('toggleModal', 'ReturnSalesInvoice', true)
"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Return` }}
</p>
</slot>
</Button>
<Button
v-else
class="w-full bg-green-500 dark:bg-green-700"
:class="`${isReturnInvoiceEnabledReturn ? 'py-5' : 'py-6'}`"
:disabled="disablePayButton"
@click="emitEvent('toggleModal', 'Payment', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Buy` }}
{{ t`Pay` }}
</p>
</slot>
</Button>
</div>
<Button
v-if="isReturnInvoiceEnabledReturn"
class="w-full bg-green-500 mt-2 dark:bg-green-700 py-5"
:disabled="disablePayButton"
@click="emitEvent('toggleModal', 'Payment', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Pay` }}
</p>
</slot>
</Button>
</div>
</div>
</div>
@ -323,6 +362,7 @@ import LoyaltyProgramModal from './LoyaltyProgramModal.vue';
import { POSItem, ItemQtyMap } from 'src/components/POS/types';
import ItemsGrid from 'src/components/POS/Classic/ItemsGrid.vue';
import ItemsTable from 'src/components/POS/Classic/ItemsTable.vue';
import ReturnSalesInvoiceModal from './ReturnSalesInvoiceModal.vue';
import MultiLabelLink from 'src/components/Controls/MultiLabelLink.vue';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import SelectedItemTable from 'src/components/POS/Classic/SelectedItemTable.vue';
@ -352,6 +392,7 @@ export default defineComponent({
LoyaltyProgramModal,
WeightEnabledBarcode,
FloatingLabelFloatInput,
ReturnSalesInvoiceModal,
FloatingLabelCurrencyInput,
},
props: {
@ -368,6 +409,7 @@ export default defineComponent({
openSavedInvoiceModal: Boolean,
openLoyaltyProgramModal: Boolean,
openAppliedCouponsModal: Boolean,
openReturnSalesInvoiceModal: Boolean,
totalQuantity: {
type: Number,
default: 0,
@ -418,6 +460,7 @@ export default defineComponent({
'createTransaction',
'setTransferAmount',
'selectedInvoiceName',
'selectedReturnInvoice',
'setTransferClearanceDate',
],
data() {
@ -426,6 +469,10 @@ export default defineComponent({
itemSearchTerm: '',
};
},
computed: {
isReturnInvoiceEnabledReturn: () =>
fyo.singles.AccountingSettings?.enableInvoiceReturns ?? undefined,
},
methods: {
emitEvent(
eventName: PosEmits,

View File

@ -56,6 +56,13 @@
"
/>
<ReturnSalesInvoiceModal
:open-modal="openReturnSalesInvoiceModal"
:modal-status="openReturnSalesInvoiceModal"
@selected-return-invoice="(value:any) => emitEvent('selectedReturnInvoice', value)"
@toggle-modal="emitEvent('toggleModal', 'ReturnSalesInvoice')"
/>
<AlertModal
:open-modal="openAlertModal"
@toggle-modal="emitEvent('toggleModal', 'Alert')"
@ -185,7 +192,7 @@
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`held` }}
{{ t`Held` }}
</p>
</slot>
</Button>
@ -202,8 +209,19 @@
</p>
</slot>
</Button>
<Button
v-if="isReturnInvoiceEnabledReturn"
class="mt-2 w-full bg-orange-500 dark:bg-orange-700 py-5"
@click="emitEvent('toggleModal', 'ReturnSalesInvoice', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Return` }}
</p>
</slot>
</Button>
<Button
v-else
class="mt-2 w-full bg-green-500 dark:bg-green-700 py-5"
:disabled="disablePayButton"
@click="emitEvent('toggleModal', 'Payment', true)"
@ -216,6 +234,18 @@
</Button>
</div>
</div>
<Button
v-if="isReturnInvoiceEnabledReturn"
class="mt-2 w-full bg-green-500 dark:bg-green-700 py-5"
:disabled="disablePayButton"
@click="emitEvent('toggleModal', 'Payment', true)"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Buy` }}
</p>
</slot>
</Button>
</div>
</div>
</div>
@ -329,6 +359,7 @@ import SavedInvoiceModal from './SavedInvoiceModal.vue';
import Barcode from 'src/components/Controls/Barcode.vue';
import ClosePOSShiftModal from './ClosePOSShiftModal.vue';
import LoyaltyProgramModal from './LoyaltyProgramModal.vue';
import ReturnSalesInvoiceModal from './ReturnSalesInvoiceModal.vue';
import MultiLabelLink from 'src/components/Controls/MultiLabelLink.vue';
import { POSItem, PosEmits, ItemQtyMap } from 'src/components/POS/types';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
@ -362,6 +393,7 @@ export default defineComponent({
ModernPOSItemsTable,
WeightEnabledBarcode,
FloatingLabelFloatInput,
ReturnSalesInvoiceModal,
FloatingLabelCurrencyInput,
ModernPOSSelectedItemTable,
},
@ -380,6 +412,7 @@ export default defineComponent({
openSavedInvoiceModal: Boolean,
openLoyaltyProgramModal: Boolean,
openAppliedCouponsModal: Boolean,
openReturnSalesInvoiceModal: Boolean,
totalQuantity: {
type: Number,
default: 0,
@ -430,6 +463,7 @@ export default defineComponent({
'createTransaction',
'setTransferAmount',
'selectedInvoiceName',
'selectedReturnInvoice',
'setTransferClearanceDate',
],
data() {
@ -442,6 +476,10 @@ export default defineComponent({
itemSearchTerm: '',
};
},
computed: {
isReturnInvoiceEnabledReturn: () =>
fyo.singles.AccountingSettings?.enableInvoiceReturns ?? undefined,
},
methods: {
emitEvent(
eventName: PosEmits,

View File

@ -32,6 +32,7 @@
:open-saved-invoice-modal="openSavedInvoiceModal"
:open-loyalty-program-modal="openLoyaltyProgramModal"
:open-applied-coupons-modal="openAppliedCouponsModal"
:open-return-sales-invoice-modal="openReturnSalesInvoiceModal"
@add-item="addItem"
@toggle-view="toggleView"
@set-sinv-doc="setSinvDoc"
@ -49,6 +50,7 @@
@save-invoice-action="saveInvoiceAction"
@set-transfer-amount="setTransferAmount"
@selected-invoice-name="selectedInvoiceName"
@selected-return-invoice="selectedReturnInvoice"
@set-transfer-clearance-date="setTransferClearanceDate"
/>
<ModernPOS
@ -74,6 +76,7 @@
:open-saved-invoice-modal="openSavedInvoiceModal"
:open-loyalty-program-modal="openLoyaltyProgramModal"
:open-applied-coupons-modal="openAppliedCouponsModal"
:open-return-sales-invoice-modal="openReturnSalesInvoiceModal"
@add-item="addItem"
@toggle-view="toggleView"
@set-sinv-doc="setSinvDoc"
@ -91,6 +94,7 @@
@save-invoice-action="saveInvoiceAction"
@set-transfer-amount="setTransferAmount"
@selected-invoice-name="selectedInvoiceName"
@selected-return-invoice="selectedReturnInvoice"
@set-transfer-clearance-date="setTransferClearanceDate"
/>
</div>
@ -188,6 +192,7 @@ export default defineComponent({
openSavedInvoiceModal: false,
openLoyaltyProgramModal: false,
openAppliedCouponsModal: false,
openReturnSalesInvoiceModal: false,
totalQuantity: 0,
paidAmount: fyo.pesa(0),
@ -396,6 +401,20 @@ export default defineComponent({
});
}
},
async selectedReturnInvoice(invoiceName: string) {
const salesInvoiceDoc = (await this.fyo.doc.getDoc(
ModelNameEnum.SalesInvoice,
invoiceName
)) as SalesInvoice;
let returnDoc = (await salesInvoiceDoc.getReturnDoc()) as SalesInvoice;
if (!returnDoc || !returnDoc.name) {
return;
}
this.sinvDoc = returnDoc;
},
toggleView() {
this.tableView = !this.tableView;
},
@ -481,16 +500,23 @@ export default defineComponent({
setTransferRefNo(ref: string) {
this.transferRefNo = ref;
},
validateInvoice() {
if (this.sinvDoc.isSubmitted) {
throw new ValidationError(
t`Cannot add an item to a submitted invoice.`
);
}
if (this.sinvDoc.returnAgainst) {
throw new ValidationError(
t`Unable to add an item to the return invoice.`
);
}
},
async addItem(item: POSItem | Item | undefined, quantity?: number) {
try {
await this.sinvDoc.runFormulas();
if (this.sinvDoc.isSubmitted) {
throw new ValidationError(
t`Cannot add an item to a submitted invoice.`
);
}
this.validateInvoice();
if (!item) {
return;
@ -626,7 +652,7 @@ export default defineComponent({
if (paymentMethodDoc?.type !== 'Cash') {
await this.paymentDoc.setMultiple({
amount: this.paidAmount as Money,
amount: this.fyo.pesa(this.paidAmount as unknown as number).abs(),
referenceId: this.transferRefNo,
clearanceDate: this.transferClearanceDate,
});
@ -635,7 +661,7 @@ export default defineComponent({
if (paymentMethodDoc?.type === 'Cash') {
await this.paymentDoc.setMultiple({
paymentAccount: this.defaultPOSCashAccount,
amount: this.paidAmount as Money,
amount: this.fyo.pesa(this.paidAmount as unknown as number).abs(),
});
}

View File

@ -318,7 +318,8 @@ export default defineComponent({
if (
(this.sinvDoc.grandTotal?.float as number) < 1 &&
this.fyo.pesa(this.paidAmount.float).isZero()
this.fyo.pesa(this.paidAmount.float).isZero() &&
!this.sinvDoc.returnAgainst
) {
return true;
}
@ -334,7 +335,8 @@ export default defineComponent({
disablePayButton(): boolean {
if (
(this.sinvDoc.grandTotal?.float as number) < 1 &&
this.fyo.pesa(this.paidAmount.float).isZero()
this.fyo.pesa(this.paidAmount.float).isZero() &&
!this.sinvDoc.returnAgainst
) {
return true;
}
@ -345,6 +347,7 @@ export default defineComponent({
) {
return true;
}
return false;
},
},

View File

@ -0,0 +1,179 @@
<template>
<Modal class="h-auto w-auto p-5" :set-close-listener="false">
<p class="text-center font-semibold">{{ t`Invoices` }}</p>
<hr class="mt-2 dark:border-gray-800" />
<Row
:ratio="ratio"
class="
border
flex
items-center
mt-2
px-2
w-full
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 px-2 py-2 text-lg"
>
{{ df.label }}
</div>
</Row>
<div
class="overflow-y-auto custom-scroll custom-scroll-thumb2"
style="height: 65vh; width: 60vh"
>
<Row
v-for="row in returnedInvoices"
:key="row.name"
:ratio="ratio"
:border="true"
class="
border-b border-l border-r
dark:border-gray-800 dark:bg-gray-890
flex
group
h-row-mid
hover:bg-gray-25
items-center
justify-center
px-2
w-full
"
@click="returnInvoice(row as SalesInvoice)"
>
<FormControl
v-for="df in tableFields"
:key="df.fieldname"
size="large"
:df="df"
:value="row[df.fieldname]"
:read-only="true"
/>
</Row>
</div>
<div class="row-start-6 grid grid-cols-2 gap-4 mt-4">
<div class="col-span-2">
<Button
class="w-full p-5 bg-red-500 dark:bg-red-700"
@click="$emit('toggleModal', 'SavedInvoice')"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Cancel` }}
</p>
</slot>
</Button>
</div>
</div>
</Modal>
</template>
<script lang="ts">
import Button from 'src/components/Button.vue';
import Modal from 'src/components/Modal.vue';
import Row from 'src/components/Row.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { defineComponent, inject } from 'vue';
import { ModelNameEnum } from 'models/types';
import { Field } from 'schemas/types';
export default defineComponent({
name: 'ReturnSalesInvoice',
components: {
Modal,
Button,
FormControl,
Row,
},
props: {
modalStatus: Boolean,
},
emits: ['toggleModal', 'selectedReturnInvoice'],
setup() {
return {
sinvDoc: inject('sinvDoc') as SalesInvoice,
};
},
data() {
return {
returnedInvoices: [] as SalesInvoice[],
};
},
computed: {
ratio() {
return [1, 1, 1, 0.8];
},
tableFields() {
return [
{
fieldname: 'name',
label: 'Name',
fieldtype: 'Link',
target: 'SalesInvoice',
readOnly: true,
},
{
fieldname: 'party',
fieldtype: 'Link',
label: 'Customer',
target: 'Party',
placeholder: 'Customer',
readOnly: true,
},
{
fieldname: 'date',
label: 'Date',
fieldtype: 'Date',
readOnly: true,
},
{
fieldname: 'grandTotal',
label: 'Grand Total',
fieldtype: 'Currency',
readOnly: true,
},
] as Field[];
},
},
watch: {
async modalStatus(newVal) {
if (newVal) {
await this.setReturnedInvoices();
}
},
},
async mounted() {
await this.setReturnedInvoices();
},
async activated() {
await this.setReturnedInvoices();
},
methods: {
returnInvoice(row: SalesInvoice) {
this.$emit('selectedReturnInvoice', row.name);
this.$emit('toggleModal', 'ReturnSalesInvoice');
},
async setReturnedInvoices() {
this.returnedInvoices = (await this.fyo.db.getAll(
ModelNameEnum.SalesInvoice,
{
fields: [],
filters: { isPOS: true, submitted: true, returnAgainst: null },
}
)) as SalesInvoice[];
},
},
});
</script>

View File

@ -129,7 +129,6 @@ export default defineComponent({
savedInvoiceList: true,
savedInvoices: [] as SalesInvoice[],
submittedInvoices: [] as SalesInvoice[],
isModalVisible: false,
};
},
computed: {

View File

@ -81,15 +81,20 @@ export function validateSinv(sinvDoc: SalesInvoice, itemQtyMap: ItemQtyMap) {
return;
}
validateSinvItems(sinvDoc.items as SalesInvoiceItem[], itemQtyMap);
validateSinvItems(
sinvDoc.items as SalesInvoiceItem[],
itemQtyMap,
sinvDoc.returnAgainst as string
);
}
function validateSinvItems(
sinvItems: SalesInvoiceItem[],
itemQtyMap: ItemQtyMap
itemQtyMap: ItemQtyMap,
isReturn?: string
) {
for (const item of sinvItems) {
if (!item.quantity || item.quantity < 1) {
if (!item.quantity || (item.quantity < 1 && !isReturn)) {
throw new ValidationError(
t`Invalid Quantity for Item ${item.item as string}`
);