diff --git a/server/src/database/factories/index.js b/server/src/database/factories/index.js index c4332cb8e..6981b664f 100644 --- a/server/src/database/factories/index.js +++ b/server/src/database/factories/index.js @@ -66,9 +66,11 @@ factory.define('manual_journal', 'manual_journals', async () => { const user = await factory.create('user'); return { - reference: faker.random.number(), + journal_number: faker.random.number(), + transaction_type: '', amount: faker.random.number(), - // date: faker.random, + date: faker.date.future, + status: 1, user_id: user.id, }; }); diff --git a/server/src/database/migrations/20200105195823_create_manual_journals_table.js b/server/src/database/migrations/20200105195823_create_manual_journals_table.js index 5dee2b6e4..176fd50b3 100644 --- a/server/src/database/migrations/20200105195823_create_manual_journals_table.js +++ b/server/src/database/migrations/20200105195823_create_manual_journals_table.js @@ -2,10 +2,11 @@ exports.up = function(knex) { return knex.schema.createTable('manual_journals', (table) => { table.increments(); - table.string('reference'); + table.string('journal_number'); table.string('transaction_type'); table.decimal('amount'); table.date('date'); + table.boolean('status').defaultTo(false); table.string('note'); table.integer('user_id').unsigned(); table.timestamps(); diff --git a/server/src/database/seeds/seed_resources.js b/server/src/database/seeds/seed_resources.js index 7d10bf485..0c99696be 100644 --- a/server/src/database/seeds/seed_resources.js +++ b/server/src/database/seeds/seed_resources.js @@ -8,6 +8,7 @@ exports.seed = (knex) => { { id: 1, name: 'accounts' }, { id: 2, name: 'items' }, { id: 3, name: 'expenses' }, + { id: 4, name: 'manual_journals' }, ]); }); }; diff --git a/server/src/http/controllers/Accounting.js b/server/src/http/controllers/Accounting.js index bf611bdc7..c808a6f40 100644 --- a/server/src/http/controllers/Accounting.js +++ b/server/src/http/controllers/Accounting.js @@ -8,6 +8,13 @@ import JWTAuth from '@/http/middleware/jwtAuth'; import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalEntry from '@/services/Accounting/JournalEntry'; import ManualJournal from '@/models/JournalEntry'; +import Resource from '@/models/Resource'; +import View from '@/models/View'; +import { + mapViewRolesToConditionals, + validateViewRoles, +} from '@/lib/ViewRolesBuilder'; +import FilterRoles from '@/lib/FilterRoles'; export default { /** @@ -17,6 +24,10 @@ export default { const router = express.Router(); router.use(JWTAuth); + router.get('/manual-journals', + this.manualJournals.validation, + asyncMiddleware(this.manualJournals.handler)); + router.post('/make-journal-entries', this.makeJournalEntries.validation, asyncMiddleware(this.makeJournalEntries.handler)); @@ -32,6 +43,83 @@ export default { return router; }, + /** + * Retrieve manual journals, + */ + manualJournals: { + validation: [ + query('custom_view_id').optional().isNumeric().toInt(), + query('stringified_filter_roles').optional().isJSON(), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const filter = { + filter_roles: [], + ...req.query, + }; + if (filter.stringified_filter_roles) { + filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + } + + const errorReasons = []; + const viewConditionals = []; + const manualJournalsResource = await Resource.query() + .where('name', 'manual_journals') + .withGraphFetched('fields') + .first(); + + if (!manualJournalsResource) { + return res.status(400).send({ + errors: [{ type: 'MANUAL_JOURNALS.RESOURCE.NOT.FOUND', code: 200 }], + }); + } + + const view = await View.query().onBuild((builder) => { + if (filter.custom_view_id) { + builder.where('id', filter.custom_view_id); + } else { + builder.where('favourite', true); + } + builder.where('resource_id', manualJournalsResource.id); + builder.withGraphFetched('roles.field'); + builder.withGraphFetched('columns'); + builder.first(); + }); + + if (view && view.roles.length > 0) { + viewConditionals.push( + ...mapViewRolesToConditionals(view.roles), + ); + if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) { + errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }); + } + } + // Validate the accounts resource fields. + const filterRoles = new FilterRoles(Resource.tableName, + filter.filter_roles.map((role) => ({ ...role, columnKey: role.fieldKey })), + manualJournalsResource.fields); + + if (filterRoles.validateFilterRoles().length > 0) { + errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 }); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + // Manual journals. + const manualJournals = await ManualJournal.query(); + + return res.status(200).send({ + manualJournals, + }); + }, + }, + /** * Make journal entrires. */ diff --git a/server/tests/routes/accounting.test.js b/server/tests/routes/accounting.test.js index 1ba7c7587..4068afda3 100644 --- a/server/tests/routes/accounting.test.js +++ b/server/tests/routes/accounting.test.js @@ -245,6 +245,36 @@ describe('routes: `/accounting`', () => { }); }); + + describe.only('route: `accounting/manual-journals`', async () => { + + it('Should retrieve manual journals resource not found.', async () => { + const res = await request() + .get('/api/accounting/manual-journals') + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equal(400); + expect(res.body.errors[0].type).equals('MANUAL_JOURNALS.RESOURCE.NOT.FOUND'); + expect(res.body.errors[0].code).equals(200); + }); + + it.only('Should retrieve all manual journals with pagination meta.', async () => { + const resource = await create('resource', { name: 'manual_journals' }); + const manualJournal1 = await create('manual_journal'); + const manualJournal2 = await create('manual_journal'); + + const res = await request() + .get('/api/accounting/manual-journals') + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(200); + expect(res.body.manualJournals).to.be.a('array'); + expect(res.body.manualJournals.length).equals(2); + }); + }); + describe('route: `/accounting/quick-journal-entries`', async () => { it('Shoud `credit_account_id` be required', () => { diff --git a/server/tests/routes/financial_statements.test.js b/server/tests/routes/financial_statements.test.js index 7928b8d49..c1d5afae2 100644 --- a/server/tests/routes/financial_statements.test.js +++ b/server/tests/routes/financial_statements.test.js @@ -363,7 +363,7 @@ describe('routes: `/financial_statements`', () => { }); }); - describe.only('routes: `financial_statements/balance_sheet`', () => { + describe('routes: `financial_statements/balance_sheet`', () => { it('Should response unauthorzied in case the user was not authorized.', async () => { const res = await request() .get('/api/financial_statements/balance_sheet') @@ -406,7 +406,7 @@ describe('routes: `/financial_statements`', () => { expect(res.body.balance_sheet.liabilities_equity.accounts).to.be.a('array'); }); - it.only('Should retrieve assets/liabilities total balance between the given date range.', async () => { + it('Should retrieve assets/liabilities total balance between the given date range.', async () => { const res = await request() .get('/api/financial_statements/balance_sheet') .set('x-access-token', loginRes.body.token)