2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 14:48:25 +00:00

test: invoice linked stock transfers

- update invoice cancel and delete
This commit is contained in:
18alantom 2022-11-23 13:47:23 +05:30
parent ab2d2f8975
commit a3f96a90e1
3 changed files with 324 additions and 9 deletions

View File

@ -18,7 +18,6 @@ import {
} from 'models/helpers';
import { InventorySettings } from 'models/inventory/InventorySettings';
import { StockTransfer } from 'models/inventory/StockTransfer';
import { getStockTransfer } from 'models/inventory/tests/helpers';
import { Transactional } from 'models/Transactional/Transactional';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
@ -74,6 +73,12 @@ export abstract class Invoice extends Transactional {
return this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY;
}
get stockTransferSchemaName() {
return this.isSales
? ModelNameEnum.Shipment
: ModelNameEnum.PurchaseReceipt;
}
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
super(schema, data, fyo);
this._setGetCurrencies();
@ -487,9 +492,7 @@ export abstract class Invoice extends Transactional {
return null;
}
const schemaName = this.isSales
? ModelNameEnum.Shipment
: ModelNameEnum.PurchaseReceipt;
const schemaName = this.stockTransferSchemaName;
const defaults = (this.fyo.singles.Defaults as Defaults) ?? {};
let terms;
@ -505,7 +508,6 @@ export abstract class Invoice extends Transactional {
terms,
backReference: this.name,
};
console.log(data.backReference);
const location =
(this.fyo.singles.InventorySettings as InventorySettings)
@ -521,7 +523,11 @@ export abstract class Invoice extends Transactional {
const item = row.item;
const quantity = row.stockNotTransferred;
const trackItem = itemDoc.trackItem;
const rate = row.rate;
let rate = row.rate as Money;
if (this.exchangeRate && this.exchangeRate > 1) {
rate = rate.mul(this.exchangeRate);
}
if (!quantity || !trackItem) {
continue;
@ -541,4 +547,54 @@ export abstract class Invoice extends Transactional {
return transfer;
}
async beforeCancel(): Promise<void> {
await super.beforeCancel();
await this._validateStockTransferCancelled();
}
async beforeDelete(): Promise<void> {
await super.beforeCancel();
await this._validateStockTransferCancelled();
await this._deleteCancelledStockTransfers();
}
async _deleteCancelledStockTransfers() {
const schemaName = this.stockTransferSchemaName;
const transfers = await this._getLinkedStockTransferNames(true);
for (const { name } of transfers) {
const st = await this.fyo.doc.getDoc(schemaName, name);
await st.delete();
}
}
async _validateStockTransferCancelled() {
const schemaName = this.stockTransferSchemaName;
const transfers = await this._getLinkedStockTransferNames(false);
if (!transfers?.length) {
return;
}
const names = transfers.map(({ name }) => name).join(', ');
const label = this.fyo.schemaMap[schemaName]?.label ?? schemaName;
throw new ValidationError(
this.fyo.t`Cannot cancel ${this.schema.label} ${this
.name!} because of the following ${label}: ${names}`
);
}
async _getLinkedStockTransferNames(cancelled: boolean) {
const name = this.name;
if (!name) {
throw new ValidationError(`Name not found for ${this.schema.label}`);
}
const schemaName = this.stockTransferSchemaName;
const transfers = (await this.fyo.db.getAllRaw(schemaName, {
fields: ['name'],
filters: { backReference: this.name!, cancelled },
})) as { name: string }[];
return transfers;
}
}

View File

@ -9,7 +9,6 @@ import { getLedgerLinkAction, getNumberSeries } from 'models/helpers';
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { getMapFromList } from 'utils/index';
import { StockTransferItem } from './StockTransferItem';
import { Transfer } from './Transfer';
@ -176,7 +175,10 @@ export abstract class StockTransfer extends Transfer {
const notTransferred = (row.stockNotTransferred as number) ?? 0;
const transferred = transferMap[item];
if (!transferred || !notTransferred) {
if (
typeof transferred !== 'number' ||
typeof notTransferred !== 'number'
) {
continue;
}
@ -218,4 +220,10 @@ export abstract class StockTransfer extends Transfer {
return acc;
}, {} as Record<string, number>);
}
override duplicate(): Doc {
const doc = super.duplicate() as StockTransfer;
doc.backReference = undefined;
return doc;
}
}

View File

@ -1,12 +1,14 @@
import {
assertDoesNotThrow,
assertThrows
assertThrows,
} from 'backend/database/tests/helpers';
import { Invoice } from 'models/baseModels/Invoice/Invoice';
import { ModelNameEnum } from 'models/types';
import { RawValue } from 'schemas/types';
import test from 'tape';
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
import { InventorySettings } from '../InventorySettings';
import { StockTransfer } from '../StockTransfer';
import { ValuationMethod } from '../types';
import { getALEs, getItem, getSLEs, getStockTransfer } from './helpers';
@ -285,4 +287,253 @@ test('Purchase Receipt, cancel and delete', async (t) => {
'doc deleted'
);
});
test('Purchase Invoice then Purchase Receipt', async (t) => {
const rate = testDocs.Item[item].rate as number;
const quantity = 3;
const pinv = fyo.doc.getNewDoc(ModelNameEnum.PurchaseInvoice) as Invoice;
const date = new Date('2022-01-04');
await pinv.set({
date,
party,
account: 'Creditors',
});
await pinv.append('items', { item, quantity, rate });
await pinv.sync();
await pinv.submit();
t.equal(pinv.name, 'PINV-1001', 'PINV name matches');
t.equal(pinv.stockNotTransferred, quantity, 'stock not transferred');
const prec = await pinv.getStockTransfer();
if (prec === null) {
return t.ok(false, 'prec was null');
}
prec.date = new Date('2022-01-05');
t.equal(
ModelNameEnum.PurchaseReceipt,
prec.schemaName,
'stock transfer is a PREC'
);
t.equal(prec.backReference, pinv.name, 'back reference is set');
t.equal(prec.items?.[0].quantity, quantity, 'PREC transfers quantity');
await assertDoesNotThrow(async () => await prec.sync());
await assertDoesNotThrow(async () => await prec.submit());
t.equal(prec.name, 'PREC-1002', 'PREC name matches');
t.equal(pinv.stockNotTransferred, 0, 'stock has been transferred');
t.equal(pinv.items?.[0].stockNotTransferred, 0, 'stock has been transferred');
});
test('Back Ref Purchase Receipt cancel', async (t) => {
const prec = (await fyo.doc.getDoc(
ModelNameEnum.PurchaseReceipt,
'PREC-1002'
)) as StockTransfer;
t.equal(prec.backReference, 'PINV-1001', 'back reference matches');
await assertDoesNotThrow(async () => {
await prec.cancel();
});
const pinv = (await fyo.doc.getDoc(
ModelNameEnum.PurchaseInvoice,
'PINV-1001'
)) as Invoice;
t.equal(pinv.stockNotTransferred, 3, 'pinv stock untransferred');
t.equal(
pinv.items?.[0].stockNotTransferred,
3,
'pinv item stock untransferred'
);
});
test('Cancel Purchase Invoice after Purchase Receipt is created', async (t) => {
const pinv = (await fyo.doc.getDoc(
ModelNameEnum.PurchaseInvoice,
'PINV-1001'
)) as Invoice;
const prec = await pinv.getStockTransfer();
if (prec === null) {
return t.ok(false, 'prec was null');
}
prec.date = new Date('2022-01-05');
await prec.sync();
await prec.submit();
t.equal(prec.name, 'PREC-1003', 'PREC name matches');
t.equal(prec.backReference, 'PINV-1001', 'PREC backref matches');
await assertThrows(async () => {
await pinv.cancel();
}, 'cancel prevented cause of PREC');
const ales = await fyo.db.getAllRaw(ModelNameEnum.AccountingLedgerEntry, {
fields: ['name', 'reverted'],
filters: { referenceName: pinv.name!, reverted: true },
});
t.equal(ales.length, 0);
});
test('Sales Invoice then partial Shipment', async (t) => {
const rate = testDocs.Item[item].rate as number;
const sinv = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice) as Invoice;
await sinv.set({
party,
date: new Date('2022-01-06'),
account: 'Debtors',
});
await sinv.append('items', { item, quantity: 3, rate });
await sinv.sync();
await sinv.submit();
t.equal(sinv.name, 'SINV-1001', 'SINV name matches');
t.equal(sinv.stockNotTransferred, 3, 'stock not transferred');
const shpm = await sinv.getStockTransfer();
if (shpm === null) {
return t.ok(false, 'shpm was null');
}
shpm.date = new Date('2022-01-07');
await shpm.items?.[0].set('quantity', 1);
await assertDoesNotThrow(async () => await shpm.sync());
await assertDoesNotThrow(async () => await shpm.submit());
t.equal(ModelNameEnum.Shipment, shpm.schemaName, 'stock transfer is a SHPM');
t.equal(shpm.backReference, sinv.name, 'back reference is set');
t.equal(shpm.items?.[0].quantity, 1, 'shpm transfers quantity 1');
t.equal(shpm.name, 'SHPM-1003', 'SHPM name matches');
t.equal(sinv.stockNotTransferred, 2, 'stock qty 2 has not been transferred');
t.equal(
sinv.items?.[0].stockNotTransferred,
2,
'item stock qty 2 has not been transferred'
);
});
test('Sales Invoice then another Shipment', async (t) => {
const sinv = (await fyo.doc.getDoc(
ModelNameEnum.SalesInvoice,
'SINV-1001'
)) as Invoice;
const shpm = await sinv.getStockTransfer();
if (shpm === null) {
return t.ok(false, 'shpm was null');
}
await assertDoesNotThrow(async () => await shpm.sync());
await assertDoesNotThrow(async () => await shpm.submit());
t.equal(shpm.name, 'SHPM-1004', 'SHPM name matches');
t.equal(shpm.items?.[0].quantity, 2, 'shpm transfers quantity 2');
t.equal(sinv.stockNotTransferred, 0, 'stock has been transferred');
t.equal(
sinv.items?.[0].stockNotTransferred,
0,
'item stock has been transferred'
);
t.equal(await sinv.getStockTransfer(), null, 'no more stock transfers');
});
test('Cancel Sales Invoice after Shipment is created', async (t) => {
const sinv = (await fyo.doc.getDoc(
ModelNameEnum.SalesInvoice,
'SINV-1001'
)) as Invoice;
await assertThrows(
async () => await sinv.cancel(),
'cancel prevent cause of SHPM'
);
const ales = await fyo.db.getAllRaw(ModelNameEnum.AccountingLedgerEntry, {
fields: ['name', 'reverted'],
filters: { referenceName: sinv.name!, reverted: true },
});
t.equal(ales.length, 0);
});
test('Cancel partial Shipment', async (t) => {
let shpm = (await fyo.doc.getDoc(
ModelNameEnum.Shipment,
'SHPM-1003'
)) as StockTransfer;
t.equal(shpm.backReference, 'SINV-1001', 'SHPM 1 back ref is set');
t.equal(shpm.items?.[0].quantity, 1, 'SHPM transfers qty 1');
await assertDoesNotThrow(async () => await shpm.cancel());
t.ok(shpm.isCancelled, 'SHPM cancelled');
const sinv = (await fyo.doc.getDoc(
ModelNameEnum.SalesInvoice,
'SINV-1001'
)) as Invoice;
t.equal(sinv.stockNotTransferred, 1, 'stock qty 1 untransferred');
shpm = (await fyo.doc.getDoc(
ModelNameEnum.Shipment,
'SHPM-1004'
)) as StockTransfer;
t.equal(shpm.backReference, 'SINV-1001', 'SHPM 2 back ref is set');
t.equal(shpm.items?.[0].quantity, 2, 'SHPM transfers qty 2');
await assertDoesNotThrow(async () => await shpm.cancel());
t.ok(shpm.isCancelled, 'SHPM cancelled');
t.equal(sinv.stockNotTransferred, 3, 'all stock untransferred');
});
test('Duplicate Shipment, backref unset', async (t) => {
const shpm = (await fyo.doc.getDoc(
ModelNameEnum.Shipment,
'SHPM-1003'
)) as StockTransfer;
t.ok(shpm.backReference, 'SHPM back ref is set');
const doc = shpm.duplicate();
t.notOk(doc.backReference, 'Duplicate SHPM back ref is not set');
});
test('Cancel and Delete Sales Invoice with cancelled Shipments', async (t) => {
const sinv = (await fyo.doc.getDoc(
ModelNameEnum.SalesInvoice,
'SINV-1001'
)) as Invoice;
await assertDoesNotThrow(async () => await sinv.cancel());
t.ok(sinv.isCancelled, 'sinv cancelled');
const transfers = (await fyo.db.getAllRaw(ModelNameEnum.Shipment, {
fields: ['name'],
filters: { backReference: 'SINV-1001' },
})) as { name: string }[];
await assertDoesNotThrow(async () => await sinv.delete());
t.notOk(
await fyo.db.exists(ModelNameEnum.SalesInvoice, 'SINV-1001'),
'SINV-1001 deleted'
);
for (const { name } of transfers) {
t.notOk(
await fyo.db.exists(ModelNameEnum.Shipment, 'SINV-1001'),
`linked Shipment ${name} deleted`
);
}
});
closeTestFyo(fyo, __filename);