credit || debit,
+ then: Yup.number().required(),
+ }),
+ note: Yup.string().nullable(),
}),
)
});
- const defaultEntry = {
+ const defaultEntry = useMemo(() => ({
account_id: null,
credit: null,
debit: null,
- note: '',
- };
+ note: '',
+ }), []);
const formik = useFormik({
enableReinitialize: true,
validationSchema,
initialValues: {
- reference: '',
+ journal_number: '',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
+ reference: '',
entries: [
defaultEntry,
defaultEntry,
defaultEntry,
defaultEntry,
+ defaultEntry,
+ defaultEntry,
],
},
onSubmit: (values) => {
const form = values.entries.filter((entry) => (
(entry.credit || entry.debit)
));
- makeJournalEntries(values).then((response) => {
- AppToaster.show({
- message: 'the_account_has_been_submit',
- });
- }).catch((error) => {
+ const getTotal = (type = 'credit') => {
+ return form.reduce((total, item) => {
+ return item[type] ? item[type] + total : total;
+ }, 0);
+ }
+ const totalCredit = getTotal('credit');
+ const totalDebit = getTotal('debit');
- });
+ if (totalCredit !== totalDebit) {
+ AppToaster.show({
+ message: 'credit_and_debit_not_equal',
+ });
+ return;
+ }
+
+ makeJournalEntries({ ...values, entries: form })
+ .then((response) => {
+ AppToaster.show({
+ message: 'manual_journal_has_been_submit',
+ });
+ }).catch((error) => {
+
+ });
},
});
diff --git a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesTable.js b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesTable.js
index 5dde6a63f..e64a3aaf8 100644
--- a/client/src/containers/Dashboard/Accounting/MakeJournalEntriesTable.js
+++ b/client/src/containers/Dashboard/Accounting/MakeJournalEntriesTable.js
@@ -1,9 +1,9 @@
import React, {useState, useMemo, useCallback} from 'react';
-import DataTable from 'components/DataTable';
import {
Button,
Intent,
} from '@blueprintjs/core';
+import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import AccountsConnect from 'connectors/Accounts.connector.js';
import {compose, formattedAmount} from 'utils';
@@ -11,7 +11,8 @@ import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
-} from 'comp}onents/DataTableCells';
+} from 'components/DataTableCells';
+import { omit } from 'lodash';
// Actions cell renderer.
const ActionsCellRenderer = ({
@@ -50,7 +51,7 @@ const TotalAccountCellRenderer = (chainedComponent) => (props) => {
// Total credit/debit cell renderer.
const TotalCreditDebitCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === (props.row.index + 2)) {
- const total = props.data.reduce((total, entry) => {
+ const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
@@ -82,18 +83,20 @@ function MakeJournalEntriesTable({
// Handles update datatable data.
const handleUpdateData = useCallback((rowIndex, columnId, value) => {
- setRow((old) =>
- old.map((row, index) => {
- if (index === rowIndex) {
- return {
- ...old[rowIndex],
- [columnId]: value,
- }
- }
- return row
- })
- )
- }, []);
+ const newRows = rows.map((row, index) => {
+ if (index === rowIndex) {
+ return {
+ ...rows[rowIndex],
+ [columnId]: value,
+ };
+ }
+ return { ...row };
+ });
+ setRow(newRows);
+ formik.setFieldValue('entries', newRows.map(row => ({
+ ...omit(row, ['rowType']),
+ })));
+ }, [rows, formik]);
// Handles click remove datatable row.
const handleRemoveRow = useCallback((rowIndex) => {
@@ -119,6 +122,7 @@ function MakeJournalEntriesTable({
},
{
Header: 'Account',
+ id: 'account_id',
accessor: 'account',
Cell: TotalAccountCellRenderer(AccountsListFieldCell),
className: "account",
@@ -174,6 +178,7 @@ function MakeJournalEntriesTable({
const rowClassNames = useCallback((row) => ({
'row--total': rows.length === (row.index + 2),
}), [rows]);
+
return (
diff --git a/client/src/style/objects/form.scss b/client/src/style/objects/form.scss
index 763488918..a5d4bfa99 100644
--- a/client/src/style/objects/form.scss
+++ b/client/src/style/objects/form.scss
@@ -98,6 +98,12 @@ label{
&.bp3-fill{
width: 100%;
}
+
+ &.bp3-intent-danger{
+ .bp3-button:not(.bp3-minimal){
+ border-color: #db3737;
+ }
+ }
}
diff --git a/client/src/style/pages/make-journal-entries.scss b/client/src/style/pages/make-journal-entries.scss
index 241044e82..a135bc1a3 100644
--- a/client/src/style/pages/make-journal-entries.scss
+++ b/client/src/style/pages/make-journal-entries.scss
@@ -1,5 +1,6 @@
.make-journal-entries{
+ padding-bottom: 80px;
&__header{
padding: 25px 27px 20px;
@@ -74,6 +75,14 @@
padding-right: 8px;
}
+ .form-group--select-list{
+ &.bp3-intent-danger{
+ .bp3-button:not(.bp3-minimal){
+ border-color: #efa8a8;
+ }
+ }
+ }
+
&:last-of-type{
.td{
diff --git a/client/src/utils.js b/client/src/utils.js
index 77ade104c..2aac703cb 100644
--- a/client/src/utils.js
+++ b/client/src/utils.js
@@ -1,5 +1,8 @@
import moment from 'moment';
import _ from 'lodash';
+import Currency from 'js-money/lib/currency';
+import accounting from 'accounting';
+
export function removeEmptyFromObject(obj) {
obj = Object.assign({}, obj);
@@ -131,4 +134,12 @@ export const defaultExpanderReducer = (tableRows, level) => {
};
walker(tableRows);
return expended;
+}
+
+
+export function formattedAmount(cents, currency) {
+ const { symbol, decimal_digits: precision } = Currency[currency];
+ const amount = cents / Math.pow(10, precision);
+
+ return accounting.formatMoney(amount, { symbol, precision });
}
\ No newline at end of file
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 176fd50b3..93cc9cbdf 100644
--- a/server/src/database/migrations/20200105195823_create_manual_journals_table.js
+++ b/server/src/database/migrations/20200105195823_create_manual_journals_table.js
@@ -3,11 +3,12 @@ exports.up = function(knex) {
return knex.schema.createTable('manual_journals', (table) => {
table.increments();
table.string('journal_number');
+ table.string('reference');
table.string('transaction_type');
table.decimal('amount');
table.date('date');
table.boolean('status').defaultTo(false);
- table.string('note');
+ table.string('description');
table.integer('user_id').unsigned();
table.timestamps();
});
diff --git a/server/src/http/controllers/Accounting.js b/server/src/http/controllers/Accounting.js
index c808a6f40..488ec48e9 100644
--- a/server/src/http/controllers/Accounting.js
+++ b/server/src/http/controllers/Accounting.js
@@ -126,9 +126,11 @@ export default {
makeJournalEntries: {
validation: [
check('date').isISO8601(),
- check('reference').exists(),
- check('memo').optional().trim().escape(),
- check('entries').isArray({ min: 1 }),
+ check('journal_number').exists().trim().escape(),
+ check('transaction_type').optional({ nullable: true }).trim().escape(),
+ check('reference').optional({ nullable: true }),
+ check('description').optional().trim().escape(),
+ check('entries').isArray({ min: 2 }),
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
check('entries.*.account_id').isNumeric().toInt(),
@@ -144,6 +146,8 @@ export default {
}
const form = {
date: new Date(),
+ transaction_type: 'journal',
+ reference: '',
...req.body,
};
@@ -183,10 +187,11 @@ export default {
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
}
- const journalReference = await ManualJournal.query().where('reference', form.reference);
+ const journalNumber = await ManualJournal.query()
+ .where('journal_number', form.journal_number);
- if (journalReference.length > 0) {
- errorReasons.push({ type: 'REFERENCE.ALREADY.EXISTS', code: 300 });
+ if (journalNumber.length > 0) {
+ errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
@@ -196,9 +201,10 @@ export default {
const manualJournal = await ManualJournal.query().insert({
reference: form.reference,
transaction_type: 'Journal',
+ journal_number: form.journal_number,
amount: totalCredit,
date: formattedDate,
- note: form.memo,
+ description: form.description,
user_id: user.id,
});
const journalPoster = new JournalPoster();
@@ -210,7 +216,8 @@ export default {
debit: entry.debit,
credit: entry.credit,
account: account.id,
- transactionType: 'Journal',
+ referenceType: 'Journal',
+ referenceId: manualJournal.id,
accountNormal: account.type.normal,
note: entry.note,
date: formattedDate,
diff --git a/server/tests/routes/accounting.test.js b/server/tests/routes/accounting.test.js
index 4068afda3..467908d0b 100644
--- a/server/tests/routes/accounting.test.js
+++ b/server/tests/routes/accounting.test.js
@@ -18,7 +18,7 @@ describe('routes: `/accounting`', () => {
loginRes = null;
});
- describe('route: `/accounting/make-journal-entries`', async () => {
+ describe.only('route: `/accounting/make-journal-entries`', async () => {
it('Should sumation of credit or debit does not equal zero.', async () => {
const account = await create('account');
const res = await request()
@@ -26,6 +26,7 @@ describe('routes: `/accounting`', () => {
.set('x-access-token', loginRes.body.token)
.send({
date: new Date().toISOString(),
+ journal_number: '123',
reference: 'ASC',
entries: [
{
@@ -54,7 +55,7 @@ describe('routes: `/accounting`', () => {
.set('x-access-token', loginRes.body.token)
.send({
date: new Date().toISOString(),
- reference: 'ASC',
+ journal_number: '123',
entries: [
{
credit: 1000,
@@ -85,7 +86,7 @@ describe('routes: `/accounting`', () => {
.set('x-access-token', loginRes.body.token)
.send({
date: new Date().toISOString(),
- reference: manualJournal.reference,
+ journal_number: manualJournal.journalNumber,
entries: [
{
credit: 1000,
@@ -102,18 +103,18 @@ describe('routes: `/accounting`', () => {
expect(res.status).equals(400);
expect(res.body.errors).include.something.that.deep.equal({
- type: 'REFERENCE.ALREADY.EXISTS',
+ type: 'JOURNAL.NUMBER.ALREADY.EXISTS',
code: 300,
});
});
- it('Should response error in case account id not exists.', async () => {
+ it('Should response error in case account id not exists in one of the given entries.', async () => {
const res = await request()
.post('/api/accounting/make-journal-entries')
.set('x-access-token', loginRes.body.token)
.send({
date: new Date().toISOString(),
- reference: '1000',
+ journal_number: '123',
entries: [
{
credit: 1000,
@@ -144,7 +145,7 @@ describe('routes: `/accounting`', () => {
.set('x-access-token', loginRes.body.token)
.send({
date: new Date().toISOString(),
- reference: '1000',
+ journal_number: '1000',
entries: [
{
credit: null,
@@ -166,7 +167,7 @@ describe('routes: `/accounting`', () => {
});
});
- it('Should store manual journal transaction to the storage.', async () => {
+ it.only('Should store manual journal transaction to the storage.', async () => {
const account1 = await create('account');
const account2 = await create('account');
@@ -175,8 +176,9 @@ describe('routes: `/accounting`', () => {
.set('x-access-token', loginRes.body.token)
.send({
date: new Date('2020-2-2').toISOString(),
- reference: '1000',
- memo: 'Description here.',
+ journal_number: '1000',
+ reference: '2000',
+ description: 'Description here.',
entries: [
{
credit: 1000,
@@ -192,12 +194,14 @@ describe('routes: `/accounting`', () => {
const foundManualJournal = await ManualJournal.query();
expect(foundManualJournal.length).equals(1);
- expect(foundManualJournal[0].reference).equals('1000');
+
+ expect(foundManualJournal[0].reference).equals('2000');
+ expect(foundManualJournal[0].journalNumber).equals('1000');
expect(foundManualJournal[0].transactionType).equals('Journal');
expect(foundManualJournal[0].amount).equals(1000);
expect(moment(foundManualJournal[0].date).format('YYYY-MM-DD')).equals('2020-02-02');
- expect(foundManualJournal[0].note).equals('Description here.');
- expect(foundManualJournal[0].userId).equals(1);
+ expect(foundManualJournal[0].description).equals('Description here.');
+ expect(foundManualJournal[0].userId).to.be.a('integer');
});
it('Should store journal transactions to the storage.', async () => {
@@ -246,7 +250,7 @@ describe('routes: `/accounting`', () => {
});
- describe.only('route: `accounting/manual-journals`', async () => {
+ describe('route: `accounting/manual-journals`', async () => {
it('Should retrieve manual journals resource not found.', async () => {
const res = await request()