mirror of
https://github.com/frappe/books.git
synced 2024-11-08 14:50:56 +00:00
feat: stock return
This commit is contained in:
parent
c5cc916127
commit
3577a8fab0
@ -8,6 +8,8 @@ import {
|
||||
import { ModelNameEnum } from '../../models/types';
|
||||
import DatabaseCore from './core';
|
||||
import { BespokeFunction } from './types';
|
||||
import { safeParseFloat } from 'utils/index';
|
||||
import { DocValueMap } from 'fyo/core/types';
|
||||
|
||||
export class BespokeQueries {
|
||||
[key: string]: BespokeFunction;
|
||||
@ -180,4 +182,65 @@ export class BespokeQueries {
|
||||
|
||||
return value[0][Object.keys(value[0])[0]];
|
||||
}
|
||||
|
||||
static async getReturnBalanceItemsQty(
|
||||
db: DatabaseCore,
|
||||
schemaName: string,
|
||||
docName: string
|
||||
): Promise<DocValueMap[] | undefined> {
|
||||
const docItems = (await db.knex!(`${schemaName}Item`)
|
||||
.select('item')
|
||||
.where('parent', docName)
|
||||
.groupBy('item')
|
||||
.sum({ quantity: 'quantity' })) as DocValueMap[];
|
||||
|
||||
const returnDocNames = (
|
||||
await db.knex!(schemaName)
|
||||
.select('name')
|
||||
.where('returnAgainst', docName)
|
||||
.andWhere('submitted', true)
|
||||
.andWhere('cancelled', false)
|
||||
).map((i: { name: string }) => i.name);
|
||||
|
||||
if (!returnDocNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const returnedItems = (await db.knex!(`${schemaName}Item`)
|
||||
.select('item')
|
||||
.sum({ quantity: 'quantity' })
|
||||
.whereIn('parent', returnDocNames)
|
||||
.groupBy('item')) as DocValueMap[];
|
||||
|
||||
if (!returnedItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const returnBalanceItemQty = [];
|
||||
|
||||
for (const item of returnedItems) {
|
||||
const docItem = docItems.filter(
|
||||
(invItem) => invItem.item === item.item
|
||||
)[0];
|
||||
|
||||
if (!docItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let balanceQty = safeParseFloat(
|
||||
(docItem.quantity as number) - Math.abs(item.quantity as number)
|
||||
);
|
||||
|
||||
if (balanceQty === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (balanceQty > 0) {
|
||||
balanceQty *= -1;
|
||||
}
|
||||
returnBalanceItemQty.push({ ...item, quantity: balanceQty });
|
||||
}
|
||||
|
||||
return returnBalanceItemQty;
|
||||
}
|
||||
}
|
||||
|
@ -330,6 +330,17 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
)) as number | null;
|
||||
}
|
||||
|
||||
async getReturnBalanceItemsQty(
|
||||
schemaName: string,
|
||||
docName: string
|
||||
): Promise<DocValueMap[] | undefined> {
|
||||
return (await this.#demux.callBespoke(
|
||||
'getReturnBalanceItemsQty',
|
||||
schemaName,
|
||||
docName
|
||||
)) as DocValueMap[] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal methods
|
||||
*/
|
||||
|
@ -15,6 +15,7 @@ export class AccountingSettings extends Doc {
|
||||
enableDiscounting?: boolean;
|
||||
enableInventory?: boolean;
|
||||
enablePriceList?: boolean;
|
||||
enableStockReturns?: boolean;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
writeOffAccount: () => ({
|
||||
@ -46,11 +47,15 @@ export class AccountingSettings extends Doc {
|
||||
enableInventory: () => {
|
||||
return !!this.enableInventory;
|
||||
},
|
||||
enableStockReturns: () => {
|
||||
return !!this.enableStockReturns;
|
||||
},
|
||||
};
|
||||
|
||||
override hidden: HiddenMap = {
|
||||
discountAccount: () => !this.enableDiscounting,
|
||||
gstin: () => this.fyo.singles.SystemSettings?.countryCode !== 'in',
|
||||
enableStockReturns: () => !this.enableInventory,
|
||||
};
|
||||
|
||||
async change(ch: ChangeArg) {
|
||||
|
@ -34,6 +34,7 @@ export function getStockTransferActions(
|
||||
getMakeInvoiceAction(fyo, schemaName),
|
||||
getLedgerLinkAction(fyo, false),
|
||||
getLedgerLinkAction(fyo, true),
|
||||
getMakeReturnDocAction(fyo),
|
||||
];
|
||||
}
|
||||
|
||||
@ -160,6 +161,27 @@ export function getLedgerLink(
|
||||
},
|
||||
};
|
||||
}
|
||||
export function getMakeReturnDocAction(fyo: Fyo): Action {
|
||||
return {
|
||||
label: fyo.t`Return`,
|
||||
group: fyo.t`Create`,
|
||||
condition: (doc: Doc) =>
|
||||
!!fyo.singles.AccountingSettings?.enableStockReturns &&
|
||||
doc.isSubmitted &&
|
||||
!doc.isCancelled &&
|
||||
!doc.isReturn,
|
||||
action: async (doc: Doc) => {
|
||||
const returnDoc = await (doc as StockTransfer).getReturnDoc();
|
||||
if (!returnDoc || !returnDoc.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { routeTo } = await import('src/utils/ui');
|
||||
const path = `/edit/${doc.schemaName}/${returnDoc.name}`;
|
||||
await routeTo(path);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getTransactionStatusColumn(): ColumnConfig {
|
||||
return {
|
||||
@ -190,6 +212,8 @@ export const statusColor: Record<
|
||||
NotSaved: 'gray',
|
||||
Submitted: 'green',
|
||||
Cancelled: 'red',
|
||||
Return: 'orange',
|
||||
ReturnIssued: 'gray',
|
||||
};
|
||||
|
||||
export function getStatusText(status: DocStatus | InvoiceStatus): string {
|
||||
@ -208,6 +232,10 @@ export function getStatusText(status: DocStatus | InvoiceStatus): string {
|
||||
return t`Paid`;
|
||||
case 'Unpaid':
|
||||
return t`Unpaid`;
|
||||
case 'Return':
|
||||
return t`Return`;
|
||||
case 'ReturnIssued':
|
||||
return t`Return Issued`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@ -244,6 +272,20 @@ function getSubmittableDocStatus(doc: RenderData | Doc) {
|
||||
return getInvoiceStatus(doc);
|
||||
}
|
||||
|
||||
if (
|
||||
[ModelNameEnum.Shipment, ModelNameEnum.PurchaseReceipt].includes(
|
||||
doc.schema.name as ModelNameEnum
|
||||
)
|
||||
) {
|
||||
if (doc.isReturn && doc.submitted && !doc.cancelled) {
|
||||
return 'Return';
|
||||
}
|
||||
|
||||
if (doc.isItemsReturned && doc.submitted && !doc.cancelled) {
|
||||
return 'ReturnIssued';
|
||||
}
|
||||
}
|
||||
|
||||
if (!!doc.submitted && !doc.cancelled) {
|
||||
return 'Submitted';
|
||||
}
|
||||
|
@ -56,12 +56,13 @@ export class StockManager {
|
||||
|
||||
async validateCancel(transferDetails: SMTransferDetails[]) {
|
||||
const reverseTransferDetails = transferDetails.map(
|
||||
({ item, rate, quantity, fromLocation, toLocation }) => ({
|
||||
({ item, rate, quantity, fromLocation, toLocation, isReturn }) => ({
|
||||
item,
|
||||
rate,
|
||||
quantity,
|
||||
fromLocation: toLocation,
|
||||
toLocation: fromLocation,
|
||||
isReturn,
|
||||
})
|
||||
);
|
||||
await this.validateTransfers(reverseTransferDetails);
|
||||
@ -94,8 +95,7 @@ export class StockManager {
|
||||
if (!details.quantity) {
|
||||
throw new ValidationError(t`Quantity needs to be set`);
|
||||
}
|
||||
|
||||
if (details.quantity <= 0) {
|
||||
if (!details.isReturn && details.quantity <= 0) {
|
||||
throw new ValidationError(
|
||||
t`Quantity (${details.quantity}) has to be greater than zero`
|
||||
);
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
validateBatch,
|
||||
validateSerialNumber,
|
||||
} from './helpers';
|
||||
import { safeParseFloat } from 'utils/index';
|
||||
|
||||
export abstract class StockTransfer extends Transfer {
|
||||
name?: string;
|
||||
@ -36,6 +37,9 @@ export abstract class StockTransfer extends Transfer {
|
||||
grandTotal?: Money;
|
||||
backReference?: string;
|
||||
items?: StockTransferItem[];
|
||||
isReturn?: boolean;
|
||||
returnAgainst?: string;
|
||||
isItemsReturned?: boolean;
|
||||
|
||||
get isSales() {
|
||||
return this.schemaName === ModelNameEnum.Shipment;
|
||||
@ -61,6 +65,8 @@ export abstract class StockTransfer extends Transfer {
|
||||
terms: () => !(this.terms || !(this.isSubmitted || this.isCancelled)),
|
||||
attachment: () =>
|
||||
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
|
||||
isReturn: () => !this.fyo.singles.AccountingSettings?.enableStockReturns,
|
||||
returnAgainst: () => !this.isReturn,
|
||||
};
|
||||
|
||||
static defaults: DefaultMap = {
|
||||
@ -86,6 +92,11 @@ export abstract class StockTransfer extends Transfer {
|
||||
submitted: true,
|
||||
cancelled: false,
|
||||
}),
|
||||
returnAgainst: () => ({
|
||||
isReturn: false,
|
||||
submitted: true,
|
||||
cancelled: false,
|
||||
}),
|
||||
};
|
||||
|
||||
override _getTransferDetails() {
|
||||
@ -107,6 +118,7 @@ export abstract class StockTransfer extends Transfer {
|
||||
serialNumber: row.serialNumber!,
|
||||
fromLocation,
|
||||
toLocation,
|
||||
isReturn: this.isReturn,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -127,16 +139,26 @@ export abstract class StockTransfer extends Transfer {
|
||||
'costOfGoodsSold'
|
||||
)) as string;
|
||||
|
||||
await posting.debit(costOfGoodsSold, amount);
|
||||
await posting.credit(stockInHand, amount);
|
||||
if (this.isReturn) {
|
||||
await posting.debit(stockInHand, amount);
|
||||
await posting.credit(costOfGoodsSold, amount);
|
||||
} else {
|
||||
await posting.debit(costOfGoodsSold, amount);
|
||||
await posting.credit(stockInHand, amount);
|
||||
}
|
||||
} else {
|
||||
const stockReceivedButNotBilled = (await this.fyo.getValue(
|
||||
ModelNameEnum.InventorySettings,
|
||||
'stockReceivedButNotBilled'
|
||||
)) as string;
|
||||
|
||||
await posting.debit(stockInHand, amount);
|
||||
await posting.credit(stockReceivedButNotBilled, amount);
|
||||
if (this.isReturn) {
|
||||
await posting.debit(stockReceivedButNotBilled, amount);
|
||||
await posting.credit(stockInHand, amount);
|
||||
} else {
|
||||
await posting.debit(stockInHand, amount);
|
||||
await posting.credit(stockReceivedButNotBilled, amount);
|
||||
}
|
||||
}
|
||||
|
||||
await posting.makeRoundOffEntry();
|
||||
@ -184,12 +206,14 @@ export abstract class StockTransfer extends Transfer {
|
||||
await super.afterSubmit();
|
||||
await updateSerialNumbers(this, false);
|
||||
await this._updateBackReference();
|
||||
await this._updateItemsReturned();
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
await super.afterCancel();
|
||||
await updateSerialNumbers(this, true);
|
||||
await this._updateBackReference();
|
||||
await this._updateItemsReturned();
|
||||
}
|
||||
|
||||
async _updateBackReference() {
|
||||
@ -246,6 +270,30 @@ export abstract class StockTransfer extends Transfer {
|
||||
await invoice.setAndSync('stockNotTransferred', notTransferred);
|
||||
}
|
||||
|
||||
async _updateItemsReturned() {
|
||||
if (!this.returnAgainst) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const returnDocs = await this.fyo.db.getAll(this.schema.name, {
|
||||
filters: {
|
||||
returnAgainst: this.returnAgainst,
|
||||
submitted: true,
|
||||
cancelled: false,
|
||||
},
|
||||
});
|
||||
|
||||
const isItemsReturned = returnDocs.length;
|
||||
|
||||
const referenceDoc = await this.fyo.doc.getDoc(
|
||||
this.schema.name,
|
||||
this.returnAgainst
|
||||
);
|
||||
|
||||
await referenceDoc.setAndSync({ isItemsReturned });
|
||||
await referenceDoc.submit();
|
||||
}
|
||||
|
||||
_getTransferMap() {
|
||||
return (this.items ?? []).reduce((acc, item) => {
|
||||
if (!item.item) {
|
||||
@ -373,6 +421,64 @@ export abstract class StockTransfer extends Transfer {
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
async getReturnDoc(): Promise<StockTransfer | null> {
|
||||
if (!this.items?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const docData = this.getValidDict(true, true);
|
||||
const docItems = docData.items as StockTransferItem[];
|
||||
const returnDocItems: StockTransferItem[] = [];
|
||||
|
||||
const returnBalanceItemsQty = await this.fyo.db.getReturnBalanceItemsQty(
|
||||
this.schema.name,
|
||||
this.name!
|
||||
);
|
||||
|
||||
for (const item of docItems) {
|
||||
if (!item.quantity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let quantity = -1 * item.quantity;
|
||||
|
||||
if (returnBalanceItemsQty) {
|
||||
const balanceItemQty = returnBalanceItemsQty.filter(
|
||||
(i) => i.item === item.item
|
||||
)[0];
|
||||
|
||||
if (!balanceItemQty) {
|
||||
continue;
|
||||
}
|
||||
quantity = balanceItemQty.quantity as number;
|
||||
}
|
||||
|
||||
item.quantity = safeParseFloat(quantity);
|
||||
delete item.name;
|
||||
returnDocItems.push(item);
|
||||
}
|
||||
|
||||
const returnDocData = {
|
||||
...docData,
|
||||
name: null,
|
||||
date: new Date(),
|
||||
items: returnDocItems,
|
||||
isReturn: true,
|
||||
returnAgainst: docData.name,
|
||||
grandTotal: this.fyo.pesa(0),
|
||||
};
|
||||
|
||||
const rawReturnDoc = this.fyo.doc.getNewDoc(
|
||||
this.schema.name,
|
||||
returnDocData
|
||||
) as StockTransfer;
|
||||
|
||||
rawReturnDoc.once('beforeSync', async () => {
|
||||
await rawReturnDoc.runFormulas();
|
||||
});
|
||||
return rawReturnDoc;
|
||||
}
|
||||
}
|
||||
|
||||
async function validateSerialNumberStatus(doc: StockTransfer) {
|
||||
|
@ -38,6 +38,10 @@ export class StockTransferItem extends TransferItem {
|
||||
return this.schemaName === ModelNameEnum.ShipmentItem;
|
||||
}
|
||||
|
||||
get isReturn() {
|
||||
return !!this.parentdoc?.isReturn;
|
||||
}
|
||||
|
||||
formulas: FormulaMap = {
|
||||
description: {
|
||||
formula: async () =>
|
||||
@ -94,6 +98,15 @@ export class StockTransferItem extends TransferItem {
|
||||
const unitDoc = itemDoc.getLink('uom');
|
||||
|
||||
let quantity: number = this.quantity ?? 1;
|
||||
|
||||
if (this.isReturn && quantity > 0) {
|
||||
quantity *= -1;
|
||||
}
|
||||
|
||||
if (!this.isReturn && quantity < 0) {
|
||||
quantity *= -1;
|
||||
}
|
||||
|
||||
if (fieldname === 'transferQuantity') {
|
||||
quantity = this.transferQuantity! * this.unitConversionFactor!;
|
||||
}
|
||||
@ -109,6 +122,7 @@ export class StockTransferItem extends TransferItem {
|
||||
'transferQuantity',
|
||||
'transferUnit',
|
||||
'unitConversionFactor',
|
||||
'isReturn',
|
||||
],
|
||||
},
|
||||
unitConversionFactor: {
|
||||
|
@ -37,6 +37,7 @@ export interface SMTransferDetails {
|
||||
serialNumber?: string;
|
||||
fromLocation?: string;
|
||||
toLocation?: string;
|
||||
isReturn?: boolean;
|
||||
}
|
||||
|
||||
export interface SMIDetails extends SMDetails, SMTransferDetails {}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export type InvoiceStatus = 'Draft' | 'Saved' | 'Unpaid' | 'Cancelled' | 'Paid';
|
||||
export type InvoiceStatus = 'Draft' | 'Saved' | 'Unpaid' | 'Cancelled' | 'Paid' | 'Return' | 'ReturnIssued';
|
||||
export enum ModelNameEnum {
|
||||
Account = 'Account',
|
||||
AccountingLedgerEntry = 'AccountingLedgerEntry',
|
||||
|
@ -105,8 +105,8 @@ export abstract class LedgerReport extends Report {
|
||||
name: safeParseInt(entry.name),
|
||||
account: entry.account,
|
||||
date: new Date(entry.date),
|
||||
debit: safeParseFloat(entry.debit),
|
||||
credit: safeParseFloat(entry.credit),
|
||||
debit: Math.abs(safeParseFloat(entry.debit)),
|
||||
credit: Math.abs(safeParseFloat(entry.credit)),
|
||||
balance: 0,
|
||||
referenceType: entry.referenceType,
|
||||
referenceName: entry.referenceName,
|
||||
|
@ -43,6 +43,8 @@ export default defineComponent({
|
||||
Paid: this.t`Paid`,
|
||||
Saved: this.t`Saved`,
|
||||
Submitted: this.t`Submitted`,
|
||||
Return: this.t`Return`,
|
||||
ReturnIssued: this.t`Return Issued`
|
||||
}[this.status];
|
||||
},
|
||||
color(): UIColors {
|
||||
@ -61,6 +63,8 @@ const statusColorMap: Record<Status, UIColors> = {
|
||||
Paid: 'green',
|
||||
Saved: 'blue',
|
||||
Submitted: 'blue',
|
||||
Return: 'orange',
|
||||
ReturnIssued: 'gray'
|
||||
};
|
||||
|
||||
function getStatus(doc: Doc) {
|
||||
@ -109,6 +113,14 @@ function getSubmittableStatus(doc: Doc) {
|
||||
return 'Paid';
|
||||
}
|
||||
|
||||
if(doc.isReturn && doc.isSubmitted && !doc.isCancelled){
|
||||
return 'Return'
|
||||
}
|
||||
|
||||
if(doc.isItemsReturned && doc.isSubmitted && !doc.isCancelled){
|
||||
return 'ReturnIssued'
|
||||
}
|
||||
|
||||
if (doc.isSubmitted) {
|
||||
return 'Submitted';
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user