2
0
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:
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 { ModelNameEnum } from '../../models/types';
import DatabaseCore from './core'; import DatabaseCore from './core';
import { BespokeFunction } from './types'; import { BespokeFunction } from './types';
import { safeParseFloat } from 'utils/index';
import { DocValueMap } from 'fyo/core/types';
export class BespokeQueries { export class BespokeQueries {
[key: string]: BespokeFunction; [key: string]: BespokeFunction;
@ -180,4 +182,65 @@ export class BespokeQueries {
return value[0][Object.keys(value[0])[0]]; 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; )) 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 * Internal methods
*/ */

View File

@ -15,6 +15,7 @@ export class AccountingSettings extends Doc {
enableDiscounting?: boolean; enableDiscounting?: boolean;
enableInventory?: boolean; enableInventory?: boolean;
enablePriceList?: boolean; enablePriceList?: boolean;
enableStockReturns?: boolean;
static filters: FiltersMap = { static filters: FiltersMap = {
writeOffAccount: () => ({ writeOffAccount: () => ({
@ -46,11 +47,15 @@ export class AccountingSettings extends Doc {
enableInventory: () => { enableInventory: () => {
return !!this.enableInventory; return !!this.enableInventory;
}, },
enableStockReturns: () => {
return !!this.enableStockReturns;
},
}; };
override hidden: HiddenMap = { override hidden: HiddenMap = {
discountAccount: () => !this.enableDiscounting, discountAccount: () => !this.enableDiscounting,
gstin: () => this.fyo.singles.SystemSettings?.countryCode !== 'in', gstin: () => this.fyo.singles.SystemSettings?.countryCode !== 'in',
enableStockReturns: () => !this.enableInventory,
}; };
async change(ch: ChangeArg) { async change(ch: ChangeArg) {

View File

@ -34,6 +34,7 @@ export function getStockTransferActions(
getMakeInvoiceAction(fyo, schemaName), getMakeInvoiceAction(fyo, schemaName),
getLedgerLinkAction(fyo, false), getLedgerLinkAction(fyo, false),
getLedgerLinkAction(fyo, true), 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 { export function getTransactionStatusColumn(): ColumnConfig {
return { return {
@ -190,6 +212,8 @@ export const statusColor: Record<
NotSaved: 'gray', NotSaved: 'gray',
Submitted: 'green', Submitted: 'green',
Cancelled: 'red', Cancelled: 'red',
Return: 'orange',
ReturnIssued: 'gray',
}; };
export function getStatusText(status: DocStatus | InvoiceStatus): string { export function getStatusText(status: DocStatus | InvoiceStatus): string {
@ -208,6 +232,10 @@ export function getStatusText(status: DocStatus | InvoiceStatus): string {
return t`Paid`; return t`Paid`;
case 'Unpaid': case 'Unpaid':
return t`Unpaid`; return t`Unpaid`;
case 'Return':
return t`Return`;
case 'ReturnIssued':
return t`Return Issued`;
default: default:
return ''; return '';
} }
@ -244,6 +272,20 @@ function getSubmittableDocStatus(doc: RenderData | Doc) {
return getInvoiceStatus(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) { if (!!doc.submitted && !doc.cancelled) {
return 'Submitted'; return 'Submitted';
} }

View File

@ -56,12 +56,13 @@ export class StockManager {
async validateCancel(transferDetails: SMTransferDetails[]) { async validateCancel(transferDetails: SMTransferDetails[]) {
const reverseTransferDetails = transferDetails.map( const reverseTransferDetails = transferDetails.map(
({ item, rate, quantity, fromLocation, toLocation }) => ({ ({ item, rate, quantity, fromLocation, toLocation, isReturn }) => ({
item, item,
rate, rate,
quantity, quantity,
fromLocation: toLocation, fromLocation: toLocation,
toLocation: fromLocation, toLocation: fromLocation,
isReturn,
}) })
); );
await this.validateTransfers(reverseTransferDetails); await this.validateTransfers(reverseTransferDetails);
@ -94,8 +95,7 @@ export class StockManager {
if (!details.quantity) { if (!details.quantity) {
throw new ValidationError(t`Quantity needs to be set`); throw new ValidationError(t`Quantity needs to be set`);
} }
if (!details.isReturn && details.quantity <= 0) {
if (details.quantity <= 0) {
throw new ValidationError( throw new ValidationError(
t`Quantity (${details.quantity}) has to be greater than zero` t`Quantity (${details.quantity}) has to be greater than zero`
); );

View File

@ -26,6 +26,7 @@ import {
validateBatch, validateBatch,
validateSerialNumber, validateSerialNumber,
} from './helpers'; } from './helpers';
import { safeParseFloat } from 'utils/index';
export abstract class StockTransfer extends Transfer { export abstract class StockTransfer extends Transfer {
name?: string; name?: string;
@ -36,6 +37,9 @@ export abstract class StockTransfer extends Transfer {
grandTotal?: Money; grandTotal?: Money;
backReference?: string; backReference?: string;
items?: StockTransferItem[]; items?: StockTransferItem[];
isReturn?: boolean;
returnAgainst?: string;
isItemsReturned?: boolean;
get isSales() { get isSales() {
return this.schemaName === ModelNameEnum.Shipment; return this.schemaName === ModelNameEnum.Shipment;
@ -61,6 +65,8 @@ export abstract class StockTransfer extends Transfer {
terms: () => !(this.terms || !(this.isSubmitted || this.isCancelled)), terms: () => !(this.terms || !(this.isSubmitted || this.isCancelled)),
attachment: () => attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)), !(this.attachment || !(this.isSubmitted || this.isCancelled)),
isReturn: () => !this.fyo.singles.AccountingSettings?.enableStockReturns,
returnAgainst: () => !this.isReturn,
}; };
static defaults: DefaultMap = { static defaults: DefaultMap = {
@ -86,6 +92,11 @@ export abstract class StockTransfer extends Transfer {
submitted: true, submitted: true,
cancelled: false, cancelled: false,
}), }),
returnAgainst: () => ({
isReturn: false,
submitted: true,
cancelled: false,
}),
}; };
override _getTransferDetails() { override _getTransferDetails() {
@ -107,6 +118,7 @@ export abstract class StockTransfer extends Transfer {
serialNumber: row.serialNumber!, serialNumber: row.serialNumber!,
fromLocation, fromLocation,
toLocation, toLocation,
isReturn: this.isReturn,
}; };
}); });
} }
@ -127,17 +139,27 @@ export abstract class StockTransfer extends Transfer {
'costOfGoodsSold' 'costOfGoodsSold'
)) as string; )) as string;
if (this.isReturn) {
await posting.debit(stockInHand, amount);
await posting.credit(costOfGoodsSold, amount);
} else {
await posting.debit(costOfGoodsSold, amount); await posting.debit(costOfGoodsSold, amount);
await posting.credit(stockInHand, amount); await posting.credit(stockInHand, amount);
}
} else { } else {
const stockReceivedButNotBilled = (await this.fyo.getValue( const stockReceivedButNotBilled = (await this.fyo.getValue(
ModelNameEnum.InventorySettings, ModelNameEnum.InventorySettings,
'stockReceivedButNotBilled' 'stockReceivedButNotBilled'
)) as string; )) as string;
if (this.isReturn) {
await posting.debit(stockReceivedButNotBilled, amount);
await posting.credit(stockInHand, amount);
} else {
await posting.debit(stockInHand, amount); await posting.debit(stockInHand, amount);
await posting.credit(stockReceivedButNotBilled, amount); await posting.credit(stockReceivedButNotBilled, amount);
} }
}
await posting.makeRoundOffEntry(); await posting.makeRoundOffEntry();
return posting; return posting;
@ -184,12 +206,14 @@ export abstract class StockTransfer extends Transfer {
await super.afterSubmit(); await super.afterSubmit();
await updateSerialNumbers(this, false); await updateSerialNumbers(this, false);
await this._updateBackReference(); await this._updateBackReference();
await this._updateItemsReturned();
} }
async afterCancel(): Promise<void> { async afterCancel(): Promise<void> {
await super.afterCancel(); await super.afterCancel();
await updateSerialNumbers(this, true); await updateSerialNumbers(this, true);
await this._updateBackReference(); await this._updateBackReference();
await this._updateItemsReturned();
} }
async _updateBackReference() { async _updateBackReference() {
@ -246,6 +270,30 @@ export abstract class StockTransfer extends Transfer {
await invoice.setAndSync('stockNotTransferred', notTransferred); 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() { _getTransferMap() {
return (this.items ?? []).reduce((acc, item) => { return (this.items ?? []).reduce((acc, item) => {
if (!item.item) { if (!item.item) {
@ -373,6 +421,64 @@ export abstract class StockTransfer extends Transfer {
return invoice; 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) { async function validateSerialNumberStatus(doc: StockTransfer) {

View File

@ -38,6 +38,10 @@ export class StockTransferItem extends TransferItem {
return this.schemaName === ModelNameEnum.ShipmentItem; return this.schemaName === ModelNameEnum.ShipmentItem;
} }
get isReturn() {
return !!this.parentdoc?.isReturn;
}
formulas: FormulaMap = { formulas: FormulaMap = {
description: { description: {
formula: async () => formula: async () =>
@ -94,6 +98,15 @@ export class StockTransferItem extends TransferItem {
const unitDoc = itemDoc.getLink('uom'); const unitDoc = itemDoc.getLink('uom');
let quantity: number = this.quantity ?? 1; let quantity: number = this.quantity ?? 1;
if (this.isReturn && quantity > 0) {
quantity *= -1;
}
if (!this.isReturn && quantity < 0) {
quantity *= -1;
}
if (fieldname === 'transferQuantity') { if (fieldname === 'transferQuantity') {
quantity = this.transferQuantity! * this.unitConversionFactor!; quantity = this.transferQuantity! * this.unitConversionFactor!;
} }
@ -109,6 +122,7 @@ export class StockTransferItem extends TransferItem {
'transferQuantity', 'transferQuantity',
'transferUnit', 'transferUnit',
'unitConversionFactor', 'unitConversionFactor',
'isReturn',
], ],
}, },
unitConversionFactor: { unitConversionFactor: {

View File

@ -37,6 +37,7 @@ export interface SMTransferDetails {
serialNumber?: string; serialNumber?: string;
fromLocation?: string; fromLocation?: string;
toLocation?: string; toLocation?: string;
isReturn?: boolean;
} }
export interface SMIDetails extends SMDetails, SMTransferDetails {} 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 { export enum ModelNameEnum {
Account = 'Account', Account = 'Account',
AccountingLedgerEntry = 'AccountingLedgerEntry', AccountingLedgerEntry = 'AccountingLedgerEntry',

View File

@ -105,8 +105,8 @@ export abstract class LedgerReport extends Report {
name: safeParseInt(entry.name), name: safeParseInt(entry.name),
account: entry.account, account: entry.account,
date: new Date(entry.date), date: new Date(entry.date),
debit: safeParseFloat(entry.debit), debit: Math.abs(safeParseFloat(entry.debit)),
credit: safeParseFloat(entry.credit), credit: Math.abs(safeParseFloat(entry.credit)),
balance: 0, balance: 0,
referenceType: entry.referenceType, referenceType: entry.referenceType,
referenceName: entry.referenceName, referenceName: entry.referenceName,

View File

@ -43,6 +43,8 @@ export default defineComponent({
Paid: this.t`Paid`, Paid: this.t`Paid`,
Saved: this.t`Saved`, Saved: this.t`Saved`,
Submitted: this.t`Submitted`, Submitted: this.t`Submitted`,
Return: this.t`Return`,
ReturnIssued: this.t`Return Issued`
}[this.status]; }[this.status];
}, },
color(): UIColors { color(): UIColors {
@ -61,6 +63,8 @@ const statusColorMap: Record<Status, UIColors> = {
Paid: 'green', Paid: 'green',
Saved: 'blue', Saved: 'blue',
Submitted: 'blue', Submitted: 'blue',
Return: 'orange',
ReturnIssued: 'gray'
}; };
function getStatus(doc: Doc) { function getStatus(doc: Doc) {
@ -109,6 +113,14 @@ function getSubmittableStatus(doc: Doc) {
return 'Paid'; return 'Paid';
} }
if(doc.isReturn && doc.isSubmitted && !doc.isCancelled){
return 'Return'
}
if(doc.isItemsReturned && doc.isSubmitted && !doc.isCancelled){
return 'ReturnIssued'
}
if (doc.isSubmitted) { if (doc.isSubmitted) {
return 'Submitted'; return 'Submitted';
} }