mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
feat: Manual journals.
This commit is contained in:
@@ -66,9 +66,11 @@ factory.define('manual_journal', 'manual_journals', async () => {
|
|||||||
const user = await factory.create('user');
|
const user = await factory.create('user');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reference: faker.random.number(),
|
journal_number: faker.random.number(),
|
||||||
|
transaction_type: '',
|
||||||
amount: faker.random.number(),
|
amount: faker.random.number(),
|
||||||
// date: faker.random,
|
date: faker.date.future,
|
||||||
|
status: 1,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
exports.up = function(knex) {
|
exports.up = function(knex) {
|
||||||
return knex.schema.createTable('manual_journals', (table) => {
|
return knex.schema.createTable('manual_journals', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.string('reference');
|
table.string('journal_number');
|
||||||
table.string('transaction_type');
|
table.string('transaction_type');
|
||||||
table.decimal('amount');
|
table.decimal('amount');
|
||||||
table.date('date');
|
table.date('date');
|
||||||
|
table.boolean('status').defaultTo(false);
|
||||||
table.string('note');
|
table.string('note');
|
||||||
table.integer('user_id').unsigned();
|
table.integer('user_id').unsigned();
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ exports.seed = (knex) => {
|
|||||||
{ id: 1, name: 'accounts' },
|
{ id: 1, name: 'accounts' },
|
||||||
{ id: 2, name: 'items' },
|
{ id: 2, name: 'items' },
|
||||||
{ id: 3, name: 'expenses' },
|
{ id: 3, name: 'expenses' },
|
||||||
|
{ id: 4, name: 'manual_journals' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import JWTAuth from '@/http/middleware/jwtAuth';
|
|||||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||||
import ManualJournal from '@/models/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 {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +24,10 @@ export default {
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(JWTAuth);
|
router.use(JWTAuth);
|
||||||
|
|
||||||
|
router.get('/manual-journals',
|
||||||
|
this.manualJournals.validation,
|
||||||
|
asyncMiddleware(this.manualJournals.handler));
|
||||||
|
|
||||||
router.post('/make-journal-entries',
|
router.post('/make-journal-entries',
|
||||||
this.makeJournalEntries.validation,
|
this.makeJournalEntries.validation,
|
||||||
asyncMiddleware(this.makeJournalEntries.handler));
|
asyncMiddleware(this.makeJournalEntries.handler));
|
||||||
@@ -32,6 +43,83 @@ export default {
|
|||||||
return router;
|
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.
|
* Make journal entrires.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 () => {
|
describe('route: `/accounting/quick-journal-entries`', async () => {
|
||||||
it('Shoud `credit_account_id` be required', () => {
|
it('Shoud `credit_account_id` be required', () => {
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
it('Should response unauthorzied in case the user was not authorized.', async () => {
|
||||||
const res = await request()
|
const res = await request()
|
||||||
.get('/api/financial_statements/balance_sheet')
|
.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');
|
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()
|
const res = await request()
|
||||||
.get('/api/financial_statements/balance_sheet')
|
.get('/api/financial_statements/balance_sheet')
|
||||||
.set('x-access-token', loginRes.body.token)
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
|||||||
Reference in New Issue
Block a user