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:
parent
ab2d2f8975
commit
a3f96a90e1
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user