mirror of
https://github.com/frappe/books.git
synced 2025-02-02 20:18:26 +00:00
Merge pull request #536 from 18alantom/manufacture-stock-movement
Manufacture stock movement
This commit is contained in:
commit
362e10c948
@ -10,6 +10,7 @@ module.exports = {
|
|||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
'arrow-body-style': 'off',
|
'arrow-body-style': 'off',
|
||||||
'prefer-arrow-callback': 'off',
|
'prefer-arrow-callback': 'off',
|
||||||
|
'vue/no-mutating-props': 'off',
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
'vue/no-useless-template-attributes': 'off',
|
'vue/no-useless-template-attributes': 'off',
|
||||||
},
|
},
|
||||||
|
@ -2,9 +2,19 @@ import { getDefaultMetaFieldValueMap } from '../../backend/helpers';
|
|||||||
import { DatabaseManager } from '../database/manager';
|
import { DatabaseManager } from '../database/manager';
|
||||||
|
|
||||||
async function execute(dm: DatabaseManager) {
|
async function execute(dm: DatabaseManager) {
|
||||||
|
const s = (await dm.db?.getAll('SingleValue', {
|
||||||
|
fields: ['value'],
|
||||||
|
filters: { fieldname: 'setupComplete' },
|
||||||
|
})) as { value: string }[];
|
||||||
|
|
||||||
|
if (!Number(s?.[0]?.value ?? '0')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const names: Record<string, string> = {
|
const names: Record<string, string> = {
|
||||||
StockMovement: 'SMOV-',
|
StockMovement: 'SMOV-',
|
||||||
Shipment: 'SHP-',
|
PurchaseReceipt: 'PREC-',
|
||||||
|
Shipment: 'SHPM-',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const referenceType in names) {
|
for (const referenceType in names) {
|
||||||
|
@ -6,7 +6,12 @@ import {
|
|||||||
FormulaMap,
|
FormulaMap,
|
||||||
ListViewSettings,
|
ListViewSettings,
|
||||||
} from 'fyo/model/types';
|
} from 'fyo/model/types';
|
||||||
import { addItem, getDocStatusListColumn, getLedgerLinkAction } from 'models/helpers';
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
|
import {
|
||||||
|
addItem,
|
||||||
|
getDocStatusListColumn,
|
||||||
|
getLedgerLinkAction,
|
||||||
|
} from 'models/helpers';
|
||||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
@ -42,6 +47,24 @@ export class StockMovement extends Transfer {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async validate() {
|
||||||
|
await super.validate();
|
||||||
|
if (this.movementType !== MovementType.Manufacture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFrom = this.items?.findIndex((f) => f.fromLocation) !== -1;
|
||||||
|
const hasTo = this.items?.findIndex((f) => f.toLocation) !== -1;
|
||||||
|
|
||||||
|
if (!hasFrom) {
|
||||||
|
throw new ValidationError(this.fyo.t`Item with From location not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTo) {
|
||||||
|
throw new ValidationError(this.fyo.t`Item with To location not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
|
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
|
||||||
};
|
};
|
||||||
@ -68,6 +91,7 @@ export class StockMovement extends Transfer {
|
|||||||
[MovementType.MaterialIssue]: fyo.t`Material Issue`,
|
[MovementType.MaterialIssue]: fyo.t`Material Issue`,
|
||||||
[MovementType.MaterialReceipt]: fyo.t`Material Receipt`,
|
[MovementType.MaterialReceipt]: fyo.t`Material Receipt`,
|
||||||
[MovementType.MaterialTransfer]: fyo.t`Material Transfer`,
|
[MovementType.MaterialTransfer]: fyo.t`Material Transfer`,
|
||||||
|
[MovementType.Manufacture]: fyo.t`Manufacture`,
|
||||||
}[movementType] ?? '';
|
}[movementType] ?? '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -4,7 +4,9 @@ import {
|
|||||||
FormulaMap,
|
FormulaMap,
|
||||||
ReadOnlyMap,
|
ReadOnlyMap,
|
||||||
RequiredMap,
|
RequiredMap,
|
||||||
|
ValidationMap,
|
||||||
} from 'fyo/model/types';
|
} from 'fyo/model/types';
|
||||||
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { StockMovement } from './StockMovement';
|
import { StockMovement } from './StockMovement';
|
||||||
@ -32,6 +34,10 @@ export class StockMovementItem extends Doc {
|
|||||||
return this.parentdoc?.movementType === MovementType.MaterialTransfer;
|
return this.parentdoc?.movementType === MovementType.MaterialTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isManufacture() {
|
||||||
|
return this.parentdoc?.movementType === MovementType.Manufacture;
|
||||||
|
}
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
item: () => ({ trackItem: true }),
|
item: () => ({ trackItem: true }),
|
||||||
};
|
};
|
||||||
@ -52,14 +58,14 @@ export class StockMovementItem extends Doc {
|
|||||||
dependsOn: ['item', 'rate', 'quantity'],
|
dependsOn: ['item', 'rate', 'quantity'],
|
||||||
},
|
},
|
||||||
fromLocation: {
|
fromLocation: {
|
||||||
formula: (fn) => {
|
formula: () => {
|
||||||
if (this.isReceipt || this.isTransfer) {
|
if (this.isReceipt || this.isTransfer || this.isManufacture) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLocation = this.fyo.singles.InventorySettings
|
const defaultLocation = this.fyo.singles.InventorySettings
|
||||||
?.defaultLocation as string | undefined;
|
?.defaultLocation as string | undefined;
|
||||||
if (defaultLocation && !this.location && this.isIssue) {
|
if (defaultLocation && !this.fromLocation && this.isIssue) {
|
||||||
return defaultLocation;
|
return defaultLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,14 +74,14 @@ export class StockMovementItem extends Doc {
|
|||||||
dependsOn: ['movementType'],
|
dependsOn: ['movementType'],
|
||||||
},
|
},
|
||||||
toLocation: {
|
toLocation: {
|
||||||
formula: (fn) => {
|
formula: () => {
|
||||||
if (this.isIssue || this.isTransfer) {
|
if (this.isIssue || this.isTransfer || this.isManufacture) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLocation = this.fyo.singles.InventorySettings
|
const defaultLocation = this.fyo.singles.InventorySettings
|
||||||
?.defaultLocation as string | undefined;
|
?.defaultLocation as string | undefined;
|
||||||
if (defaultLocation && !this.location && this.isReceipt) {
|
if (defaultLocation && !this.toLocation && this.isReceipt) {
|
||||||
return defaultLocation;
|
return defaultLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +91,31 @@ export class StockMovementItem extends Doc {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
validations: ValidationMap = {
|
||||||
|
fromLocation: (value) => {
|
||||||
|
if (!this.isManufacture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && this.toLocation) {
|
||||||
|
throw new ValidationError(
|
||||||
|
this.fyo.t`Only From or To can be set for Manucature`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toLocation: (value) => {
|
||||||
|
if (!this.isManufacture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && this.fromLocation) {
|
||||||
|
throw new ValidationError(
|
||||||
|
this.fyo.t`Only From or To can be set for Manufacture`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
required: RequiredMap = {
|
required: RequiredMap = {
|
||||||
fromLocation: () => this.isIssue || this.isTransfer,
|
fromLocation: () => this.isIssue || this.isTransfer,
|
||||||
toLocation: () => this.isReceipt || this.isTransfer,
|
toLocation: () => this.isReceipt || this.isTransfer,
|
||||||
|
136
models/inventory/tests/testStockMovement.spec.ts
Normal file
136
models/inventory/tests/testStockMovement.spec.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import test from 'tape';
|
||||||
|
import { getItem } from './helpers';
|
||||||
|
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||||
|
import { MovementType } from '../types';
|
||||||
|
import {
|
||||||
|
assertDoesNotThrow,
|
||||||
|
assertThrows,
|
||||||
|
} from 'backend/database/tests/helpers';
|
||||||
|
import { StockMovement } from '../StockMovement';
|
||||||
|
|
||||||
|
const fyo = getTestFyo();
|
||||||
|
setupTestFyo(fyo, __filename);
|
||||||
|
|
||||||
|
test('check store and create test items', async (t) => {
|
||||||
|
const e = await fyo.db.exists(ModelNameEnum.Location, 'Stores');
|
||||||
|
t.equals(e, true, 'location Stores exist');
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
getItem('RawOne', 100),
|
||||||
|
getItem('RawTwo', 100),
|
||||||
|
getItem('Final', 200),
|
||||||
|
];
|
||||||
|
|
||||||
|
const exists: boolean[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
await fyo.doc.getNewDoc('Item', item).sync();
|
||||||
|
exists.push(await fyo.db.exists('Item', item.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ok(exists.every(Boolean), 'items created');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Stock Movement, Material Receipt', async (t) => {
|
||||||
|
const sm = fyo.doc.getNewDoc(ModelNameEnum.StockMovement);
|
||||||
|
|
||||||
|
await sm.set({
|
||||||
|
date: new Date('2022-01-01'),
|
||||||
|
movementType: MovementType.MaterialReceipt,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'RawOne',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
toLocation: 'Stores',
|
||||||
|
});
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'RawTwo',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
toLocation: 'Stores',
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertDoesNotThrow(async () => await sm.sync());
|
||||||
|
await assertDoesNotThrow(async () => await sm.submit());
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('RawOne', 'Stores'),
|
||||||
|
1,
|
||||||
|
'item RawOne added'
|
||||||
|
);
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('RawTwo', 'Stores'),
|
||||||
|
1,
|
||||||
|
'item RawTwo added'
|
||||||
|
);
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('Final', 'Stores'),
|
||||||
|
null,
|
||||||
|
'item Final not yet added'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Stock Movement, Manufacture', async (t) => {
|
||||||
|
const sm = fyo.doc.getNewDoc(ModelNameEnum.StockMovement) as StockMovement;
|
||||||
|
|
||||||
|
await sm.set({
|
||||||
|
date: new Date('2022-01-02'),
|
||||||
|
movementType: MovementType.Manufacture,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'RawOne',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertDoesNotThrow(
|
||||||
|
async () => await sm.items?.[0].set('fromLocation', 'Stores')
|
||||||
|
);
|
||||||
|
await assertThrows(
|
||||||
|
async () => await sm.items?.[0].set('toLocation', 'Stores')
|
||||||
|
);
|
||||||
|
t.notOk(sm.items?.[0].to, 'to location not set');
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'RawTwo',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
fromLocation: 'Stores',
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertThrows(async () => await sm.sync());
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'Final',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
toLocation: 'Stores',
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertDoesNotThrow(async () => await sm.sync());
|
||||||
|
await assertDoesNotThrow(async () => await sm.submit());
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('RawOne', 'Stores'),
|
||||||
|
0,
|
||||||
|
'item RawOne removed'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('RawTwo', 'Stores'),
|
||||||
|
0,
|
||||||
|
'item RawTwo removed'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('Final', 'Stores'),
|
||||||
|
1,
|
||||||
|
'item Final added'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
closeTestFyo(fyo, __filename);
|
@ -9,6 +9,7 @@ export enum MovementType {
|
|||||||
'MaterialIssue' = 'MaterialIssue',
|
'MaterialIssue' = 'MaterialIssue',
|
||||||
'MaterialReceipt' = 'MaterialReceipt',
|
'MaterialReceipt' = 'MaterialReceipt',
|
||||||
'MaterialTransfer' = 'MaterialTransfer',
|
'MaterialTransfer' = 'MaterialTransfer',
|
||||||
|
'Manufacture' = 'Manufacture',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SMDetails {
|
export interface SMDetails {
|
||||||
|
@ -35,6 +35,10 @@
|
|||||||
{
|
{
|
||||||
"value": "MaterialTransfer",
|
"value": "MaterialTransfer",
|
||||||
"label": "Material Transfer"
|
"label": "Material Transfer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "Manufacture",
|
||||||
|
"label": "Manufacture"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"required": true
|
"required": true
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
import { Doc } from 'fyo/model/doc';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import Row from 'src/components/Row.vue';
|
import Row from 'src/components/Row.vue';
|
||||||
import { getErrorMessage } from 'src/utils';
|
import { getErrorMessage } from 'src/utils';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
import Button from '../Button.vue';
|
import Button from '../Button.vue';
|
||||||
import FormControl from './FormControl.vue';
|
import FormControl from './FormControl.vue';
|
||||||
|
|
||||||
@ -102,15 +103,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange(df, value) {
|
async onChange(df, value) {
|
||||||
if (value == null) {
|
const fieldname = df.fieldname;
|
||||||
return;
|
this.errors[fieldname] = null;
|
||||||
}
|
const oldValue = this.row[fieldname];
|
||||||
|
|
||||||
this.errors[df.fieldname] = null;
|
try {
|
||||||
this.row.set(df.fieldname, value).catch((e) => {
|
await this.row.set(fieldname, value);
|
||||||
this.errors[df.fieldname] = getErrorMessage(e, this.row);
|
} catch (e) {
|
||||||
});
|
this.errors[fieldname] = getErrorMessage(e, this.row);
|
||||||
|
this.row[fieldname] = '';
|
||||||
|
nextTick(() => (this.row[fieldname] = oldValue));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getErrorString() {
|
getErrorString() {
|
||||||
return Object.values(this.errors).filter(Boolean).join(' ');
|
return Object.values(this.errors).filter(Boolean).join(' ');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user