mirror of
https://github.com/frappe/books.git
synced 2024-11-08 06:44:06 +00:00
fix: stock movement
- test: stock movement create and cancel
This commit is contained in:
parent
2a5686058d
commit
43784984c3
@ -46,6 +46,7 @@ export const databaseMethodSet: Set<DatabaseMethod> = new Set([
|
||||
'rename',
|
||||
'update',
|
||||
'delete',
|
||||
'deleteAll',
|
||||
'close',
|
||||
'exists',
|
||||
]);
|
||||
|
@ -129,7 +129,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return !!this.submitted && !!this.cancelled;
|
||||
}
|
||||
|
||||
get syncing() {
|
||||
get isSyncing() {
|
||||
return this._syncing;
|
||||
}
|
||||
|
||||
@ -159,13 +159,18 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
|
||||
_setValuesWithoutChecks(data: DocValueMap, convertToDocValue: boolean) {
|
||||
for (const field of this.schema.fields) {
|
||||
const fieldname = field.fieldname;
|
||||
const { fieldname, fieldtype } = field;
|
||||
const value = data[field.fieldname];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const row of value) {
|
||||
this.push(fieldname, row, convertToDocValue);
|
||||
}
|
||||
} else if (
|
||||
fieldtype === FieldTypeEnum.Currency &&
|
||||
typeof value === 'number'
|
||||
) {
|
||||
this[fieldname] = this.fyo.pesa(value);
|
||||
} else if (value !== undefined && !convertToDocValue) {
|
||||
this[fieldname] = value;
|
||||
} else if (value !== undefined) {
|
||||
@ -269,13 +274,13 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
async _applyChange(
|
||||
fieldname: string,
|
||||
changedFieldname: string,
|
||||
retriggerChildDocApplyChange?: boolean
|
||||
): Promise<boolean> {
|
||||
await this._applyFormula(fieldname, retriggerChildDocApplyChange);
|
||||
await this._applyFormula(changedFieldname, retriggerChildDocApplyChange);
|
||||
await this.trigger('change', {
|
||||
doc: this,
|
||||
changed: fieldname,
|
||||
changed: changedFieldname,
|
||||
});
|
||||
|
||||
return true;
|
||||
@ -666,16 +671,17 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
async _applyFormula(
|
||||
fieldname?: string,
|
||||
changedFieldname?: string,
|
||||
retriggerChildDocApplyChange?: boolean
|
||||
): Promise<boolean> {
|
||||
const doc = this;
|
||||
let changed = await this._callAllTableFieldsApplyFormula(fieldname);
|
||||
changed = (await this._applyFormulaForFields(doc, fieldname)) || changed;
|
||||
let changed = await this._callAllTableFieldsApplyFormula(changedFieldname);
|
||||
changed =
|
||||
(await this._applyFormulaForFields(doc, changedFieldname)) || changed;
|
||||
|
||||
if (changed && retriggerChildDocApplyChange) {
|
||||
await this._callAllTableFieldsApplyFormula(fieldname);
|
||||
await this._applyFormulaForFields(doc, fieldname);
|
||||
await this._callAllTableFieldsApplyFormula(changedFieldname);
|
||||
await this._applyFormulaForFields(doc, changedFieldname);
|
||||
}
|
||||
|
||||
return changed;
|
||||
|
@ -105,10 +105,45 @@ export function shouldApplyFormula(field: Field, doc: Doc, fieldname?: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (doc.isSyncing && dependsOn.length > 0) {
|
||||
return shouldApplyFormulaPreSync(field.fieldname, dependsOn, doc);
|
||||
}
|
||||
|
||||
const value = doc.get(field.fieldname);
|
||||
return getIsNullOrUndef(value);
|
||||
}
|
||||
|
||||
function shouldApplyFormulaPreSync(
|
||||
fieldname: string,
|
||||
dependsOn: string[],
|
||||
doc: Doc
|
||||
): boolean {
|
||||
if (isDocValueTruthy(doc.get(fieldname))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const d of dependsOn) {
|
||||
const isSet = isDocValueTruthy(doc.get(d));
|
||||
if (isSet) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isDocValueTruthy(docValue: DocValue | Doc[]) {
|
||||
if (isPesa(docValue)) {
|
||||
return !(docValue as Money).isZero();
|
||||
}
|
||||
|
||||
if (Array.isArray(docValue)) {
|
||||
return docValue.length > 0;
|
||||
}
|
||||
|
||||
return !!docValue;
|
||||
}
|
||||
|
||||
export function setChildDocIdx(childDocs: Doc[]) {
|
||||
for (const idx in childDocs) {
|
||||
childDocs[idx].idx = +idx;
|
||||
|
@ -25,25 +25,41 @@ export class StockManager {
|
||||
this.fyo = fyo;
|
||||
}
|
||||
|
||||
async transferStock(transferDetails: SMTransferDetails) {
|
||||
const details = this.#getSMIDetails(transferDetails);
|
||||
const item = new StockManagerItem(details, this.fyo);
|
||||
await item.transferStock(this.isCancelled);
|
||||
this.items.push(item);
|
||||
async createTransfers(transferDetails: SMTransferDetails[]) {
|
||||
for (const detail of transferDetails) {
|
||||
await this.#createTransfer(detail);
|
||||
}
|
||||
|
||||
await this.#sync();
|
||||
}
|
||||
|
||||
async sync() {
|
||||
async cancelTransfers() {
|
||||
const { referenceName, referenceType } = this.details;
|
||||
await this.fyo.db.deleteAll(ModelNameEnum.StockLedgerEntry, {
|
||||
referenceType,
|
||||
referenceName,
|
||||
});
|
||||
}
|
||||
|
||||
async #sync() {
|
||||
for (const item of this.items) {
|
||||
await item.sync();
|
||||
}
|
||||
}
|
||||
|
||||
async #createTransfer(transferDetails: SMTransferDetails) {
|
||||
const details = this.#getSMIDetails(transferDetails);
|
||||
const item = new StockManagerItem(details, this.fyo);
|
||||
await item.transferStock();
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
#getSMIDetails(transferDetails: SMTransferDetails): SMIDetails {
|
||||
return Object.assign({}, this.details, transferDetails);
|
||||
}
|
||||
}
|
||||
|
||||
export class StockManagerItem {
|
||||
class StockManagerItem {
|
||||
/**
|
||||
* The Stock Manager Item is used to move stock to and from a location. It
|
||||
* updates the Stock Queue and creates Stock Ledger Entries.
|
||||
@ -80,9 +96,9 @@ export class StockManagerItem {
|
||||
this.fyo = fyo;
|
||||
}
|
||||
|
||||
async transferStock(isCancelled: boolean) {
|
||||
transferStock() {
|
||||
this.#clear();
|
||||
await this.#moveStockForBothLocations(isCancelled);
|
||||
this.#moveStockForBothLocations();
|
||||
}
|
||||
|
||||
async sync() {
|
||||
@ -91,29 +107,17 @@ export class StockManagerItem {
|
||||
}
|
||||
}
|
||||
|
||||
async #moveStockForBothLocations(isCancelled: boolean) {
|
||||
#moveStockForBothLocations() {
|
||||
if (this.fromLocation) {
|
||||
await this.#moveStockForSingleLocation(
|
||||
this.fromLocation,
|
||||
isCancelled ? false : true,
|
||||
isCancelled
|
||||
);
|
||||
this.#moveStockForSingleLocation(this.fromLocation, true);
|
||||
}
|
||||
|
||||
if (this.toLocation) {
|
||||
await this.#moveStockForSingleLocation(
|
||||
this.toLocation,
|
||||
isCancelled ? true : false,
|
||||
isCancelled
|
||||
);
|
||||
this.#moveStockForSingleLocation(this.toLocation, false);
|
||||
}
|
||||
}
|
||||
|
||||
async #moveStockForSingleLocation(
|
||||
location: string,
|
||||
isOutward: boolean,
|
||||
isCancelled: boolean
|
||||
) {
|
||||
#moveStockForSingleLocation(location: string, isOutward: boolean) {
|
||||
let quantity = this.quantity!;
|
||||
if (quantity === 0) {
|
||||
return;
|
||||
@ -124,10 +128,8 @@ export class StockManagerItem {
|
||||
}
|
||||
|
||||
// Stock Ledger Entry
|
||||
if (!isCancelled) {
|
||||
const stockLedgerEntry = this.#getStockLedgerEntry(location, quantity);
|
||||
this.stockLedgerEntries?.push(stockLedgerEntry);
|
||||
}
|
||||
const stockLedgerEntry = this.#getStockLedgerEntry(location, quantity);
|
||||
this.stockLedgerEntries?.push(stockLedgerEntry);
|
||||
}
|
||||
|
||||
#getStockLedgerEntry(location: string, quantity: number) {
|
||||
|
@ -44,29 +44,22 @@ export class StockMovement extends Doc {
|
||||
}
|
||||
|
||||
async afterSubmit(): Promise<void> {
|
||||
await this._transferStock();
|
||||
const transferDetails = this._getTransferDetails();
|
||||
await this._getStockManager().createTransfers(transferDetails);
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
await this._transferStock();
|
||||
await this._getStockManager().cancelTransfers();
|
||||
}
|
||||
|
||||
async _transferStock() {
|
||||
const stockManager = this._getStockManager();
|
||||
this._makeTransfers(stockManager);
|
||||
await stockManager.sync();
|
||||
}
|
||||
|
||||
_makeTransfers(stockManager: StockManager) {
|
||||
for (const row of this.items ?? []) {
|
||||
stockManager.transferStock({
|
||||
item: row.item!,
|
||||
rate: row.rate!,
|
||||
quantity: row.quantity!,
|
||||
fromLocation: row.fromLocation,
|
||||
toLocation: row.toLocation,
|
||||
});
|
||||
}
|
||||
_getTransferDetails() {
|
||||
return (this.items ?? []).map((row) => ({
|
||||
item: row.item!,
|
||||
rate: row.rate!,
|
||||
quantity: row.quantity!,
|
||||
fromLocation: row.fromLocation,
|
||||
toLocation: row.toLocation,
|
||||
}));
|
||||
}
|
||||
|
||||
_getStockManager(): StockManager {
|
||||
|
@ -1,21 +1,64 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import test from 'tape';
|
||||
import { StockMovement } from '../StockMovement';
|
||||
import { MovementType } from '../types';
|
||||
|
||||
export const dummyItems = [
|
||||
{
|
||||
name: 'Ball Pen',
|
||||
rate: 50,
|
||||
for: 'Both',
|
||||
trackItem: true,
|
||||
},
|
||||
{
|
||||
name: 'Ink Pen',
|
||||
rate: 700,
|
||||
for: 'Both',
|
||||
trackItem: true,
|
||||
},
|
||||
];
|
||||
type SLE = {
|
||||
date: string;
|
||||
name: string;
|
||||
item: string;
|
||||
location: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
};
|
||||
|
||||
export function createDummyItems(fyo: Fyo) {
|
||||
type Transfer = {
|
||||
item: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
quantity: number;
|
||||
rate: number;
|
||||
};
|
||||
|
||||
export function getItem(name: string, rate: number) {
|
||||
return { name, rate, trackItem: true };
|
||||
}
|
||||
|
||||
export async function getStockMovement(
|
||||
movementType: MovementType,
|
||||
transfers: Transfer[],
|
||||
fyo: Fyo
|
||||
): Promise<StockMovement> {
|
||||
const doc = fyo.doc.getNewDoc(ModelNameEnum.StockMovement, {
|
||||
movementType,
|
||||
}) as StockMovement;
|
||||
|
||||
for (const {
|
||||
item,
|
||||
from: fromLocation,
|
||||
to: toLocation,
|
||||
quantity,
|
||||
rate,
|
||||
} of transfers) {
|
||||
await doc.append('items', {
|
||||
item,
|
||||
fromLocation,
|
||||
toLocation,
|
||||
rate,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function getSLEs(
|
||||
referenceName: string,
|
||||
referenceType: string,
|
||||
fyo: Fyo
|
||||
) {
|
||||
return (await fyo.db.getAllRaw(ModelNameEnum.StockLedgerEntry, {
|
||||
filters: { referenceName, referenceType },
|
||||
fields: ['date', 'name', 'item', 'location', 'rate', 'quantity'],
|
||||
})) as SLE[];
|
||||
}
|
||||
|
@ -1,24 +1,189 @@
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import test from 'tape';
|
||||
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||
import { dummyItems } from './helpers';
|
||||
import { StockMovement } from '../StockMovement';
|
||||
import { MovementType } from '../types';
|
||||
import { getItem, getSLEs, getStockMovement } from './helpers';
|
||||
|
||||
const fyo = getTestFyo();
|
||||
|
||||
setupTestFyo(fyo, __filename);
|
||||
|
||||
test('create dummy items', async (t) => {
|
||||
for (const item of dummyItems) {
|
||||
const doc = fyo.doc.getNewDoc(ModelNameEnum.Item, item);
|
||||
t.ok(await doc.sync(), `${item.name} created`);
|
||||
const itemMap = {
|
||||
Pen: {
|
||||
name: 'Pen',
|
||||
rate: 700,
|
||||
},
|
||||
Ink: {
|
||||
name: 'Ink',
|
||||
rate: 50,
|
||||
},
|
||||
};
|
||||
|
||||
const locationMap = {
|
||||
LocationOne: 'LocationOne',
|
||||
LocationTwo: 'LocationTwo',
|
||||
};
|
||||
|
||||
/**
|
||||
* Section 1: Test Creation of Items and Locations
|
||||
*/
|
||||
|
||||
test('create dummy items & locations', async (t) => {
|
||||
// Create Items
|
||||
for (const { name, rate } of Object.values(itemMap)) {
|
||||
const item = getItem(name, rate);
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Item, item).sync();
|
||||
t.ok(await fyo.db.exists(ModelNameEnum.Item, name), `${name} exists`);
|
||||
}
|
||||
|
||||
// Create Locations
|
||||
for (const name of Object.values(locationMap)) {
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Location, { name }).sync();
|
||||
t.ok(await fyo.db.exists(ModelNameEnum.Location, name), `${name} exists`);
|
||||
}
|
||||
});
|
||||
|
||||
test('check dummy items', async (t) => {
|
||||
for (const { name } of dummyItems) {
|
||||
const exists = await fyo.db.exists(ModelNameEnum.Item, name);
|
||||
t.ok(exists, `${name} exists`);
|
||||
/**
|
||||
* Section 2: Test Creation of Stock Movements
|
||||
*/
|
||||
|
||||
test('create stock movement, material receipt', async (t) => {
|
||||
const { rate } = itemMap.Ink;
|
||||
const quantity = 2;
|
||||
const amount = rate * quantity;
|
||||
const stockMovement = await getStockMovement(
|
||||
MovementType.MaterialReceipt,
|
||||
[
|
||||
{
|
||||
item: itemMap.Ink.name,
|
||||
to: locationMap.LocationOne,
|
||||
quantity,
|
||||
rate,
|
||||
},
|
||||
],
|
||||
fyo
|
||||
);
|
||||
|
||||
await (await stockMovement.sync()).submit();
|
||||
t.ok(stockMovement.name?.startsWith('SMOV-'));
|
||||
t.equal(stockMovement.amount?.float, amount);
|
||||
t.equal(stockMovement.items?.[0].amount?.float, amount);
|
||||
|
||||
const name = stockMovement.name!;
|
||||
|
||||
const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
|
||||
t.equal(sles.length, 1);
|
||||
|
||||
const sle = sles[0];
|
||||
t.notEqual(new Date(sle.date).toString(), 'Invalid Date');
|
||||
t.equal(parseInt(sle.name), 1);
|
||||
t.equal(sle.item, itemMap.Ink.name);
|
||||
t.equal(parseFloat(sle.rate), rate);
|
||||
t.equal(sle.quantity, quantity);
|
||||
t.equal(sle.location, locationMap.LocationOne);
|
||||
});
|
||||
|
||||
test('create stock movement, material transfer', async (t) => {
|
||||
const { rate } = itemMap.Ink;
|
||||
const quantity = 2;
|
||||
|
||||
const stockMovement = await getStockMovement(
|
||||
MovementType.MaterialTransfer,
|
||||
[
|
||||
{
|
||||
item: itemMap.Ink.name,
|
||||
from: locationMap.LocationOne,
|
||||
to: locationMap.LocationTwo,
|
||||
quantity,
|
||||
rate,
|
||||
},
|
||||
],
|
||||
fyo
|
||||
);
|
||||
|
||||
await (await stockMovement.sync()).submit();
|
||||
const name = stockMovement.name!;
|
||||
|
||||
const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
|
||||
t.equal(sles.length, 2);
|
||||
|
||||
for (const sle of sles) {
|
||||
t.notEqual(new Date(sle.date).toString(), 'Invalid Date');
|
||||
t.equal(sle.item, itemMap.Ink.name);
|
||||
t.equal(parseFloat(sle.rate), rate);
|
||||
|
||||
if (sle.location === locationMap.LocationOne) {
|
||||
t.equal(sle.quantity, -quantity);
|
||||
} else if (sle.location === locationMap.LocationTwo) {
|
||||
t.equal(sle.quantity, quantity);
|
||||
} else {
|
||||
t.ok(false, 'no-op');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('create stock movement, material issue', async (t) => {
|
||||
const { rate } = itemMap.Ink;
|
||||
const quantity = 2;
|
||||
|
||||
const stockMovement = await getStockMovement(
|
||||
MovementType.MaterialIssue,
|
||||
[
|
||||
{
|
||||
item: itemMap.Ink.name,
|
||||
from: locationMap.LocationTwo,
|
||||
quantity,
|
||||
rate,
|
||||
},
|
||||
],
|
||||
fyo
|
||||
);
|
||||
|
||||
await (await stockMovement.sync()).submit();
|
||||
const name = stockMovement.name!;
|
||||
|
||||
const sles = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
|
||||
t.equal(sles.length, 1);
|
||||
|
||||
const sle = sles[0];
|
||||
t.notEqual(new Date(sle.date).toString(), 'Invalid Date');
|
||||
t.equal(sle.item, itemMap.Ink.name);
|
||||
t.equal(parseFloat(sle.rate), rate);
|
||||
t.equal(sle.quantity, -quantity);
|
||||
t.equal(sle.location, locationMap.LocationTwo);
|
||||
});
|
||||
|
||||
/**
|
||||
* Section 3: Test Cancellation of Stock Movements
|
||||
*/
|
||||
|
||||
test('cancel stock movement', async (t) => {
|
||||
const names = (await fyo.db.getAllRaw(ModelNameEnum.StockMovement)) as {
|
||||
name: string;
|
||||
}[];
|
||||
|
||||
for (const { name } of names) {
|
||||
const slesBefore = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
|
||||
const doc = (await fyo.doc.getDoc(
|
||||
ModelNameEnum.StockMovement,
|
||||
name
|
||||
)) as StockMovement;
|
||||
|
||||
if (doc.movementType === MovementType.MaterialTransfer) {
|
||||
t.equal(slesBefore.length, (doc.items?.length ?? 0) * 2);
|
||||
} else {
|
||||
t.equal(slesBefore.length, doc.items?.length ?? 0);
|
||||
}
|
||||
|
||||
await doc.cancel();
|
||||
const slesAfter = await getSLEs(name, ModelNameEnum.StockMovement, fyo);
|
||||
t.equal(slesAfter.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Section 4: Test Invalid entries
|
||||
*/
|
||||
|
||||
closeTestFyo(fyo, __filename);
|
||||
|
Loading…
Reference in New Issue
Block a user