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

fix: stock movement

- test: stock movement create and cancel
This commit is contained in:
18alantom 2022-11-02 20:26:37 +05:30
parent 2a5686058d
commit 43784984c3
7 changed files with 327 additions and 82 deletions

View File

@ -46,6 +46,7 @@ export const databaseMethodSet: Set<DatabaseMethod> = new Set([
'rename',
'update',
'delete',
'deleteAll',
'close',
'exists',
]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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