WIP Financial accounting module.

This commit is contained in:
Ahmed Bouhuolia
2020-01-25 23:34:08 +02:00
parent 488709088b
commit 77c67cc4cb
26 changed files with 1414 additions and 354 deletions

View File

@@ -181,6 +181,17 @@ factory.define('resource_field', 'resource_fields', async () => {
};
});
factory.define('resource_custom_field_metadata', 'resource_custom_fields_metadata', async () => {
const resource = await factory.create('resource');
return {
resource_id: resource.id,
resource_item_id: 1,
key: faker.lorem.words(),
value: faker.lorem.words(),
};
});
factory.define('view_role', 'view_roles', async () => {
const view = await factory.create('view');
const field = await factory.create('resource_field');

View File

@@ -3,7 +3,7 @@ exports.up = function (knex) {
return knex.schema.createTable('items', (table) => {
table.increments();
table.string('name');
table.integer('type_id').unsigned();
table.string('type');
table.decimal('cost_price').unsigned();
table.decimal('sell_price').unsigned();
table.string('currency_code', 3);

View File

@@ -10,6 +10,7 @@ exports.up = function (knex) {
table.boolean('active');
table.boolean('predefined');
table.boolean('columnable');
table.integer('index');
table.json('options');
table.integer('resource_id').unsigned().references('id').inTable('resources');
});

View File

@@ -9,6 +9,7 @@ exports.up = function(knex) {
table.integer('reference_id');
table.integer('account_id').unsigned().references('id').inTable('accounts');
table.string('note');
table.boolean('draft').defaultTo(false);
table.integer('user_id').unsigned().references('id').inTable('users');
table.date('date');
table.timestamps();

View File

@@ -9,6 +9,7 @@ exports.up = function(knex) {
table.integer('expense_account_id').unsigned().references('id').inTable('accounts');
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
table.string('reference');
table.boolean('published').defaultTo(false);
table.integer('user_id').unsigned().references('id').inTable('users');
table.date('date');
// table.timestamps();

View File

@@ -0,0 +1,14 @@
exports.up = function(knex) {
return knex.schema.createTable('resource_custom_fields_metadata', (table) => {
table.increments();
table.integer('resource_id').unsigned().references('id').inTable('resources');
table.integer('resource_item_id').unsigned();
table.string('key');
table.string('value');
})
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('resource_custom_fields_metadata');
};

View File

@@ -1,10 +1,13 @@
import express from 'express';
import { check, validationResult, oneOf } from 'express-validator';
import { difference } from 'lodash';
import moment from 'moment';
import asyncMiddleware from '../middleware/asyncMiddleware';
import jwtAuth from '@/http/middleware/jwtAuth';
import Account from '@/models/Account';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import ManualJournal from '@/models/ManualJournal';
export default {
/**
@@ -13,6 +16,8 @@ export default {
router() {
const router = express.Router();
router.use(jwtAuth);
router.post('/',
this.openingBalnace.validation,
asyncMiddleware(this.openingBalnace.handler));
@@ -27,11 +32,14 @@ export default {
*/
openingBalnace: {
validation: [
check('date').optional(),
check('note').optional().trim().escape(),
check('balance_adjustment_account').exists().isNumeric().toInt(),
check('accounts').isArray({ min: 1 }),
check('accounts.*.id').exists().isInt(),
oneOf([
check('accounts.*.debit').isNumeric().toFloat(),
check('accounts.*.credit').isNumeric().toFloat(),
check('accounts.*.debit').exists().isNumeric().toFloat(),
check('accounts.*.credit').exists().isNumeric().toFloat(),
]),
],
async handler(req, res) {
@@ -44,13 +52,19 @@ export default {
}
const { accounts } = req.body;
const { user } = req;
const form = { ...req.body };
const date = moment(form.date).format('YYYY-MM-DD');
const accountsIds = accounts.map((account) => account.id);
const accountsCollection = await Account.query()
.select(['id'])
.whereIn('id', accountsIds);
const storedAccounts = await Account.query()
.select(['id']).whereIn('id', accountsIds)
.withGraphFetched('type');
const accountsCollection = new Map(storedAccounts.map(i => [i.id, i]));
// Get the stored accounts Ids and difference with submit accounts.
const accountsStoredIds = accountsCollection.map((account) => account.id);
const accountsStoredIds = storedAccounts.map((account) => account.id);
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
const errorReasons = [];
@@ -58,36 +72,76 @@ export default {
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
}
if (form.balance_adjustment_account) {
const account = await Account.query().findById(form.balance_adjustment_account);
if (!account) {
errorReasons.push({ type: 'BALANCE.ADJUSTMENT.ACCOUNT.NOT.EXIST', code: 300 });
}
}
if (errorReasons.length > 0) {
return res.boom.badData(null, { errors: errorReasons });
}
const sharedJournalDetails = new JournalEntry({
referenceType: 'OpeningBalance',
referenceId: 1,
});
const journalEntries = new JournalPoster(sharedJournalDetails);
const journalEntries = new JournalPoster();
accounts.forEach((account) => {
const entry = new JournalEntry({
account: account.id,
accountNormal: account.type.normal,
});
const storedAccount = accountsCollection.get(account.id);
// Can't continue in case the stored account was not found.
if (!storedAccount) { return; }
const entryModel = new JournalEntry({
referenceType: 'OpeningBalance',
account: account.id,
accountNormal: storedAccount.type.normal,
userId: user.id,
});
if (account.credit) {
entry.credit = account.credit;
journalEntries.credit(entry);
entryModel.entry.credit = account.credit;
journalEntries.credit(entryModel);
} else if (account.debit) {
entry.debit = account.debit;
journalEntries.debit(entry);
entryModel.entry.debit = account.debit;
journalEntries.debit(entryModel);
}
});
// Calculates the credit and debit balance of stacked entries.
const trial = journalEntries.getTrialBalance();
if (trial.credit !== trial.debit) {
const entryModel = new JournalEntry({
referenceType: 'OpeningBalance',
account: form.balance_adjustment_account,
accountNormal: 'credit',
userId: user.id,
});
if (trial.credit > trial.debit) {
entryModel.entry.credit = Math.abs(trial.credit);
journalEntries.credit(entryModel);
} else if (trial.credit < trial.debit) {
entryModel.entry.debit = Math.abs(trial.debit);
journalEntries.debit(entryModel);
}
}
const manualJournal = await ManualJournal.query().insert({
amount: Math.max(trial.credit, trial.debit),
transaction_type: 'OpeningBalance',
date,
note: form.note,
user_id: user.id,
});
journalEntries.entries = journalEntries.entries.map((entry) => ({
...entry,
referenceId: manualJournal.id,
}));
await Promise.all([
journalEntries.saveEntries(),
journalEntries.saveBalance(),
]);
return res.status(200).send();
return res.status(200).send({ id: manualJournal.id });
},
},
};

View File

@@ -69,7 +69,6 @@ export default {
errors: [{ type: 'budget.not.found', code: 100 }],
});
}
const accountTypes = await AccountType.query().where('balance_sheet', true);
const [budgetEntries, accounts] = await Promise.all([
@@ -211,6 +210,7 @@ export default {
query('profit_loss').optional().isBoolean().toBoolean(),
query('page').optional().isNumeric().toInt(),
query('page_size').isNumeric().toInt(),
query('custom_view_id').optional().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
@@ -241,7 +241,7 @@ export default {
});
return res.status(200).send({
items: budgets.items,
})
});
},
},
};

View File

@@ -16,6 +16,7 @@ import JWTAuth from '@/http/middleware/jwtAuth';
import AccountTransaction from '@/models/AccountTransaction';
import View from '@/models/View';
import Resource from '../../models/Resource';
import ResourceCustomFieldRepository from '@/services/CustomFields/ResourceCustomFieldRepository';
export default {
/**
@@ -29,6 +30,10 @@ export default {
this.newExpense.validation,
asyncMiddleware(this.newExpense.handler));
router.post('/:id/publish',
this.publishExpense.validation,
asyncMiddleware(this.publishExpense.handler));
router.delete('/:id',
this.deleteExpense.validation,
asyncMiddleware(this.deleteExpense.handler));
@@ -60,6 +65,10 @@ export default {
check('amount').exists().isNumeric().toFloat(),
check('currency_code').optional(),
check('exchange_rate').optional().isNumeric().toFloat(),
check('publish').optional().isBoolean().toBoolean(),
check('custom_fields').optional().isArray({ min: 1 }),
check('custom_fields.*.key').exists().trim().escape(),
check('custom_fields.*.value').exists(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
@@ -71,6 +80,7 @@ export default {
}
const form = {
date: new Date(),
published: false,
...req.body,
};
// Convert the date to the general format.
@@ -83,16 +93,23 @@ export default {
if (!paymentAccount) {
errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 100 });
}
const expenseAccount = await Account.query()
.findById(form.expense_account_id).first();
const expenseAccount = await Account.query().findById(form.expense_account_id).first();
if (!expenseAccount) {
errorReasons.push({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200 });
}
const customFields = new ResourceCustomFieldRepository('Expense');
await customFields.load();
customFields.fillCustomFields(form.custom_fields);
if (customFields.validateExistCustomFields()) {
errorReasons.push({ type: 'CUSTOM.FIELDS.SLUGS.NOT.EXISTS', code: 400 });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const expenseTransaction = await Expense.query().insert({ ...form });
const expenseTransaction = await Expense.query().insertAndFetch({ ...form });
const journalEntries = new JournalPoster();
const creditEntry = new JournalEntry({
@@ -102,6 +119,7 @@ export default {
date: form.date,
account: expenseAccount.id,
accountNormal: 'debit',
draft: !form.published,
});
const debitEntry = new JournalEntry({
debit: form.amount,
@@ -110,11 +128,13 @@ export default {
date: form.date,
account: paymentAccount.id,
accountNormal: 'debit',
draft: !form.published,
});
journalEntries.credit(creditEntry);
journalEntries.debit(debitEntry);
await Promise.all([
customFields.saveCustomFields(expenseTransaction.id),
journalEntries.saveEntries(),
journalEntries.saveBalance(),
]);
@@ -221,6 +241,55 @@ export default {
},
},
/**
* Publish the given expense id.
*/
publishExpense: {
validation: [
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const errorReasons = [];
const expense = await Expense.query().findById(id);
if (!expense) {
errorReasons.push({ type: 'EXPENSE.NOT.FOUND', code: 100 });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
if (expense.published) {
errorReasons.push({ type: 'EXPENSE.ALREADY.PUBLISHED', code: 200 });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
await AccountTransaction.query()
.where('reference_id', expense.id)
.where('reference_type', 'Expense')
.patch({
draft: false,
});
await Expense.query()
.where('id', expense.id)
.update({ published: true });
return res.status(200).send();
},
},
/**
* Retrieve paginated expenses list.
*/
@@ -324,7 +393,7 @@ export default {
expenseEntriesCollect.reverseEntries();
await Promise.all([
expenseTransaction.delete(),
Expense.query().findById(expenseTransaction.id).delete(),
expenseEntriesCollect.deleteEntries(),
expenseEntriesCollect.saveBalance(),
]);

View File

@@ -16,7 +16,7 @@ export default {
router() {
const router = express.Router();
router.post('/resource/:resource_id',
router.post('/resource/:resource_name',
this.addNewField.validation,
asyncMiddleware(this.addNewField.handler));
@@ -28,11 +28,11 @@ export default {
this.changeStatus.validation,
asyncMiddleware(this.changeStatus.handler));
router.get('/:field_id',
asyncMiddleware(this.getField.handler));
// router.get('/:field_id',
// asyncMiddleware(this.getField.handler));
router.delete('/:field_id',
asyncMiddleware(this.deleteField.handler));
// router.delete('/:field_id',
// asyncMiddleware(this.deleteField.handler));
return router;
},
@@ -44,47 +44,44 @@ export default {
*/
addNewField: {
validation: [
param('resource_id').toInt(),
param('resource_name').exists().trim().escape(),
check('label').exists().escape().trim(),
check('data_type').exists().isIn(TYPES),
check('help_text').optional(),
check('default').optional(),
check('options').optional().isArray(),
check('options.*.key').exists().isNumeric().toInt(),
check('options.*.value').exists(),
],
async handler(req, res) {
const { resource_id: resourceId } = req.params;
const { resource_name: resourceName } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
code: 'validation_error', ...validationErrors,
});
}
const resource = await Resource.where('id', resourceId).fetch();
const resource = await Resource.query().where('name', resourceName).first();
if (!resource) {
return res.boom.notFound(null, {
errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }],
});
}
const form = { options: [], ...req.body };
const choices = form.options.map((option) => ({ key: option.key, value: option.value }));
const { label, data_type: dataType, help_text: helpText } = req.body;
const { default: defaultValue, options } = req.body;
const choices = options.map((option, index) => ({ key: index + 1, value: option }));
const field = ResourceField.forge({
data_type: dataType,
label_name: label,
help_text: helpText,
default: defaultValue,
const storedResource = await ResourceField.query().insertAndFetch({
data_type: form.data_type,
label_name: form.label,
help_text: form.help_text,
default: form.default,
resource_id: resource.id,
options: choices,
index: -1,
});
await field.save();
return res.status(200).send();
return res.status(200).send({ id: storedResource.id });
},
},
@@ -93,12 +90,14 @@ export default {
*/
editField: {
validation: [
param('field_id').toInt(),
param('field_id').exists().isNumeric().toInt(),
check('label').exists().escape().trim(),
check('data_type').exists(),
check('data_type').exists().isIn(TYPES),
check('help_text').optional(),
check('default').optional(),
check('options').optional().isArray(),
check('options.*.key').exists().isNumeric().toInt(),
check('options.*.value').exists(),
],
async handler(req, res) {
const { field_id: fieldId } = req.params;
@@ -106,52 +105,28 @@ export default {
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
code: 'validation_error', ...validationErrors,
});
}
const field = await ResourceField.where('id', fieldId).fetch();
const field = await ResourceField.query().findById(fieldId);
if (!field) {
return res.boom.notFound(null, {
errors: [{ type: 'FIELD_NOT_FOUND', code: 100 }],
});
}
// Sets the default value of optional fields.
const form = { options: [], ...req.body };
const choices = form.options.map((option) => ({ key: option.key, value: option.value }));
const { label, data_type: dataType, help_text: helpText } = form;
const { default: defaultValue, options } = form;
const storedFieldOptions = field.attributes.options || [];
let lastChoiceIndex = 0;
storedFieldOptions.forEach((option) => {
const key = parseInt(option.key, 10);
if (key > lastChoiceIndex) {
lastChoiceIndex = key;
}
});
const savedOptionKeys = options.filter((op) => typeof op === 'object');
const notSavedOptionsKeys = options.filter((op) => typeof op !== 'object');
const choices = [
...savedOptionKeys,
...notSavedOptionsKeys.map((option) => {
lastChoiceIndex += 1;
return { key: lastChoiceIndex, value: option };
}),
];
await field.save({
data_type: dataType,
label_name: label,
help_text: helpText,
default: defaultValue,
await ResourceField.query().findById(field.id).update({
data_type: form.data_type,
label_name: form.label,
help_text: form.help_text,
default: form.default,
options: choices,
});
return res.status(200).send({ id: field.get('id') });
return res.status(200).send({ id: field.id });
},
},
@@ -162,11 +137,25 @@ export default {
*/
fieldsList: {
validation: [
param('resource_id').toInt(),
param('resource_name').toInt(),
],
async handler(req, res) {
const { resource_id: resourceId } = req.params;
const fields = await ResourceField.where('resource_id', resourceId).fetchAll();
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { resource_name: resourceName } = req.params;
const resource = await Resource.query().where('name', resourceName).first();
if (!resource) {
return res.boom.notFound(null, {
errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }],
});
}
const fields = await ResourceField.where('resource_id', resource.id).fetchAll();
return res.status(200).send({ fields: fields.toJSON() });
},
@@ -182,7 +171,7 @@ export default {
],
async handler(req, res) {
const { field_id: fieldId } = req.params;
const field = await ResourceField.where('id', fieldId).fetch();
const field = await ResourceField.query().findById(fieldId);
if (!field) {
return res.boom.notFound(null, {
@@ -191,9 +180,9 @@ export default {
}
const { active } = req.body;
await field.save({ active });
await ResourceField.query().findById(field.id).patch({ active });
return res.status(200).send({ id: field.get('id') });
return res.status(200).send({ id: field.id });
},
},

View File

@@ -50,13 +50,9 @@ export default {
this.profitLossSheet.validation,
asyncMiddleware(this.profitLossSheet.handler));
// router.get('/cash_flow_statement',
// this.cashFlowStatement.validation,
// asyncMiddleware(this.cashFlowStatement.handler));
// router.get('/badget_verses_actual',
// this.badgetVersesActuals.validation,
// asyncMiddleware(this.badgetVersesActuals.handler));
router.get('/cash_flow_statement',
this.cashFlowStatement.validation,
asyncMiddleware(this.cashFlowStatement.handler));
return router;
},
@@ -510,17 +506,12 @@ export default {
],
async handler(req, res) {
return res.status(200).send();
},
},
badgetVersesActuals: {
validation: [
],
async handler(req, res) {
return res.status(200).send({
meta: {},
operating: [],
financing: [],
investing: [],
});
},
},
}

View File

@@ -19,18 +19,18 @@ export default {
router.use(jwtAuth);
// router.post('/:id',
// this.editItem.validation,
// asyncMiddleware(this.editCategory.handler));
router.post('/:id',
this.editItem.validation,
asyncMiddleware(this.editItem.handler));
router.post('/',
permit('create'),
// permit('create'),
this.newItem.validation,
asyncMiddleware(this.newItem.handler));
// router.delete('/:id',
// this.deleteItem.validation,
// asyncMiddleware(this.deleteItem.handler));
router.delete('/:id',
this.deleteItem.validation,
asyncMiddleware(this.deleteItem.handler));
// router.get('/:id',
// this.getCategory.validation,
@@ -49,14 +49,14 @@ export default {
newItem: {
validation: [
check('name').exists(),
check('type_id').exists().isInt(),
check('buy_price').exists().isNumeric(),
check('type').exists().trim().escape().isIn(['service', 'product']),
check('cost_price').exists().isNumeric(),
check('sell_price').exists().isNumeric(),
check('cost_account_id').exists().isInt(),
check('sell_account_id').exists().isInt(),
check('category_id').optional().isInt(),
check('custom_fields').isArray({ min: 1 }),
check('custom_fields').optional().isArray({ min: 1 }),
check('custom_fields.*.key').exists().isNumeric().toInt(),
check('custom_fields.*.value').exists(),
@@ -70,13 +70,16 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const form = { ...req.body };
const form = {
custom_fields: [],
...req.body,
};
const errorReasons = [];
const costAccountPromise = Account.where('id', form.cost_account_id).fetch();
const sellAccountPromise = Account.where('id', form.sell_account_id).fetch();
const costAccountPromise = Account.query().findById(form.cost_account_id);
const sellAccountPromise = Account.query().findById(form.sell_account_id);
const itemCategoryPromise = (form.category_id)
? ItemCategory.where('id', form.category_id).fetch() : null;
? ItemCategory.query().findById(form.category_id) : null;
// Validate the custom fields key and value type.
if (form.custom_fields.length > 0) {
@@ -98,6 +101,76 @@ export default {
errorReasons.push({ type: 'FIELD_KEY_NOT_FOUND', code: 150, fields: notFoundFields });
}
}
const [costAccount, sellAccount, itemCategory] = await Promise.all([
costAccountPromise, sellAccountPromise, itemCategoryPromise,
]);
if (!costAccount) {
errorReasons.push({ type: 'COST_ACCOUNT_NOT_FOUND', code: 100 });
}
if (!sellAccount) {
errorReasons.push({ type: 'SELL_ACCOUNT_NOT_FOUND', code: 120 });
}
if (!itemCategory && form.category_id) {
errorReasons.push({ type: 'ITEM_CATEGORY_NOT_FOUND', code: 140 });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
const item = await Item.query().insertAndFetch({
name: form.name,
type: form.type,
cost_price: form.cost_price,
sell_price: form.sell_price,
sell_account_id: form.sell_account_id,
cost_account_id: form.cost_account_id,
currency_code: form.currency_code,
note: form.note,
});
return res.status(200).send({ id: item.id });
},
},
/**
* Edit the given item.
*/
editItem: {
validation: [
check('name').exists(),
check('type').exists().trim().escape().isIn(['product', 'service']),
check('cost_price').exists().isNumeric(),
check('sell_price').exists().isNumeric(),
check('cost_account_id').exists().isInt(),
check('sell_account_id').exists().isInt(),
check('category_id').optional().isInt(),
check('note').optional(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const form = {
custom_fields: [],
...req.body,
};
const item = await Item.query().findById(id);
if (!item) {
return res.boom.notFound(null, { errors: [
{ type: 'ITEM.NOT.FOUND', code: 100 },
]});
}
const errorReasons = [];
const costAccountPromise = Account.query().findById(form.cost_account_id);
const sellAccountPromise = Account.query().findById(form.sell_account_id);
const itemCategoryPromise = (form.category_id)
? ItemCategory.query().findById(form.category_id) : null;
const [costAccount, sellAccount, itemCategory] = await Promise.all([
costAccountPromise, sellAccountPromise, itemCategoryPromise,
@@ -114,41 +187,19 @@ export default {
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
const item = Item.forge({
name: req.body.name,
type_id: 1,
buy_price: req.body.buy_price,
sell_price: req.body.sell_price,
currency_code: req.body.currency_code,
note: req.body.note,
const updatedItem = await Item.query().findById(id).patch({
name: form.name,
type: form.type,
cost_price: form.cost_price,
sell_price: form.sell_price,
currency_code: form.currency_code,
sell_account_id: form.sell_account_id,
cost_account_id: form.cost_account_id,
category_id: form.category_id,
note: form.note,
});
await item.save();
return res.status(200).send();
},
},
/**
* Edit the given item.
*/
editItem: {
validation: [],
async handler(req, res) {
const { id } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const item = await Item.where('id', id).fetch();
if (!item) {
return res.boom.notFound();
}
return res.status(200).send();
return res.status(200).send({ id: updatedItem.id });
},
},
@@ -159,7 +210,7 @@ export default {
validation: [],
async handler(req, res) {
const { id } = req.params;
const item = await Item.where('id', id).fetch();
const item = await Item.query().findById(id);
if (!item) {
return res.boom.notFound(null, {
@@ -167,7 +218,9 @@ export default {
});
}
await item.destroy();
// Delete the fucking the given item id.
await Item.query().findById(item.id).delete();
return res.status(200).send();
},
},

View File

@@ -28,7 +28,7 @@ export default (app) => {
app.use('/api/roles', Roles.router());
app.use('/api/accounts', Accounts.router());
app.use('/api/accounting', Accounting.router());
app.use('/api/accounts_opeing_balance', AccountOpeningBalance.router());
app.use('/api/accounts_opening_balances', AccountOpeningBalance.router());
app.use('/api/views', Views.router());
app.use('/api/fields', CustomFields.router());
app.use('/api/items', Items.router());

View File

@@ -0,0 +1,10 @@
import BaseModel from '@/models/Model';
export default class ManualJournal extends BaseModel {
/**
* Table name.
*/
static get tableName() {
return 'manual_journals';
}
}

View File

@@ -8,7 +8,10 @@ export default class ModelBase extends Model {
static query(...args) {
return super.query(...args).runAfter((result) => {
return this.collection.from(result);
if (Array.isArray(result)) {
return this.collection.from(result);
}
return result;
});
}
}

View File

@@ -11,6 +11,21 @@ export default class ResourceField extends BaseModel {
return 'resource_fields';
}
static get jsonAttributes() {
return ['options'];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
whereNotPredefined(query) {
query.whereNot('predefined', true);
},
};
}
/**
* Timestamp columns.
*/

View File

@@ -0,0 +1,39 @@
import { Model } from 'objection';
import path from 'path';
import BaseModel from '@/models/Model';
import MetableCollection from '@/lib/Metable/MetableCollection';
export default class ResourceFieldMetadata extends BaseModel {
/**
* Table name.
*/
static get tableName() {
return 'resource_custom_fields_metadata';
}
/**
* Override the resource field metadata collection.
*/
static get collection() {
return MetableCollection;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
return {
/**
* Resource field may belongs to resource model.
*/
resource: {
relation: Model.BelongsToOneRelation,
modelBase: path.join(__dirname, 'Resource'),
join: {
from: 'resource_fields.resource_id',
to: 'resources.id',
},
},
};
}
}

View File

@@ -0,0 +1,123 @@
import Resource from '@/models/Resource';
import ResourceField from '@/models/ResourceField';
import ResourceFieldMetadata from '@/models/ResourceFieldMetadata';
import ModelBase from '@/models/Model';
export default class ResourceCustomFieldRepository {
constructor(model) {
if (typeof model === 'function') {
this.resourceName = model.name;
} else if (typeof model === 'string') {
this.resourceName = model;
}
// Custom fields of the given resource.
this.customFields = [];
this.filledCustomFields = [];
// metadata of custom fields of the given resource.
this.metadata = {};
this.resource = {};
}
/**
* Fetches metadata of custom fields of the given resource.
* @param {Integer} id - Resource item id.
*/
async fetchCustomFieldsMetadata(id) {
if (typeof id === 'undefined') {
throw new Error('Please define the resource item id.');
}
if (!this.resource) {
throw new Error('Target resource model is not found.');
}
const metadata = await ResourceFieldMetadata.query()
.where('resource_id', this.resource.id)
.where('resource_item_id', id);
this.metadata[id] = metadata;
}
/**
* Load resource.
*/
async loadResource() {
const resource = await Resource.query().where('name', this.resourceName).first();
if (!resource) {
throw new Error('There is no stored resource in the storage with the given model name.');
}
this.setResource(resource);
}
/**
* Load metadata of the resource.
*/
async loadResourceCustomFields() {
if (typeof this.resource.id === 'undefined') {
throw new Error('Please fetch resource details before fetch custom fields of the resource.');
}
const customFields = await ResourceField.query()
.where('resource_id', this.resource.id)
.modify('whereNotPredefined');
this.setResourceCustomFields(customFields);
}
/**
* Sets resource model.
* @param {Resource} resource -
*/
setResource(resource) {
this.resource = resource;
}
/**
* Sets resource custom fields collection.
* @param {Array} customFields -
*/
setResourceCustomFields(customFields) {
this.customFields = customFields;
}
/**
* Retrieve metadata of the resource custom fields.
* @param {Integer} itemId -
*/
getMetadata(itemId) {
return this.metadata[itemId] || this.metadata;
}
fillCustomFields(id, attributes) {
if (typeof this.filledCustomFields[id] === 'undefined') {
this.filledCustomFields[id] = [];
}
attributes.forEach((attr) => {
this.filledCustomFields[id].push(attr);
this.fieldsMetadata[id].setMeta(attr.key, attr.value);
});
}
saveCustomFields(id) {
this.fieldsMetadata.saveMeta();
}
/**
* Validates the exist custom fields.
*/
validateExistCustomFields() {
}
toArray() {
return this.fieldsMetadata.toArray();
}
async load() {
await Promise.all([
this.loadResource(),
this.loadResourceCustomFields(),
]);
}
}

View File

@@ -3,4 +3,4 @@ import { extendMoment } from 'moment-range';
const moment = extendMoment(Moment);
export default moment;
export default moment;