diff --git a/.eslintrc.js b/.eslintrc.js index 7574bf64..2b09c43c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,6 +10,7 @@ module.exports = { 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'arrow-body-style': 'off', 'prefer-arrow-callback': 'off', + 'vue/no-mutating-props': 'off', 'vue/multi-word-component-names': 'off', 'vue/no-useless-template-attributes': 'off', }, diff --git a/backend/patches/createInventoryNumberSeries.ts b/backend/patches/createInventoryNumberSeries.ts index 2852691a..96410427 100644 --- a/backend/patches/createInventoryNumberSeries.ts +++ b/backend/patches/createInventoryNumberSeries.ts @@ -2,9 +2,19 @@ import { getDefaultMetaFieldValueMap } from '../../backend/helpers'; import { DatabaseManager } from '../database/manager'; 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 = { StockMovement: 'SMOV-', - Shipment: 'SHP-', + PurchaseReceipt: 'PREC-', + Shipment: 'SHPM-', }; for (const referenceType in names) { diff --git a/models/inventory/StockMovement.ts b/models/inventory/StockMovement.ts index 25fabee1..2a157e83 100644 --- a/models/inventory/StockMovement.ts +++ b/models/inventory/StockMovement.ts @@ -6,7 +6,12 @@ import { FormulaMap, ListViewSettings, } 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 { ModelNameEnum } from 'models/types'; 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 = { numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }), }; @@ -68,6 +91,7 @@ export class StockMovement extends Transfer { [MovementType.MaterialIssue]: fyo.t`Material Issue`, [MovementType.MaterialReceipt]: fyo.t`Material Receipt`, [MovementType.MaterialTransfer]: fyo.t`Material Transfer`, + [MovementType.Manufacture]: fyo.t`Manufacture`, }[movementType] ?? ''; return { diff --git a/models/inventory/StockMovementItem.ts b/models/inventory/StockMovementItem.ts index 665852a1..da012873 100644 --- a/models/inventory/StockMovementItem.ts +++ b/models/inventory/StockMovementItem.ts @@ -4,7 +4,9 @@ import { FormulaMap, ReadOnlyMap, RequiredMap, + ValidationMap, } from 'fyo/model/types'; +import { ValidationError } from 'fyo/utils/errors'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { StockMovement } from './StockMovement'; @@ -32,6 +34,10 @@ export class StockMovementItem extends Doc { return this.parentdoc?.movementType === MovementType.MaterialTransfer; } + get isManufacture() { + return this.parentdoc?.movementType === MovementType.Manufacture; + } + static filters: FiltersMap = { item: () => ({ trackItem: true }), }; @@ -52,14 +58,14 @@ export class StockMovementItem extends Doc { dependsOn: ['item', 'rate', 'quantity'], }, fromLocation: { - formula: (fn) => { - if (this.isReceipt || this.isTransfer) { + formula: () => { + if (this.isReceipt || this.isTransfer || this.isManufacture) { return null; } const defaultLocation = this.fyo.singles.InventorySettings ?.defaultLocation as string | undefined; - if (defaultLocation && !this.location && this.isIssue) { + if (defaultLocation && !this.fromLocation && this.isIssue) { return defaultLocation; } @@ -68,14 +74,14 @@ export class StockMovementItem extends Doc { dependsOn: ['movementType'], }, toLocation: { - formula: (fn) => { - if (this.isIssue || this.isTransfer) { + formula: () => { + if (this.isIssue || this.isTransfer || this.isManufacture) { return null; } const defaultLocation = this.fyo.singles.InventorySettings ?.defaultLocation as string | undefined; - if (defaultLocation && !this.location && this.isReceipt) { + if (defaultLocation && !this.toLocation && this.isReceipt) { 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 = { fromLocation: () => this.isIssue || this.isTransfer, toLocation: () => this.isReceipt || this.isTransfer, diff --git a/models/inventory/tests/testStockMovement.spec.ts b/models/inventory/tests/testStockMovement.spec.ts new file mode 100644 index 00000000..8de6a767 --- /dev/null +++ b/models/inventory/tests/testStockMovement.spec.ts @@ -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); diff --git a/models/inventory/types.ts b/models/inventory/types.ts index 6a846b1a..80ecb81d 100644 --- a/models/inventory/types.ts +++ b/models/inventory/types.ts @@ -9,6 +9,7 @@ export enum MovementType { 'MaterialIssue' = 'MaterialIssue', 'MaterialReceipt' = 'MaterialReceipt', 'MaterialTransfer' = 'MaterialTransfer', + 'Manufacture' = 'Manufacture', } export interface SMDetails { diff --git a/schemas/app/inventory/StockMovement.json b/schemas/app/inventory/StockMovement.json index b4d41684..99651250 100644 --- a/schemas/app/inventory/StockMovement.json +++ b/schemas/app/inventory/StockMovement.json @@ -35,6 +35,10 @@ { "value": "MaterialTransfer", "label": "Material Transfer" + }, + { + "value": "Manufacture", + "label": "Manufacture" } ], "required": true diff --git a/src/components/Controls/TableRow.vue b/src/components/Controls/TableRow.vue index 6824adfb..bef30b88 100644 --- a/src/components/Controls/TableRow.vue +++ b/src/components/Controls/TableRow.vue @@ -62,6 +62,7 @@ import { Doc } from 'fyo/model/doc'; import Row from 'src/components/Row.vue'; import { getErrorMessage } from 'src/utils'; +import { nextTick } from 'vue'; import Button from '../Button.vue'; import FormControl from './FormControl.vue'; @@ -102,15 +103,18 @@ export default { }, }, methods: { - onChange(df, value) { - if (value == null) { - return; - } + async onChange(df, value) { + const fieldname = df.fieldname; + this.errors[fieldname] = null; + const oldValue = this.row[fieldname]; - this.errors[df.fieldname] = null; - this.row.set(df.fieldname, value).catch((e) => { - this.errors[df.fieldname] = getErrorMessage(e, this.row); - }); + try { + await this.row.set(fieldname, value); + } catch (e) { + this.errors[fieldname] = getErrorMessage(e, this.row); + this.row[fieldname] = ''; + nextTick(() => (this.row[fieldname] = oldValue)); + } }, getErrorString() { return Object.values(this.errors).filter(Boolean).join(' ');