2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 06:44:06 +00:00

feat: stock return

This commit is contained in:
akshayitzme 2023-07-01 13:01:00 +05:30
parent c5cc916127
commit 3577a8fab0
11 changed files with 264 additions and 10 deletions

View File

@ -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;
}
}

View File

@ -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
*/

View File

@ -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) {

View File

@ -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';
}

View File

@ -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`
);

View File

@ -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) {

View File

@ -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: {

View File

@ -37,6 +37,7 @@ export interface SMTransferDetails {
serialNumber?: string;
fromLocation?: string;
toLocation?: string;
isReturn?: boolean;
}
export interface SMIDetails extends SMDetails, SMTransferDetails {}

View File

@ -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',

View File

@ -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,

View File

@ -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';
}