Compare commits

...

13 Commits

Author SHA1 Message Date
allcontributors[bot]
44bd916ede docs: update .all-contributorsrc [skip ci] 2023-10-25 20:14:18 +00:00
allcontributors[bot]
fd5db4f055 docs: update README.md [skip ci] 2023-10-25 20:14:17 +00:00
Robert Koch
e4a7f09dbc feat: Add tax numbers to the organization details (#269) 2023-10-25 22:10:46 +02:00
Ahmed Bouhuolia
2c5537efad fix: Trial balance sheet adjusted balance (#273) 2023-10-25 13:18:13 +02:00
Ahmed Bouhuolia
017908600e feat: improve financial statements rows color (#276) 2023-10-24 22:40:54 +02:00
dependabot[bot]
6307ca8935 chore(deps-dev): bump @babel/traverse in /packages/server (#272)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.23.0 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 19:33:12 +02:00
Ahmed Bouhuolia
1ed1c9ea1d feat: assign default sell/purchase tax rates to items (#261) 2023-10-08 23:55:59 +02:00
Ahmed Bouhuolia
d40de4d22b feat: integrate tax rates to bills (#260) 2023-10-08 16:07:18 +02:00
Ahmed Bouhuolia
ee62e3e1c2 feat: migrate to pnpm (#253) 2023-10-04 12:17:27 +02:00
Ahmed Bouhuolia
5df454dd30 chore: bump packages version to v0.10.2 2023-10-02 23:29:21 +02:00
Ahmed Bouhuolia
07628ddc37 fix(server): add missing method in ItemEntry model. 2023-10-02 23:27:19 +02:00
Ahmed Bouhuolia
69afa07e3b fix(webapp): Disable tax rates from item entries editor table on services do not support tax rates 2023-10-02 23:27:05 +02:00
Ahmed Bouhuolia
b1a043f699 chore(server): add package-lock.json file 2023-09-27 19:16:02 +02:00
299 changed files with 41624 additions and 24833 deletions

View File

@@ -60,6 +60,15 @@
"contributions": [
"bug"
]
},
{
"login": "kochie",
"name": "Robert Koch",
"avatar_url": "https://avatars.githubusercontent.com/u/10809884?v=4",
"profile": "https://me.kochie.io",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -80,6 +80,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://scheibling.se"><img src="https://avatars.githubusercontent.com/u/24367830?v=4?s=100" width="100px;" alt="Lars Scheibling"/><br /><sub><b>Lars Scheibling</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Ascheibling" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suhaibaffan"><img src="https://avatars.githubusercontent.com/u/18115937?v=4?s=100" width="100px;" alt="Suhaib Affan"/><br /><sub><b>Suhaib Affan</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=suhaibaffan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://me.kochie.io"><img src="https://avatars.githubusercontent.com/u/10809884?v=4?s=100" width="100px;" alt="Robert Koch"/><br /><sub><b>Robert Koch</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=kochie" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -1,6 +1,7 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent",
"npmClient": "pnpm",
"useWorkspaces": true,
"version": "0.9.6",
"npmClient": "npm"
}
"packages": ["packages/*"]
}

5730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
"name": "bigcapital-monorepo",
"private": true,
"scripts": {
"bootstrap": "lerna exec npm install",
"dev": "lerna run dev",
"build": "lerna run build",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
@@ -13,10 +12,6 @@
"test:e2e": "playwright test",
"prepare": "husky install"
},
"workspaces": [
"packages/*",
"shared/*"
],
"devDependencies": {
"@commitlint/cli": "^17.4.2",
"@commitlint/config-conventional": "^17.4.2",
@@ -27,7 +22,7 @@
"lerna": "^6.4.1"
},
"engines": {
"node": "14.x"
"node": "16.x || 17.x || 18.x"
},
"husky": {
"hooks": {

View File

@@ -1,7 +1,6 @@
/node_modules/
/.env
/storage
package-lock.json
stdout.log
/dist
/build
/build

13698
packages/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@bigcapital/server",
"version": "0.10.0",
"version": "0.10.2",
"description": "",
"main": "src/server.ts",
"scripts": {
@@ -35,6 +35,7 @@
"babel-loader": "^9.1.2",
"bcryptjs": "^2.4.3",
"bluebird": "^3.7.2",
"body-parser": "^1.20.2",
"compression": "^1.7.4",
"country-codes-list": "^1.6.8",
"cpy": "^8.1.2",
@@ -72,6 +73,8 @@
"memory-cache": "^0.2.0",
"moment": "^2.24.0",
"moment-range": "^4.0.2",
"moment-timezone": "^0.5.43",
"mongodb": "^6.1.0",
"mongoose": "^5.10.0",
"mustache": "^3.0.3",
"mysql": "^2.17.1",
@@ -131,7 +134,7 @@
"rimraf": "^3.0.2",
"rtlcss": "^3.3.0",
"run-script-webpack-plugin": "^0.1.1",
"sass": "^1.37.5",
"sass": "^1.58.0",
"sinon": "^7.4.2",
"start-server-webpack-plugin": "^2.2.5",
"ts-loader": "^9.4.2",

View File

@@ -16,7 +16,7 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
/**
* Router constructor.
*/
router() {
public router() {
const router = Router();
router.get(
@@ -36,7 +36,7 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
* Validation schema.
* @return {ValidationChain[]}
*/
get trialBalanceSheetValidationSchema(): ValidationChain[] {
private get trialBalanceSheetValidationSchema(): ValidationChain[] {
return [
...this.sheetNumberFormatValidationSchema,
query('basis').optional(),
@@ -59,28 +59,37 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
/**
* Retrieve the trial balance sheet.
*/
public async trialBalanceSheet(
private async trialBalanceSheet(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, settings } = req;
const { tenantId } = req;
let filter = this.matchedQueryData(req);
filter = {
...filter,
accountsIds: castArray(filter.accountsIds),
};
try {
const { data, query, meta } =
await this.trialBalanceSheetService.trialBalanceSheet(tenantId, filter);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
return res.status(200).send({
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
if (acceptType === 'application/json+table') {
const { table, meta, query } =
await this.trialBalanceSheetService.trialBalanceSheetTable(
tenantId,
filter
);
return res.status(200).send({ table, meta, query });
} else {
const { data, query, meta } =
await this.trialBalanceSheetService.trialBalanceSheet(
tenantId,
filter
);
return res.status(200).send({ data, query, meta });
}
} catch (error) {
next(error);
}

View File

@@ -149,6 +149,11 @@ export default class ItemsController extends BaseController {
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.TEXT }),
check('sell_tax_rate_id').optional({ nullable: true }).isInt().toInt(),
check('purchase_tax_rate_id')
.optional({ nullable: true })
.isInt()
.toInt(),
check('category_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
@@ -508,6 +513,28 @@ export default class ItemsController extends BaseController {
],
});
}
if (error.errorType === 'PURCHASE_TAX_RATE_NOT_FOUND') {
return res.status(400).send({
errors: [
{
type: 'PURCHASE_TAX_RATE_NOT_FOUND',
message: 'Purchase tax rate has not found.',
code: 410,
},
],
});
}
if (error.errorType === 'SELL_TAX_RATE_NOT_FOUND') {
return res.status(400).send({
errors: [
{
type: 'SELL_TAX_RATE_NOT_FOUND',
message: 'Sell tax rate is not found.',
code: 420,
},
],
});
}
}
next(error);
}

View File

@@ -31,14 +31,14 @@ export default class OrganizationController extends BaseController {
router.post(
'/build',
this.organizationValidationSchema,
this.buildOrganizationValidationSchema,
this.validationResult,
asyncMiddleware(this.build.bind(this)),
this.handleServiceErrors.bind(this)
);
router.put(
'/',
this.organizationValidationSchema,
this.updateOrganizationValidationSchema,
this.validationResult,
this.asyncMiddleware(this.updateOrganization.bind(this)),
this.handleServiceErrors.bind(this)
@@ -55,7 +55,7 @@ export default class OrganizationController extends BaseController {
* Organization setup schema.
* @return {ValidationChain[]}
*/
private get organizationValidationSchema(): ValidationChain[] {
private get commonOrganizationValidationSchema(): ValidationChain[] {
return [
check('name').exists().trim(),
check('industry').optional({ nullable: true }).isString().trim().escape(),
@@ -68,6 +68,29 @@ export default class OrganizationController extends BaseController {
];
}
/**
* Build organization validation schema.
* @returns {ValidationChain[]}
*/
private get buildOrganizationValidationSchema(): ValidationChain[] {
return [...this.commonOrganizationValidationSchema];
}
/**
* Update organization validation schema.
* @returns {ValidationChain[]}
*/
private get updateOrganizationValidationSchema(): ValidationChain[] {
return [
...this.commonOrganizationValidationSchema,
check('tax_number')
.optional({ nullable: true })
.isString()
.trim()
.escape(),
];
}
/**
* Builds tenant database and migrate database schema.
* @param {Request} req - Express request.

View File

@@ -115,6 +115,8 @@ export default class BillsController extends BaseController {
check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),
check('entries').isArray({ min: 1 }),
check('entries.*.index').exists().isNumeric().toInt(),
@@ -137,6 +139,15 @@ export default class BillsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.tax_code')
.optional({ nullable: true })
.trim()
.escape()
.isString(),
check('entries.*.tax_rate_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
];
}
@@ -542,6 +553,16 @@ export default class BillsController extends BaseController {
],
});
}
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 1800 }],
});
}
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND', code: 1900 }],
});
}
}
next(error);
}

View File

@@ -0,0 +1,10 @@
exports.up = (knex) => {
return knex.schema.table('bills', (table) => {
table.boolean('is_inclusive_tax').defaultTo(false);
table.decimal('tax_amount_withheld');
});
};
exports.down = (knex) => {
return knex.schema.table('bills', () => {});
};

View File

@@ -0,0 +1,18 @@
exports.up = (knex) => {
return knex.schema.table('items', (table) => {
table
.integer('sell_tax_rate_id')
.unsigned()
.references('id')
.inTable('tax_rates');
table
.integer('purchase_tax_rate_id')
.unsigned()
.references('id')
.inTable('tax_rates');
});
};
exports.down = (knex) => {
return knex.schema.dropTableIfExists('tax_rates');
};

View File

@@ -2,6 +2,7 @@ import { Knex } from 'knex';
import { IDynamicListFilterDTO } from './DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IBillLandedCost } from './LandedCost';
export interface IBillDTO {
vendorId: number;
billNumber: string;
@@ -15,10 +16,10 @@ export interface IBillDTO {
exchangeRate?: number;
open: boolean;
entries: IItemEntryDTO[];
branchId?: number;
warehouseId?: number;
projectId?: number;
isInclusiveTax?: boolean;
}
export interface IBillEditDTO {
@@ -80,6 +81,15 @@ export interface IBill {
localAmount?: number;
locatedLandedCosts?: IBillLandedCost[];
amountLocal: number;
subtotal: number;
subtotalLocal: number;
subtotalExcludingTax: number;
taxAmountWithheld: number;
taxAmountWithheldLocal: number;
total: number;
totalLocal: number;
}
export interface IBillsFilter extends IDynamicListFilterDTO {

View File

@@ -22,6 +22,9 @@ export interface IItem {
sellDescription: string;
purchaseDescription: string;
sellTaxRateId: number;
purchaseTaxRateId: number;
quantityOnHand: number;
note: string;
@@ -54,6 +57,9 @@ export interface IItemDTO {
sellDescription: string;
purchaseDescription: string;
sellTaxRateId: number;
purchaseTaxRateId: number;
quantityOnHand: number;
note: string;

View File

@@ -16,6 +16,8 @@ export interface ILedger {
getClosingBalance(): number;
getForeignClosingBalance(): number;
getClosingDebit(): number;
getClosingCredit(): number;
getContactsIds(): number[];
getAccountsIds(): number[];

View File

@@ -25,6 +25,7 @@ export interface IOrganizationUpdateDTO {
timezone: string;
fiscalYear: string;
industry: string;
taxNumber: string;
}
export interface IOrganizationBuildEventPayload {

View File

@@ -36,6 +36,7 @@ export interface ITaxRateCreatedPayload {
}
export interface ITaxRateEditingPayload {
oldTaxRate: ITaxRate;
editTaxRateDTO: IEditTaxRateDTO;
tenantId: number;
trx: Knex.Transaction;

View File

@@ -33,6 +33,7 @@ export interface ITrialBalanceAccount extends ITrialBalanceTotal {
id: number;
parentAccountId: number;
name: string;
formattedName: string;
code: string;
accountNormal: string;
}

View File

@@ -81,6 +81,9 @@ import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/Proj
import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber';
import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber';
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber';
import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber';
import { SyncItemTaxRateOnEditTaxSubscriber } from '@/services/TaxRates/SyncItemTaxRateOnEditTaxSubscriber';
export default () => {
return new EventPublisher();
@@ -188,8 +191,14 @@ export const susbcribers = () => {
ProjectBillableExpensesSubscriber,
ProjectBillableBillSubscriber,
// Tax Rates
// Tax Rates - Sale Invoice
SaleInvoiceTaxRateValidateSubscriber,
WriteInvoiceTaxTransactionsSubscriber,
// Tax Rates - Bills
BillTaxRateValidateSubscriber,
WriteBillTaxTransactionsSubscriber,
SyncItemTaxRateOnEditTaxSubscriber
];
};

View File

@@ -13,6 +13,109 @@ export default class Bill extends mixin(TenantModel, [
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public paymentAmount: number;
public landedCostAmount: number;
public allocatedCostAmount: number;
public isInclusiveTax: boolean;
public taxAmountWithheld: number;
public exchangeRate: number;
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'balance',
'dueAmount',
'isOpen',
'isPartiallyPaid',
'isFullyPaid',
'isPaid',
'remainingDays',
'overdueDays',
'isOverdue',
'unallocatedCostAmount',
'localAmount',
'localAllocatedCostAmount',
'billableAmount',
'amountLocal',
'subtotal',
'subtotalLocal',
'subtotalExludingTax',
'taxAmountWithheldLocal',
'total',
'totalLocal',
];
}
/**
* Invoice amount in base currency.
* @returns {number}
*/
get amountLocal() {
return this.amount * this.exchangeRate;
}
/**
* Subtotal. (Tax inclusive) if the tax inclusive is enabled.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled.
* @returns {number}
*/
get subtotalLocal() {
return this.amountLocal;
}
/**
* Sale invoice amount excluding tax.
* @returns {number}
*/
get subtotalExcludingTax() {
return this.isInclusiveTax
? this.subtotal - this.taxAmountWithheld
: this.subtotal;
}
/**
* Tax amount withheld in base currency.
* @returns {number}
*/
get taxAmountWithheldLocal() {
return this.taxAmountWithheld * this.exchangeRate;
}
/**
* Invoice total. (Tax included)
* @returns {number}
*/
get total() {
return this.isInclusiveTax
? this.subtotal
: this.subtotal + this.taxAmountWithheld;
}
/**
* Invoice total in local currency. (Tax included)
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/**
* Table name
*/
@@ -158,40 +261,13 @@ export default class Bill extends mixin(TenantModel, [
};
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'balance',
'dueAmount',
'isOpen',
'isPartiallyPaid',
'isFullyPaid',
'isPaid',
'remainingDays',
'overdueDays',
'isOverdue',
'unallocatedCostAmount',
'localAmount',
'localAllocatedCostAmount',
'billableAmount',
];
}
/**
* Invoice amount in organization base currency.
* @deprecated
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
return this.amountLocal;
}
/**
@@ -231,7 +307,7 @@ export default class Bill extends mixin(TenantModel, [
* @return {number}
*/
get dueAmount() {
return Math.max(this.amount - this.balance, 0);
return Math.max(this.total - this.balance, 0);
}
/**
@@ -247,7 +323,7 @@ export default class Bill extends mixin(TenantModel, [
* @return {boolean}
*/
get isPartiallyPaid() {
return this.dueAmount !== this.amount && this.dueAmount > 0;
return this.dueAmount !== this.total && this.dueAmount > 0;
}
/**
@@ -308,7 +384,7 @@ export default class Bill extends mixin(TenantModel, [
* Retrieves the calculated amount which have not been invoiced.
*/
get billableAmount() {
return Math.max(this.amount - this.invoicedAmount, 0);
return Math.max(this.total - this.invoicedAmount, 0);
}
/**
@@ -326,6 +402,7 @@ export default class Bill extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry');
const BillLandedCost = require('models/BillLandedCost');
const Branch = require('models/Branch');
const TaxRateTransaction = require('models/TaxRateTransaction');
return {
vendor: {
@@ -373,6 +450,21 @@ export default class Bill extends mixin(TenantModel, [
to: 'branches.id',
},
},
/**
* Bill may has associated tax rate transactions.
*/
taxes: {
relation: Model.HasManyRelation,
modelClass: TaxRateTransaction.default,
join: {
from: 'bills.id',
to: 'tax_rate_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'Bill');
},
},
};
}

View File

@@ -65,6 +65,7 @@ export default class Item extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry');
const WarehouseTransferEntry = require('models/WarehouseTransferEntry');
const InventoryAdjustmentEntry = require('models/InventoryAdjustmentEntry');
const TaxRate = require('models/TaxRate');
return {
/**
@@ -178,11 +179,35 @@ export default class Item extends mixin(TenantModel, [
to: 'media.id',
},
},
/**
* Item may has sell tax rate.
*/
sellTaxRate: {
relation: Model.BelongsToOneRelation,
modelClass: TaxRate.default,
join: {
from: 'items.sellTaxRateId',
to: 'tax_rates.id',
},
},
/**
* Item may has purchase tax rate.
*/
purchaseTaxRate: {
relation: Model.BelongsToOneRelation,
modelClass: TaxRate.default,
join: {
from: 'items.purchaseTaxRateId',
to: 'tax_rates.id',
},
},
};
}
/**
*
*
*/
static get secureDeleteRelations() {
return [

View File

@@ -99,6 +99,13 @@ export default class ItemEntry extends TenantModel {
: getExlusiveTaxAmount(this.amount, this.taxRate);
}
static calcAmount(itemEntry) {
const { discount, quantity, rate } = itemEntry;
const total = quantity * rate;
return discount ? total - total * discount * 0.01 : total;
}
/**
* Item entry relations.
*/

View File

@@ -1,5 +1,5 @@
import moment from 'moment';
import { defaultTo, uniqBy } from 'lodash';
import { defaultTo, sumBy, uniqBy } from 'lodash';
import { IAccountTransaction, ILedger, ILedgerEntry } from '@/interfaces';
export default class Ledger implements ILedger {
@@ -49,6 +49,15 @@ export default class Ledger implements ILedger {
return this.filter((entry) => entry.accountId === accountId);
}
/**
* Filters entries by the given accounts ids then returns a new ledger.
* @param {number[]} accountsIds - Accounts ids.
* @returns {ILedger}
*/
public whereAccountsIds(accountsIds: number[]): ILedger {
return this.filter((entry) => accountsIds.indexOf(entry.accountId) !== -1);
}
/**
* Filters entries that before or same the given date and returns a new ledger.
* @param {Date|string} fromDate
@@ -130,6 +139,22 @@ export default class Ledger implements ILedger {
return closingBalance;
}
/**
* Retrieves the closing credit of the entries.
* @returns {number}
*/
public getClosingCredit(): number {
return sumBy(this.entries, 'credit');
}
/**
* Retrieves the closing debit of the entries.
* @returns {number}
*/
public getClosingDebit(): number {
return sumBy(this.entries, 'debit');
}
/**
* Retrieve the closing balance of the entries.
* @returns {number}

View File

@@ -10,13 +10,26 @@ import {
} from '@/interfaces';
import FinancialSheet from '../FinancialSheet';
import { allPassedConditionsPass, flatToNestedArray } from 'utils';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
export default class TrialBalanceSheet extends FinancialSheet {
tenantId: number;
query: ITrialBalanceSheetQuery;
accounts: IAccount & { type: IAccountType }[];
journalFinancial: any;
baseCurrency: string;
/**
* Trial balance sheet query.
* @param {ITrialBalanceSheetQuery} query
*/
private query: ITrialBalanceSheetQuery;
/**
* Trial balance sheet repository.
* @param {TrialBalanceSheetRepository}
*/
private repository: TrialBalanceSheetRepository;
/**
* Organization base currency.
* @param {string}
*/
private baseCurrency: string;
/**
* Constructor method.
@@ -28,20 +41,58 @@ export default class TrialBalanceSheet extends FinancialSheet {
constructor(
tenantId: number,
query: ITrialBalanceSheetQuery,
accounts: IAccount & { type: IAccountType }[],
journalFinancial: any,
repository: TrialBalanceSheetRepository,
baseCurrency: string
) {
super();
this.tenantId = tenantId;
this.query = query;
this.accounts = accounts;
this.journalFinancial = journalFinancial;
this.repository = repository;
this.numberFormat = this.query.numberFormat;
this.baseCurrency = baseCurrency;
}
/**
* Retrieves the closing credit of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountCredit(accountId: number) {
const depsAccountsIds =
this.repository.accountsDepGraph.dependenciesOf(accountId);
return this.repository.totalAccountsLedger
.whereAccountsIds([accountId, ...depsAccountsIds])
.getClosingCredit();
}
/**
* Retrieves the closing debit of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountDebit(accountId: number) {
const depsAccountsIds =
this.repository.accountsDepGraph.dependenciesOf(accountId);
return this.repository.totalAccountsLedger
.whereAccountsIds([accountId, ...depsAccountsIds])
.getClosingDebit();
}
/**
* Retrieves the closing total of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountTotal(accountId: number) {
const credit = this.getClosingAccountCredit(accountId);
const debit = this.getClosingAccountDebit(accountId);
return debit - credit;
}
/**
* Account mapper.
* @param {IAccount} account
@@ -50,23 +101,28 @@ export default class TrialBalanceSheet extends FinancialSheet {
private accountTransformer = (
account: IAccount & { type: IAccountType }
): ITrialBalanceAccount => {
const trial = this.journalFinancial.getTrialBalanceWithDepands(account.id);
const debit = this.getClosingAccountDebit(account.id);
const credit = this.getClosingAccountCredit(account.id);
const balance = this.getClosingAccountTotal(account.id);
return {
id: account.id,
parentAccountId: account.parentAccountId,
name: account.name,
formattedName: account.code
? `${account.name} - ${account.code}`
: `${account.name}`,
code: account.code,
accountNormal: account.accountNormal,
credit: trial.credit,
debit: trial.debit,
balance: trial.balance,
credit,
debit,
balance,
currencyCode: this.baseCurrency,
formattedCredit: this.formatNumber(trial.credit),
formattedDebit: this.formatNumber(trial.debit),
formattedBalance: this.formatNumber(trial.balance),
formattedCredit: this.formatNumber(credit),
formattedDebit: this.formatNumber(debit),
formattedBalance: this.formatNumber(balance),
};
};
@@ -117,10 +173,7 @@ export default class TrialBalanceSheet extends FinancialSheet {
private filterNoneTransactions = (
accountNode: ITrialBalanceAccount
): boolean => {
const entries = this.journalFinancial.getAccountEntriesWithDepents(
accountNode.id
);
return entries.length > 0;
return false === this.repository.totalAccountsLedger.isEmpty();
};
/**
@@ -200,11 +253,11 @@ export default class TrialBalanceSheet extends FinancialSheet {
*/
public reportData(): ITrialBalanceSheetData {
// Don't return noting if the journal has no transactions.
if (this.journalFinancial.isEmpty()) {
if (this.repository.totalAccountsLedger.isEmpty()) {
return null;
}
// Retrieve accounts nodes.
const accounts = this.accountsSection(this.accounts);
const accounts = this.accountsSection(this.repository.accounts);
// Retrieve account node.
const total = this.tatalSection(accounts);

View File

@@ -0,0 +1,105 @@
import { ITrialBalanceSheetQuery } from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
import { Knex } from 'knex';
import { isEmpty } from 'lodash';
import { Service } from 'typedi';
@Service()
export class TrialBalanceSheetRepository {
private query: ITrialBalanceSheetQuery;
private models: any;
public accounts: any;
public accountsDepGraph;
/**
* Total closing accounts ledger.
* @param {Ledger}
*/
public totalAccountsLedger: Ledger;
/**
* Constructor method.
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
*/
constructor(models: any, repos: any, query: ITrialBalanceSheetQuery) {
this.query = query;
this.repos = repos;
this.models = models;
}
/**
* Async initialize.
* @returns {Promise<void>}
*/
public asyncInitialize = async () => {
await this.initAccounts();
await this.initAccountsClosingTotalLedger();
};
// ----------------------------
// # Accounts
// ----------------------------
/**
* Initialize accounts.
* @returns {Promise<void>}
*/
public initAccounts = async () => {
const accounts = await this.getAccounts();
const accountsDepGraph =
await this.repos.accountRepository.getDependencyGraph();
this.accountsDepGraph = accountsDepGraph;
this.accounts = accounts;
};
/**
* Initialize all accounts closing total ledger.
* @return {Promise<void>}
*/
public initAccountsClosingTotalLedger = async (): Promise<void> => {
const totalByAccounts = await this.closingAccountsTotal(this.query.toDate);
this.totalAccountsLedger = Ledger.fromTransactions(totalByAccounts);
};
/**
* Retrieve accounts of the report.
* @return {Promise<IAccount[]>}
*/
private getAccounts = () => {
const { Account } = this.models;
return Account.query();
};
/**
* Retrieve the opening balance transactions of the report.
* @param {Date|string} openingDate -
*/
public closingAccountsTotal = async (openingDate: Date | string) => {
const { AccountTransaction } = this.models;
return AccountTransaction.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('filterDateRange', null, openingDate);
query.withGraphFetched('account');
this.commonFilterBranchesQuery(query);
});
};
/**
* Common branches filter query.
* @param {Knex.QueryBuilder} query
*/
private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
if (!isEmpty(this.query.branchesIds)) {
query.modify('filterByBranches', this.query.branchesIds);
}
};
}

View File

@@ -2,12 +2,18 @@ import { Service, Inject } from 'typedi';
import moment from 'moment';
import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster';
import { ITrialBalanceSheetMeta, ITrialBalanceSheetQuery, ITrialBalanceStatement } from '@/interfaces';
import {
ITrialBalanceSheetMeta,
ITrialBalanceSheetQuery,
ITrialBalanceStatement,
} from '@/interfaces';
import TrialBalanceSheet from './TrialBalanceSheet';
import FinancialSheet from '../FinancialSheet';
import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
import { TrialBalanceSheetTable } from './TrialBalanceSheetTable';
@Service()
export default class TrialBalanceSheetService extends FinancialSheet {
@@ -51,9 +57,8 @@ export default class TrialBalanceSheetService extends FinancialSheet {
reportMetadata(tenantId: number): ITrialBalanceSheetMeta {
const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService.isItemsCostComputeRunning(
tenantId
);
const isCostComputeRunning =
this.inventoryService.isItemsCostComputeRunning(tenantId);
const organizationName = settings.get({
group: 'organization',
key: 'name',
@@ -72,10 +77,8 @@ export default class TrialBalanceSheetService extends FinancialSheet {
/**
* Retrieve trial balance sheet statement.
* -------------
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
*
* @return {IBalanceSheetStatement}
*/
public async trialBalanceSheet(
@@ -86,43 +89,27 @@ export default class TrialBalanceSheetService extends FinancialSheet {
...this.defaultQuery,
...query,
};
const {
accountRepository,
transactionsRepository,
} = this.tenancy.repositories(tenantId);
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
this.logger.info('[trial_balance_sheet] trying to calcualte the report.', {
tenantId,
filter,
});
// Retrieve all accounts on the storage.
const accounts = await accountRepository.all();
const accountsGraph = await accountRepository.getDependencyGraph();
const models = this.tenancy.models(tenantId);
const repos = this.tenancy.repositories(tenantId);
// Retrieve all journal transactions based on the given query.
const transactions = await transactionsRepository.journal({
fromDate: query.fromDate,
toDate: query.toDate,
sumationCreditDebit: true,
branchesIds: query.branchesIds
});
// Transform transactions array to journal collection.
const transactionsJournal = Journal.fromTransactions(
transactions,
tenantId,
accountsGraph
const trialBalanceSheetRepos = new TrialBalanceSheetRepository(
models,
repos,
filter
);
await trialBalanceSheetRepos.asyncInitialize();
// Trial balance report instance.
const trialBalanceInstance = new TrialBalanceSheet(
tenantId,
filter,
accounts,
transactionsJournal,
tenant.metadata.baseCurrency,
trialBalanceSheetRepos,
tenant.metadata.baseCurrency
);
// Trial balance sheet data.
const trialBalanceSheetData = trialBalanceInstance.reportData();
@@ -133,4 +120,27 @@ export default class TrialBalanceSheetService extends FinancialSheet {
meta: this.reportMetadata(tenantId),
};
}
/**
* Retrieves the trial balance sheet table.
* @param {number} tenantId
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<any>}
*/
public async trialBalanceSheetTable(
tenantId: number,
query: ITrialBalanceSheetQuery
) {
const trialBalance = await this.trialBalanceSheet(tenantId, query);
const table = new TrialBalanceSheetTable(trialBalance.data, query, {});
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
meta: trialBalance.meta,
query: trialBalance.query,
};
}
}

View File

@@ -0,0 +1,146 @@
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import { FinancialTable } from '../FinancialTable';
import {
IBalanceSheetStatementData,
ITableColumn,
ITableColumnAccessor,
ITableRow,
ITrialBalanceAccount,
ITrialBalanceSheetData,
ITrialBalanceSheetQuery,
ITrialBalanceTotal,
} from '@/interfaces';
import { tableRowMapper } from '@/utils';
import { IROW_TYPE } from '../BalanceSheet/constants';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
export class TrialBalanceSheetTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
/**
* @param {ITrialBalanceSheetData}
*/
public data: ITrialBalanceSheetData;
/**
* Balance sheet query.
* @param {ITrialBalanceSheetQuery}
*/
public query: ITrialBalanceSheetQuery;
/**
* Constructor method.
* @param {IBalanceSheetStatementData} reportData -
* @param {ITrialBalanceSheetQuery} query -
*/
constructor(
data: ITrialBalanceSheetData,
query: ITrialBalanceSheetQuery,
i18n: any
) {
super();
this.data = data;
this.query = query;
this.i18n = i18n;
}
/**
* Retrieve the common columns for all report nodes.
* @param {ITableColumnAccessor[]}
*/
private commonColumnsAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'account', accessor: 'formattedName' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'total', accessor: 'formattedBalance' },
];
};
/**
* Maps the account node to table row.
* @param {ITrialBalanceAccount} node -
* @returns {ITableRow}
*/
private accountNodeTableRowsMapper = (
node: ITrialBalanceAccount
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.ACCOUNT],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Maps the total node to table row.
* @param {ITrialBalanceTotal} node -
* @returns {ITableRow}
*/
private totalNodeTableRowsMapper = (node: ITrialBalanceTotal): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.TOTAL],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Mappes the given report sections to table rows.
* @param {IBalanceSheetDataNode[]} nodes -
* @return {ITableRow}
*/
private accountsToTableRowsMap = (
nodes: ITrialBalanceAccount[]
): ITableRow[] => {
return this.mapNodesDeep(nodes, this.accountNodeTableRowsMapper);
};
/**
* Retrieves the accounts table rows of the given report data.
* @returns {ITableRow[]}
*/
private accountsTableRows = (): ITableRow[] => {
return this.accountsToTableRowsMap(this.data.accounts);
};
/**
* Maps the given total node to table row.
* @returns {ITableRow}
*/
private totalTableRow = (): ITableRow => {
return this.totalNodeTableRowsMapper(this.data.total);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.compose(
R.append(this.totalTableRow()),
R.concat(this.accountsTableRows())
)([]);
};
/**
* Retrrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(
this.tableColumnsCellIndexing,
R.concat([
{ key: 'account_name', label: 'Account' },
{ key: 'debit', label: 'Debit' },
{ key: 'credit', label: 'Credit' },
{ key: 'total', label: 'Total' },
])
)([]);
};
}

View File

@@ -0,0 +1,5 @@
export enum IROW_TYPE {
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}

View File

@@ -55,6 +55,18 @@ export class CreateItem {
itemDTO.inventoryAccountId
);
}
if (itemDTO.purchaseTaxRateId) {
await this.validators.validatePurchaseTaxRateExistance(
tenantId,
itemDTO.purchaseTaxRateId
);
}
if (itemDTO.sellTaxRateId) {
await this.validators.validateSellTaxRateExistance(
tenantId,
itemDTO.sellTaxRateId
);
}
}
/**

View File

@@ -76,6 +76,20 @@ export class EditItem {
itemDTO.inventoryAccountId
);
}
// Validate the purchase tax rate id existance.
if (itemDTO.purchaseTaxRateId) {
await this.validators.validatePurchaseTaxRateExistance(
tenantId,
itemDTO.purchaseTaxRateId
);
}
// Validate the sell tax rate id existance.
if (itemDTO.sellTaxRateId) {
await this.validators.validateSellTaxRateExistance(
tenantId,
itemDTO.sellTaxRateId
);
}
// Validate inventory account should be modified in inventory item
// has inventory transactions.
await this.validators.validateItemInvnetoryAccountModified(

View File

@@ -27,6 +27,8 @@ export class GetItem {
.withGraphFetched('category')
.withGraphFetched('costAccount')
.withGraphFetched('itemWarehouses.warehouse')
.withGraphFetched('sellTaxRate')
.withGraphFetched('purchaseTaxRate')
.throwIfNotFound();
return this.transformer.transform(tenantId, item, new ItemTransformer());

View File

@@ -241,4 +241,40 @@ export class ItemsValidators {
throw new ServiceError(ERRORS.ITEM_CANNOT_CHANGE_INVENTORY_TYPE);
}
}
/**
* Validate the purchase tax rate id existance.
* @param {number} tenantId -
* @param {number} taxRateId -
*/
public async validatePurchaseTaxRateExistance(
tenantId: number,
taxRateId: number
) {
const { TaxRate } = this.tenancy.models(tenantId);
const foundTaxRate = await TaxRate.query().findById(taxRateId);
if (!foundTaxRate) {
throw new ServiceError(ERRORS.PURCHASE_TAX_RATE_NOT_FOUND);
}
}
/**
* Validate the sell tax rate id existance.
* @param {number} tenantId
* @param {number} taxRateId
*/
public async validateSellTaxRateExistance(
tenantId: number,
taxRateId: number
) {
const { TaxRate } = this.tenancy.models(tenantId);
const foundTaxRate = await TaxRate.query().findById(taxRateId);
if (!foundTaxRate) {
throw new ServiceError(ERRORS.SELL_TAX_RATE_NOT_FOUND);
}
}
}

View File

@@ -22,7 +22,10 @@ export const ERRORS = {
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS'
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
};
export const DEFAULT_VIEW_COLUMNS = [];

View File

@@ -203,8 +203,8 @@ export class BillPaymentGLEntries {
/**
* Retrieves the payment GL payable entry.
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @returns {ILedgerEntry}
*/
private getPaymentGLPayableEntry = (

View File

@@ -14,6 +14,7 @@ import {
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
@Service()
export class BillDTOTransformer {
@@ -23,6 +24,9 @@ export class BillDTOTransformer {
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private taxDTOTransformer: ItemEntriesTaxTransactions;
@Inject()
private tenancy: HasTenancyService;
@@ -73,14 +77,24 @@ export class BillDTOTransformer {
const billNumber = billDTO.billNumber || oldBill?.billNumber;
const initialEntries = billDTO.entries.map((entry) => ({
reference_type: 'Bill',
referenceType: 'Bill',
isInclusiveTax: billDTO.isInclusiveTax,
...omit(entry, ['amount']),
}));
const entries = await composeAsync(
const asyncEntries = await composeAsync(
// Associate tax rate from tax id to entries.
this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries(tenantId),
// Associate tax rate id from tax code to entries.
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries(tenantId),
// Sets the default cost account to the bill entries.
this.setBillEntriesDefaultAccounts(tenantId)
)(initialEntries);
const entries = R.compose(
// Remove tax code from entries.
R.map(R.omit(['taxCode']))
)(asyncEntries);
const initialDTO = {
...formatDateFields(omit(billDTO, ['open', 'entries']), [
'billDate',
@@ -100,6 +114,8 @@ export class BillDTOTransformer {
userId: authorizedUser.id,
};
return R.compose(
// Associates tax amount withheld to the model.
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
this.branchDTOTransform.transformDTO(tenantId),
this.warehouseDTOTransform.transformDTO(tenantId)
)(initialDTO);

View File

@@ -7,6 +7,7 @@ import { AccountNormal, IBill, IItemEntry, ILedgerEntry } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
@Service()
export class BillGLEntries {
@@ -16,6 +17,9 @@ export class BillGLEntries {
@Inject()
private ledgerStorage: LedgerStorageService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
/**
* Creates bill GL entries.
* @param {number} tenantId -
@@ -43,8 +47,16 @@ export class BillGLEntries {
{},
trx
);
const billLedger = this.getBillLedger(bill, APAccount.id);
// Find or create tax payable account.
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
{},
trx
);
const billLedger = this.getBillLedger(
bill,
APAccount.id,
taxPayableAccount.id
);
// Commit the GL enties on the storage.
await this.ledgerStorage.commit(tenantId, billLedger, trx);
};
@@ -83,7 +95,7 @@ export class BillGLEntries {
/**
* Retrieves the bill common entry.
* @param {IBill} bill
* @param {IBill} bill
* @returns {ILedgerEntry}
*/
private getBillCommonEntry = (bill: IBill) => {
@@ -119,7 +131,7 @@ export class BillGLEntries {
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
const localAmount = bill.exchangeRate * entry.amount;
const localAmount = bill.exchangeRate * entry.amountExludingTax;
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
return {
@@ -173,7 +185,7 @@ export class BillGLEntries {
return {
...commonJournalMeta,
credit: bill.localAmount,
credit: bill.totalLocal,
accountId: payableAccountId,
contactId: bill.vendorId,
accountNormal: AccountNormal.CREDIT,
@@ -182,15 +194,62 @@ export class BillGLEntries {
};
};
/**
* Retrieves the bill tax GL entry.
* @param {IBill} bill -
* @param {number} taxPayableAccountId -
* @param {IItemEntry} entry -
* @param {number} index -
* @returns {ILedgerEntry}
*/
private getBillTaxEntry = R.curry(
(
bill: IBill,
taxPayableAccountId: number,
entry: IItemEntry,
index: number
): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
return {
...commonJournalMeta,
debit: entry.taxAmount,
index,
indexGroup: 30,
accountId: taxPayableAccountId,
accountNormal: AccountNormal.CREDIT,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,
};
}
);
/**
* Retrieves the bill tax GL entries.
* @param {IBill} bill
* @param {number} taxPayableAccountId
* @returns {ILedgerEntry[]}
*/
private getBillTaxEntries = (bill: IBill, taxPayableAccountId: number) => {
// Retrieves the non-zero tax entries.
const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries(
bill.entries
);
const transformTaxEntry = this.getBillTaxEntry(bill, taxPayableAccountId);
return nonZeroTaxEntries.map(transformTaxEntry);
};
/**
* Retrieves the given bill GL entries.
* @param {IBill} bill
* @param {number} payableAccountId
* @param {IBill} bill
* @param {number} payableAccountId
* @returns {ILedgerEntry[]}
*/
private getBillGLEntries = (
bill: IBill,
payableAccountId: number
payableAccountId: number,
taxPayableAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
@@ -201,18 +260,28 @@ export class BillGLEntries {
const landedCostEntries = bill.locatedLandedCosts.map(
landedCostTransformer
);
const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId);
// Allocate cost entries journal entries.
return [payableEntry, ...itemsEntries, ...landedCostEntries];
return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries];
};
/**
* Retrieves the given bill ledger.
* @param {IBill} bill
* @param {number} payableAccountId
* @param {IBill} bill
* @param {number} payableAccountId
* @returns {Ledger}
*/
private getBillLedger = (bill: IBill, payableAccountId: number) => {
const entries = this.getBillGLEntries(bill, payableAccountId);
private getBillLedger = (
bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
) => {
const entries = this.getBillGLEntries(
bill,
payableAccountId,
taxPayableAccountId
);
return new Ledger(entries);
};

View File

@@ -28,7 +28,8 @@ export class GetBill {
.findById(billId)
.withGraphFetched('vendor')
.withGraphFetched('entries.item')
.withGraphFetched('branch');
.withGraphFetched('branch')
.withGraphFetched('taxes.taxRate');
// Validates the bill existance.
this.validators.validateBillExistance(bill);

View File

@@ -1,27 +1,42 @@
import { IBill } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { SaleInvoiceTaxEntryTransformer } from '@/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer';
import { formatNumber } from 'utils';
export class PurchaseInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* Include these attributes to sale bill object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedBillDate',
'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount',
'formattedBalance',
'formattedDueAmount',
'formattedExchangeRate',
'subtotalFormatted',
'subtotalLocalFormatted',
'subtotalExcludingTaxFormatted',
'taxAmountWithheldLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* Excluded attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['amount', 'amountLocal', 'localAmount'];
};
/**
* Retrieve formatted bill date.
* @param {IBill} bill
* @returns {String}
*/
protected formattedBillDate = (bill: IBill): string => {
@@ -29,8 +44,8 @@ export class PurchaseInvoiceTransformer extends Transformer {
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* Retrieve formatted bill date.
* @param {IBill} bill
* @returns {String}
*/
protected formattedDueDate = (bill: IBill): string => {
@@ -39,7 +54,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedAmount = (bill): string => {
@@ -48,7 +63,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedPaymentAmount = (bill): string => {
@@ -59,7 +74,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedDueAmount = (bill): string => {
@@ -77,10 +92,90 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve the formatted exchange rate.
* @param {ISaleInvoice} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedExchangeRate = (invoice): string => {
return formatNumber(invoice.exchangeRate, { money: false });
protected formattedExchangeRate = (bill): string => {
return formatNumber(bill.exchangeRate, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves the formatted subtotal.
* @param {IBill} bill
* @returns {string}
*/
protected subtotalFormatted = (bill): string => {
return formatNumber(bill.subtotal, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieves the local subtotal formatted.
* @param {IBill} bill
* @returns {string}
*/
protected subtotalLocalFormatted = (bill): string => {
return formatNumber(bill.subtotalLocal, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves the formatted subtotal tax excluded.
* @param {IBill} bill
* @returns {string}
*/
protected subtotalExcludingTaxFormatted = (bill): string => {
return formatNumber(bill.subtotalExludingTax, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieves the local formatted tax amount withheld
* @param {IBill} bill
* @returns {string}
*/
protected taxAmountWithheldLocalFormatted = (bill): string => {
return formatNumber(bill.taxAmountWithheldLocal, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves the total formatted.
* @param {IBill} bill
* @returns {string}
*/
protected totalFormatted = (bill): string => {
return formatNumber(bill.total, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieves the local total formatted.
* @param {IBill} bill
* @returns {string}
*/
protected totalLocalFormatted = (bill): string => {
return formatNumber(bill.totalLocal, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieve the taxes lines of bill.
* @param {Bill} bill
*/
protected taxes = (bill) => {
return this.item(bill.taxes, new SaleInvoiceTaxEntryTransformer(), {
subtotal: bill.subtotal,
isInclusiveTax: bill.isInclusiveTax,
currencyCode: bill.currencyCode,
});
};
}

View File

@@ -218,7 +218,8 @@ export class SaleInvoiceGLEntries {
...commonEntry,
credit: entry.taxAmount,
accountId: taxPayableAccountId,
index: index + 3,
index: index + 1,
indexGroup: 30,
accountNormal: AccountNormal.CREDIT,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,

View File

@@ -62,8 +62,8 @@ export class SaleInvoiceTaxEntryTransformer extends Transformer {
const taxRate = this.taxRate(taxEntry);
return this.options.isInclusiveTax
? getInclusiveTaxAmount(this.options.amount, taxRate)
: getExlusiveTaxAmount(this.options.amount, taxRate);
? getInclusiveTaxAmount(this.options.subtotal, taxRate)
: getExlusiveTaxAmount(this.options.subtotal, taxRate);
};
/**

View File

@@ -171,7 +171,7 @@ export class SaleInvoiceTransformer extends Transformer {
*/
protected taxes = (invoice) => {
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), {
amount: invoice.amount,
subtotal: invoice.subtotal,
isInclusiveTax: invoice.isInclusiveTax,
currencyCode: invoice.currencyCode,
});

View File

@@ -115,6 +115,7 @@ export class EditTaxRateService {
// Triggers `onTaxRateEdited` event.
await this.eventPublisher.emitAsync(events.taxRates.onEdited, {
editTaxRateDTO,
oldTaxRate,
taxRate,
tenantId,
trx,

View File

@@ -0,0 +1,55 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
@Service()
export class SyncItemTaxRateOnEditTaxRate {
@Inject()
private tenancy: HasTenancyService;
/**
* Syncs the new tax rate created to item default sell tax rate.
* @param {number} tenantId
* @param {number} itemId
* @param {number} sellTaxRateId
*/
public updateItemSellTaxRate = async (
tenantId: number,
oldSellTaxRateId: number,
sellTaxRateId: number,
trx?: Knex.Transaction
) => {
const { Item } = this.tenancy.models(tenantId);
// Can't continue if the old and new sell tax rate id are equal.
if (oldSellTaxRateId === sellTaxRateId) return;
await Item.query().where('sellTaxRateId', oldSellTaxRateId).update({
sellTaxRateId,
});
};
/**
* Syncs the new tax rate created to item default purchase tax rate.
* @param {number} tenantId
* @param {number} itemId
* @param {number} purchaseTaxRateId
*/
public updateItemPurchaseTaxRate = async (
tenantId: number,
oldPurchaseTaxRateId: number,
purchaseTaxRateId: number,
trx?: Knex.Transaction
) => {
const { Item } = this.tenancy.models(tenantId);
// Can't continue if the old and new sell tax rate id are equal.
if (oldPurchaseTaxRateId === purchaseTaxRateId) return;
await Item.query(trx)
.where('purchaseTaxRateId', oldPurchaseTaxRateId)
.update({
purchaseTaxRateId,
});
};
}

View File

@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { SyncItemTaxRateOnEditTaxRate } from './SyncItemTaxRateOnEditTaxRate';
import events from '@/subscribers/events';
import { ITaxRateEditedPayload } from '@/interfaces';
import { runAfterTransaction } from '../UnitOfWork/TransactionsHooks';
@Service()
export class SyncItemTaxRateOnEditTaxSubscriber {
@Inject()
private syncItemRateOnEdit: SyncItemTaxRateOnEditTaxRate;
/**
* Attaches events with handles.
*/
public attach(bus) {
bus.subscribe(
events.taxRates.onEdited,
this.handleSyncNewTaxRateToItemTaxRate
);
}
/**
* Syncs the new tax rate created to default item tax rates.
* @param {ITaxRateEditedPayload} payload -
*/
private handleSyncNewTaxRateToItemTaxRate = async ({
taxRate,
tenantId,
oldTaxRate,
trx,
}: ITaxRateEditedPayload) => {
runAfterTransaction(trx, async () => {
await this.syncItemRateOnEdit.updateItemPurchaseTaxRate(
tenantId,
oldTaxRate.id,
taxRate.id
);
await this.syncItemRateOnEdit.updateItemSellTaxRate(
tenantId,
oldTaxRate.id,
taxRate.id
);
});
};
}

View File

@@ -0,0 +1,89 @@
import { Inject, Service } from 'typedi';
import { IBillCreatingPayload, IBillEditingPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
@Service()
export class BillTaxRateValidateSubscriber {
@Inject()
private taxRateDTOValidator: CommandTaxRatesValidators;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.bill.onCreating,
this.validateBillEntriesTaxCodeExistanceOnCreating
);
bus.subscribe(
events.bill.onCreating,
this.validateBillEntriesTaxIdExistanceOnCreating
);
bus.subscribe(
events.bill.onEditing,
this.validateBillEntriesTaxCodeExistanceOnEditing
);
bus.subscribe(
events.bill.onEditing,
this.validateBillEntriesTaxIdExistanceOnEditing
);
return bus;
}
/**
* Validate bill entries tax rate code existance when creating.
* @param {IBillCreatingPayload}
*/
private validateBillEntriesTaxCodeExistanceOnCreating = async ({
billDTO,
tenantId,
}: IBillCreatingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
tenantId,
billDTO.entries
);
};
/**
* Validate the tax rate id existance when creating.
* @param {IBillCreatingPayload}
*/
private validateBillEntriesTaxIdExistanceOnCreating = async ({
billDTO,
tenantId,
}: IBillCreatingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
tenantId,
billDTO.entries
);
};
/**
* Validate bill entries tax rate code existance when editing.
* @param {IBillEditingPayload}
*/
private validateBillEntriesTaxCodeExistanceOnEditing = async ({
tenantId,
billDTO,
}: IBillEditingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
tenantId,
billDTO.entries
);
};
/**
* Validates the bill entries tax rate id existance when editing.
* @param {ISaleInvoiceEditingPayload} payload -
*/
private validateBillEntriesTaxIdExistanceOnEditing = async ({
tenantId,
billDTO,
}: IBillEditingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
tenantId,
billDTO.entries
);
};
}

View File

@@ -0,0 +1,87 @@
import { Inject, Service } from 'typedi';
import {
IBIllEventDeletedPayload,
IBillCreatedPayload,
IBillEditedPayload,
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { WriteTaxTransactionsItemEntries } from '../WriteTaxTransactionsItemEntries';
@Service()
export class WriteBillTaxTransactionsSubscriber {
@Inject()
private writeTaxTransactions: WriteTaxTransactionsItemEntries;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.bill.onCreated,
this.writeInvoiceTaxTransactionsOnCreated
);
bus.subscribe(
events.bill.onEdited,
this.rewriteInvoiceTaxTransactionsOnEdited
);
bus.subscribe(
events.bill.onDeleted,
this.removeInvoiceTaxTransactionsOnDeleted
);
return bus;
}
/**
* Writes the bill tax transactions on invoice created.
* @param {ISaleInvoiceCreatingPaylaod}
*/
private writeInvoiceTaxTransactionsOnCreated = async ({
tenantId,
bill,
trx,
}: IBillCreatedPayload) => {
await this.writeTaxTransactions.writeTaxTransactionsFromItemEntries(
tenantId,
bill.entries,
trx
);
};
/**
* Rewrites the bill tax transactions on invoice edited.
* @param {IBillEditedPayload} payload -
*/
private rewriteInvoiceTaxTransactionsOnEdited = async ({
tenantId,
bill,
trx,
}: IBillEditedPayload) => {
await this.writeTaxTransactions.rewriteTaxRateTransactionsFromItemEntries(
tenantId,
bill.entries,
'Bill',
bill.id,
trx
);
};
/**
* Removes the invoice tax transactions on invoice deleted.
* @param {IBIllEventDeletedPayload}
*/
private removeInvoiceTaxTransactionsOnDeleted = async ({
tenantId,
oldBill,
trx,
}: IBIllEventDeletedPayload) => {
await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries(
tenantId,
oldBill.id,
'Bill',
trx
);
};
}

View File

@@ -1,87 +0,0 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import BillsService from '@/services/Purchases/Bills';
import {
IBillCreatedPayload,
IBillEditedPayload,
IBIllEventDeletedPayload,
} from '@/interfaces';
@Service()
export default class BillWriteGLEntriesSubscriber {
@Inject()
tenancy: TenancyService;
@Inject()
billsService: BillsService;
/**
* Attaches events with handles.
*/
public attach(bus) {
bus.subscribe(
events.bill.onCreated,
this.handlerWriteJournalEntriesOnCreate
);
bus.subscribe(
events.bill.onEdited,
this.handleOverwriteJournalEntriesOnEdit
);
bus.subscribe(events.bill.onDeleted, this.handlerDeleteJournalEntries);
}
/**
* Handles writing journal entries once bill created.
* @param {IBillCreatedPayload} payload -
*/
private handlerWriteJournalEntriesOnCreate = async ({
tenantId,
billId,
bill,
trx,
}: IBillCreatedPayload) => {
// Can't continue if the bill is not opened yet.
if (!bill.openedAt) return null;
await this.billsService.recordJournalTransactions(
tenantId,
billId,
false,
trx
);
};
/**
* Handles the overwriting journal entries once bill edited.
* @param {IBillEditedPayload} payload -
*/
private handleOverwriteJournalEntriesOnEdit = async ({
tenantId,
billId,
bill,
trx,
}: IBillEditedPayload) => {
// Can't continue if the bill is not opened yet.
if (!bill.openedAt) return null;
await this.billsService.recordJournalTransactions(
tenantId,
billId,
true,
trx
);
};
/**
* Handles revert journal entries on bill deleted.
* @param {IBIllEventDeletedPayload} payload -
*/
private handlerDeleteJournalEntries = async ({
tenantId,
billId,
trx,
}: IBIllEventDeletedPayload) => {
await this.billsService.revertJournalEntries(tenantId, billId, trx);
};
}

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('tenants_metadata', (table) => {
table.string('tax_number')
});
};
exports.down = function (knex) {
return knex.schema.table('tenants_metadata', (table) => {
table.dropColumn('tax_number');
});
};

View File

@@ -1 +0,0 @@
@bigcapitalhq:registry=https://npm.pkg.github.com

View File

@@ -1,18 +1,7 @@
const path = require('path');
const webpack = require('webpack');
const dotenv = require('dotenv-webpack');
module.exports = {
webpack: {
plugins: [
new dotenv(),
new webpack.DefinePlugin({
'process.env': {
MONOREPO_VERSION: JSON.stringify(require('../../lerna.json').version),
},
}),
],
alias: {
'@': path.resolve(__dirname, 'src'),
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,18 @@
{
"name": "@bigcapital/webapp",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"dependencies": {
"@blueprintjs-formik/core": "^0.3.4",
"@blueprintjs-formik/datetime": "^0.3.4",
"@blueprintjs-formik/select": "^0.3.2",
"@blueprintjs/core": "^3.50.2",
"@blueprintjs/datetime": "^3.23.12",
"@blueprintjs-formik/core": "^0.3.6",
"@blueprintjs-formik/datetime": "^0.3.7",
"@blueprintjs-formik/select": "^0.3.5",
"@blueprintjs/colors": "4.1.19",
"@blueprintjs/core": "^4.20.2",
"@blueprintjs/datetime": "^4.4.37",
"@blueprintjs/popover2": "^0.11.1",
"@blueprintjs/select": "^3.11.2",
"@blueprintjs/table": "^3.8.3",
"@blueprintjs/timezone": "^3.6.2",
"@blueprintjs/select": "^4.9.24",
"@blueprintjs/table": "^4.10.12",
"@blueprintjs/timezone": "^4.5.43",
"@casl/ability": "^5.4.3",
"@casl/react": "^2.3.0",
"@craco/craco": "^5.9.0",
@@ -26,6 +27,7 @@
"@types/ramda": "^0.28.14",
"@types/react": "^16.14.28",
"@types/react-body-classname": "^1.1.7",
"@types/react-dom": "^16.9.16",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.5",
@@ -38,44 +40,49 @@
"axios": "^0.21.2",
"basscss": "^8.0.2",
"camelcase": "^5.3.1",
"classnames": "^2.3.2",
"cross-env": "^7.0.2",
"deep-map-keys": "^2.0.1",
"deepdash": "^5.3.9",
"dependency-graph": "^0.11.0",
"dotenv-webpack": "^8.0.1",
"eslint": "^8.33.0",
"fast-deep-equal": "^3.1.3",
"flat": "^5.0.2",
"formik": "^2.2.5",
"history": "4.10.1",
"http-proxy-middleware": "^1.0.0",
"jest": "24.9.0",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-resolve": "24.9.0",
"jest-watch-typeahead": "0.4.2",
"js-cookie": "2.2.1",
"js-money": "^0.6.3",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"moment-timezone": "^0.5.33",
"node-sass": "^4.14.1",
"path-browserify": "^1.0.1",
"prop-types": "15.8.1",
"query-string": "^7.1.1",
"ramda": "^0.27.1",
"react": "^16.14.0",
"react": "^18.2.0",
"react-app-polyfill": "^1.0.6",
"react-body-classname": "^1.3.1",
"react-content-loader": "^6.0.1",
"react-dev-utils": "^11.0.4",
"react-dom": "^16.12.0",
"react-dom": "^18.2.0",
"react-dropzone": "^11.0.1",
"react-error-boundary": "^3.0.2",
"react-error-overlay": "^6.0.9",
"react-hotkeys-hook": "^3.0.3",
"react-intl-universal": "^2.4.7",
"react-loadable": "^5.5.0",
"react-query": "^3.6.0",
"react-query-devtools": "^2.1.1",
"react-redux": "^7.1.3",
"react-redux": "^7.2.9",
"react-router": "5.3.4",
"react-router-breadcrumbs-hoc": "^3.2.10",
"react-router-dom": "^5.3.3",
"react-scripts": "^3.4.4",
"react-scripts": "5.0.1",
"react-scroll-sync": "^0.7.1",
"react-scrollbars-custom": "^4.0.21",
"react-sortablejs": "^2.0.11",
@@ -86,11 +93,13 @@
"react-use": "^13.26.1",
"react-use-context-menu": "^0.1.4",
"react-virtualized": "^9.22.3",
"redux": "^4.0.5",
"redux": "^4.2.1",
"redux-devtools": "^3.5.0",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"redux-thunk": "^2.4.2",
"reselect": "4.1.7",
"rtl-detect": "^1.0.3",
"sass": "^1.68.0",
"semver": "6.3.0",
"style-loader": "0.23.1",
"styled-components": "^5.3.1",
@@ -105,13 +114,15 @@
"storybook": "start-storybook -p 6006"
},
"proxy": "http://localhost:3000/",
"devDependencies": {
"@types/react-dom": "^16.9.16",
"react-error-overlay": "^6.0.9"
},
"resolutions": {
"react-error-overlay": "6.0.9"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import { FSelect } from '@/components/Forms';
export function AccountsTypesSelect({ ...props }) {

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
// Filters accounts items.
export const accountPredicate = (query, account, _index, exactMatch) => {

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import React from 'react';
import { Formik, FastField, FieldArray, useFormikContext } from 'formik';
import { Formik, FastField, FieldArray } from 'formik';
import {
Button,
FormGroup,

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import { Router, Switch, Route } from 'react-router';
import { createBrowserHistory } from 'history';
import { QueryClientProvider, QueryClient } from 'react-query';

View File

@@ -18,10 +18,10 @@ export const CardFooterActions = styled.div`
border-top: 1px solid #e0e7ea;
margin-top: 30px;
.bp3-button {
.bp4-button {
min-width: 70px;
+ .bp3-button {
+ .bp4-button {
margin-left: 10px;
}
}

View File

@@ -1,6 +1,6 @@
.menu{
:global .bp3-heading{
:global .bp4-heading{
font-weight: 400;
opacity: 0.5;
font-size: 12px;

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import BigcapitalLoading from './BigcapitalLoading';
import withDashboard from '@/containers/Dashboard/withDashboard';

View File

@@ -21,7 +21,7 @@ export function DataTableEditable({
}
const DatatableEditableRoot = styled.div`
.bp3-form-group {
.bp4-form-group {
margin-bottom: 0;
}
.table {
@@ -69,17 +69,17 @@ const DatatableEditableRoot = styled.div`
}
.tr {
&:hover .td,
.bp3-input {
.bp4-input {
background-color: transparent;
}
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
.form-group--select-list .bp3-button {
.bp4-form-group:not(.bp4-intent-danger) .bp4-input,
.form-group--select-list .bp4-button {
border-color: #ffffff;
color: #222;
border-radius: 3px;
text-align: inherit;
}
.bp3-form-group:not(.bp3-intent-danger) .bp3-input {
.bp4-form-group:not(.bp4-intent-danger) .bp4-input {
border-radius: 2px;
padding-left: 14px;
padding-right: 14px;
@@ -88,7 +88,7 @@ const DatatableEditableRoot = styled.div`
box-shadow: 0 0 0 2px #116cd0;
}
}
.form-group--select-list .bp3-button {
.form-group--select-list .bp4-button {
padding-left: 6px;
padding-right: 6px;
@@ -97,16 +97,16 @@ const DatatableEditableRoot = styled.div`
}
}
.form-group--select-list,
.bp3-form-group {
&.bp3-intent-danger {
.bp3-button:not(.bp3-minimal),
.bp3-input {
.bp4-form-group {
&.bp4-intent-danger {
.bp4-button:not(.bp4-minimal),
.bp4-input {
border-color: #f7b6b6;
}
}
}
.td.actions {
.bp3-button {
.bp4-button {
color: #80858f;
}
}

View File

@@ -35,7 +35,7 @@ const DialogFooterActionsRoot = styled.div`
${(props) =>
props.alignment === 'right' ? 'margin-left: auto;' : 'margin-right: auto;'};
.bp3-button {
.bp4-button {
margin-left: 5px;
margin-left: 5px;
}

View File

@@ -1,5 +1,3 @@
import React from 'react';
import AccountDialog from '@/containers/Dialogs/AccountDialog';
import InviteUserDialog from '@/containers/Dialogs/InviteUserDialog';
import UserFormDialog from '@/containers/Dialogs/UserFormDialog';

View File

@@ -17,8 +17,8 @@ export function DrawerMainTabs({ children, ...restProps }) {
}
const DrawerMainTabsRoot = styled.div`
.bp3-tabs {
.bp3-tab-list {
.bp4-tabs {
.bp4-tab-list {
position: relative;
background-color: #fff;
padding: 0 15px;
@@ -28,7 +28,7 @@ const DrawerMainTabsRoot = styled.div`
margin-right: 25px;
}
&.bp3-large > .bp3-tab {
&.bp4-large > .bp4-tab {
font-size: 15px;
color: #7f8596;
margin: 0 1rem;
@@ -38,13 +38,13 @@ const DrawerMainTabsRoot = styled.div`
color: #0052cc;
}
}
.bp3-tab-indicator-wrapper .bp3-tab-indicator {
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
height: 2px;
bottom: -2px;
}
}
.bp3-tab-panel {
.bp4-tab-panel {
margin-top: 0;
.card {

View File

@@ -1,5 +1,3 @@
import React from 'react';
import AccountDrawer from '@/containers/Drawers/AccountDrawer';
import ManualJournalDrawer from '@/containers/Drawers/ManualJournalDrawer';
import ExpenseDrawer from '@/containers/Drawers/ExpenseDrawer';

View File

@@ -60,8 +60,8 @@ export function ExchangeRateMutedField({
}
const ExchangeRateFormGroup = styled(FormGroup)`
&.bp3-form-group {
label.bp3-label {
&.bp4-form-group {
label.bp4-label {
font-size: 12px;
opacity: 0.7;
line-height: 1;
@@ -79,7 +79,7 @@ const ExchangeRateButton = styled.div`
position: relative;
padding-right: 28px;
.bp3-button {
.bp4-button {
position: absolute;
right: 0;
}
@@ -88,7 +88,7 @@ const ExchangeRateButton = styled.div`
const ExchangeRateFormGroupContent = styled.div`
padding: 5px 0;
.bp3-form-group {
.bp4-form-group {
padding: 2px;
margin: 2px 4px !important;
}

View File

@@ -26,7 +26,11 @@ const SelectButton = styled(Button)`
position: relative;
padding-right: 30px;
&:not(.is-selected):not([class*='bp3-intent-']):not(.bp3-minimal) {
&.bp4-small{
padding-right: 24px;
}
&:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) {
color: #5c7080;
}
&:after {
@@ -46,13 +50,13 @@ const SelectButton = styled(Button)`
margin-right: 12px;
border-radius: 1px;
}
&:not([class*='bp3-intent-']):not(.bp3-disabled) {
&:not([class*='bp4-intent-']):not(.bp4-disabled) {
&,
&:hover {
background: #fff;
}
}
.bp3-intent-danger & {
.bp4-intent-danger & {
border-color: #db3737;
}
`;

View File

@@ -2,5 +2,5 @@
import React from 'react';
export function MenuItemLabel({ text }) {
return <span class="bp3-menu-item-labeler">{text}</span>;
return <span class="bp4-menu-item-labeler">{text}</span>;
}

View File

@@ -18,16 +18,16 @@ const FormTopBarRoot = styled(Navbar)`
height: 35px;
padding: 0 20px;
.bp3-navbar-group {
.bp4-navbar-group {
height: 35px;
}
.bp3-navbar-divider {
.bp4-navbar-divider {
border-left-color: #d2dce2;
}
.bp3-skeleton {
.bp4-skeleton {
max-height: 10px;
}
.bp3-button {
.bp4-button {
&:hover {
background: rgba(167, 182, 194, 0.12);
color: #32304a;

View File

@@ -0,0 +1,62 @@
// @ts-nocheck
import * as R from 'ramda';
import intl from 'react-intl-universal';
import { FSelect } from '@/components';
import { DialogsName } from '@/constants/dialogs';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { MenuItem } from '@blueprintjs/core';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => ({ name });
/**
* Tax rates select field binded with Formik form.
* @returns {JSX.Element}
*/
function TaxRatesSelectRoot({
// #withDialogActions
openDialog,
// #ownProps
allowCreate,
...restProps
}) {
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
// Handles the create item click.
const handleCreateItemClick = () => {
openDialog(DialogsName.TaxRateForm);
};
return (
<FSelect
valueAccessor={'id'}
labelAccessor={'code'}
textAccessor={'name_formatted'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
onCreateItemSelect={handleCreateItemClick}
{...restProps}
/>
);
}
export const TaxRatesSelect = R.compose(withDialogActions)(TaxRatesSelectRoot);

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import PropTypes from 'prop-types';
export const For = ({ render, of }) =>

View File

@@ -0,0 +1,7 @@
import { TaxType } from '@/interfaces/TaxRates';
export const InclusiveTaxOptions = [
{ key: TaxType.Inclusive, label: 'Inclusive of Tax' },
{ key: TaxType.Exclusive, label: 'Exclusive of Tax' },
];

View File

@@ -22,12 +22,12 @@ export function MakeJournalFormFooterLeft() {
}
const DescriptionFormGroup = styled(FFormGroup)`
&.bp3-form-group {
.bp3-label {
&.bp4-form-group {
.bp4-label {
font-size: 12px;
margin-bottom: 12px;
}
.bp3-form-content {
.bp4-form-content {
margin-left: 10px;
}
}

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import { Formik } from 'formik';
import { Link } from 'react-router-dom';

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { Link } from 'react-router-dom';

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { Spinner } from '@blueprintjs/core';
@@ -39,7 +40,7 @@ export const AuthInsiderCopyright = styled.div`
color: #666;
margin-top: 1.2rem;
.bp3-icon-bigcapital {
.bp4-icon-bigcapital {
svg {
path {
fill: #a3a3a3;
@@ -65,11 +66,11 @@ export const AuthFooterLink = styled.p`
export const AuthSubmitButton = styled(Button)`
margin-top: 20px;
&.bp3-intent-primary {
&.bp4-intent-primary {
background-color: #0052cc;
&:disabled,
&.bp3-disabled {
&.bp4-disabled {
background-color: rgba(0, 82, 204, 0.4);
}
}

View File

@@ -178,7 +178,7 @@ const AccountSwitchItemUpdatedAt = styled.div`
`;
const AccountSwitchButtonBase = styled(Button)`
.bp3-button-text {
.bp4-button-text {
margin-right: 5px;
}
`;

View File

@@ -34,10 +34,10 @@ export function Sidebar() {
* @returns {React.JSX}
*/
function SidebarFooterVersion() {
const { MONOREPO_VERSION } = process.env;
const { REACT_APP_VERSION } = process.env;
if (!MONOREPO_VERSION) {
if (!REACT_APP_VERSION) {
return null;
}
return <div class="sidebar__version">v{MONOREPO_VERSION}</div>;
return <div class="sidebar__version">v{REACT_APP_VERSION}</div>;
}

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import { Button, Popover, Menu, Position } from '@blueprintjs/core';
import { Icon } from '@/components';

View File

@@ -30,7 +30,7 @@ export default function BillDetailHeader() {
<CommercialDocTopHeader>
<DetailsMenu>
<AmountDetailItem label={intl.get('amount')}>
<h3 class="big-number">{bill.formatted_amount}</h3>
<h3 class="big-number">{bill.total_formatted}</h3>
</AmountDetailItem>
<StatusDetailItem>
<BillDetailsStatus bill={bill} />

View File

@@ -1,11 +1,8 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import {
TotalLineBorderStyle,
TotalLineTextStyle,
FormatNumber,
T,
TotalLines,
TotalLine,
@@ -23,12 +20,20 @@ export function BillDetailTableFooter() {
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine
title={<T id={'bill.details.subtotal'} />}
value={<FormatNumber value={bill.amount} />}
value={bill.subtotal_formatted}
borderStyle={TotalLineBorderStyle.SingleDark}
/>
{bill.taxes.map((taxRate) => (
<TotalLine
key={taxRate.id}
title={`${taxRate.name} [${taxRate.tax_rate}%]`}
value={taxRate.tax_rate_amount_formatted}
textStyle={TotalLineTextStyle.Regular}
/>
))}
<TotalLine
title={<T id={'bill.details.total'} />}
value={bill.formatted_amount}
value={bill.total_formatted}
borderStyle={TotalLineBorderStyle.DoubleDark}
textStyle={TotalLineTextStyle.Bold}
/>
@@ -39,6 +44,7 @@ export function BillDetailTableFooter() {
<TotalLine
title={<T id={'bill.details.due_amount'} />}
value={bill.formatted_due_amount}
textStyle={TotalLineTextStyle.Bold}
/>
</BillTotalLines>
</BillDetailsFooterRoot>

View File

@@ -67,6 +67,14 @@ export default function ItemDetailHeader() {
label={intl.get('cost_account_id')}
children={defaultTo(item.cost_account?.name, '-')}
/>
<DetailItem
label={intl.get('item.details.sell_tax_rate')}
children={item?.sell_tax_rate?.name}
/>
<DetailItem
label={intl.get('item.details.purchase_tax_rate')}
children={item?.purchase_tax_rate?.name}
/>
<If condition={item.type === 'inventory'}>
<DetailItem
label={intl.get('inventory_account')}

View File

@@ -53,7 +53,7 @@ export const ItemManuTransaction = ({ onChange }) => {
};
const ItemSwitchButton = styled(Button)`
.bp3-button-text {
.bp4-button-text {
display: flex;
color: #727983;
}

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { defaultTo } from 'lodash';

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { CommercialDocBox } from '@/components';

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { Card } from '@/components';

View File

@@ -0,0 +1,16 @@
import { Box } from '@/components';
import styled from 'styled-components';
export const EntriesActionsBar = styled(Box)`
padding-bottom: 12px;
display: flex;
.bp4-form-group {
margin-bottom: 0;
label.bp4-label {
opacity: 0.6;
margin-right: 8px;
}
}
`;

View File

@@ -8,20 +8,30 @@ import { DataTableEditable } from '@/components';
import { useEditableItemsEntriesColumns } from './components';
import {
useFetchItemRow,
composeRowsOnNewRow,
useComposeRowsOnEditTableCell,
useComposeRowsOnRemoveTableRow,
useComposeRowsOnNewRow,
} from './utils';
import {
ItemEntriesTableProvider,
useItemEntriesTableContext,
} from './ItemEntriesTableProvider';
import { useUncontrolled } from '@/hooks/useUncontrolled';
import { ItemEntry } from '@/interfaces/ItemEntries';
interface ItemsEntriesTableProps {
initialValue?: ItemEntry;
value?: ItemEntry[];
onChange?: (entries: ItemEntry[]) => void;
taxRates?: any[];
minLinesNumber?: number;
enableTaxRates?: boolean;
}
/**
* Items entries table.
*/
function ItemsEntriesTable(props) {
function ItemsEntriesTable(props: ItemsEntriesTableProps) {
const { value, initialValue, onChange } = props;
const [localValue, handleChange] = useUncontrolled({
@@ -51,6 +61,7 @@ function ItemEntriesTableRoot() {
currencyCode,
landedCost,
taxRates,
itemType,
} = useItemEntriesTableContext();
// Editiable items entries columns.
@@ -58,11 +69,12 @@ function ItemEntriesTableRoot() {
const composeRowsOnEditCell = useComposeRowsOnEditTableCell();
const composeRowsOnDeleteRow = useComposeRowsOnRemoveTableRow();
const composeRowsOnNewRow = useComposeRowsOnNewRow();
// Handle the fetch item row details.
const { setItemRow, cellsLoading, isItemFetching } = useFetchItemRow({
landedCost,
itemType: null,
itemType,
notifyNewRow: (newRow, rowIndex) => {
// Update the rate, description and quantity data of the row.
const newRows = composeRowsOnNewRow(rowIndex, newRow, localValue);
@@ -119,8 +131,11 @@ ItemsEntriesTable.defaultProps = {
discount: '',
},
initialEntries: [],
taxRates: [],
items: [],
linesNumber: 1,
minLinesNumber: 1,
enableTaxRates: true,
};
export default ItemsEntriesTable;

View File

@@ -92,7 +92,7 @@ const LandedCostHeaderCell = () => {
*/
export function useEditableItemsEntriesColumns() {
const { featureCan } = useFeatureCan();
const { landedCost } = useItemEntriesTableContext();
const { landedCost, enableTaxRates } = useItemEntriesTableContext();
const isProjectsFeatureEnabled = featureCan(Features.Projects);
@@ -132,13 +132,17 @@ export function useEditableItemsEntriesColumns() {
width: 70,
align: Align.Right,
},
{
Header: 'Tax rate',
accessor: 'tax_rate_id',
Cell: TaxRatesSuggestInputCell,
disableSortBy: true,
width: 110,
},
...(enableTaxRates
? [
{
Header: 'Tax rate',
accessor: 'tax_rate_id',
Cell: TaxRatesSuggestInputCell,
disableSortBy: true,
width: 110,
},
]
: []),
{
Header: intl.get('discount'),
accessor: 'discount',

View File

@@ -1,8 +1,7 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import * as R from 'ramda';
import { sumBy, isEmpty, last, keyBy } from 'lodash';
import { sumBy, isEmpty, last, keyBy, groupBy } from 'lodash';
import { useItem } from '@/hooks/query';
import {
toSafeNumber,
@@ -12,6 +11,7 @@ import {
updateAutoAddNewLine,
orderingLinesIndexes,
updateTableRow,
formattedAmount,
} from '@/utils';
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
@@ -119,17 +119,24 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
// Detarmines whether the landed cost checkbox should be disabled.
const landedCostDisabled = isLandedCostDisabled(item);
const taxRateId =
itemType === ITEM_TYPE.PURCHASABLE
? item.purchase_tax_rate_id
: item.sell_tax_rate_id;
// The new row.
const newRow = {
rate: price,
description,
quantity: 1,
tax_rate_id: taxRateId,
...(landedCost
? {
landed_cost: false,
landed_cost_disabled: landedCostDisabled,
}
: {}),
taxRateId,
};
setItemRow(null);
saveInvoke(notifyNewRow, newRow, rowIndex);
@@ -158,13 +165,21 @@ export const composeRowsOnEditCell = R.curry(
/**
* Compose table rows when insert a new row to table rows.
*/
export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
return compose(
orderingLinesIndexes,
updateItemsEntriesTotal,
updateTableRow(rowIndex, newRow),
)(rows);
});
export const useComposeRowsOnNewRow = () => {
const { taxRates, isInclusiveTax } = useItemEntriesTableContext();
return React.useMemo(() => {
return R.curry((rowIndex, newRow, rows) => {
return compose(
assignEntriesTaxAmount(isInclusiveTax),
assignEntriesTaxRate(taxRates),
orderingLinesIndexes,
updateItemsEntriesTotal,
updateTableRow(rowIndex, newRow),
)(rows);
});
}, [isInclusiveTax, taxRates]);
};
/**
* Associate tax rate to entries.
@@ -266,3 +281,29 @@ export const useComposeRowsOnRemoveTableRow = () => {
[minLinesNumber, defaultEntry, localValue],
);
};
/**
* Retrieves the aggregate tax rates from the given item entries.
*/
export const aggregateItemEntriesTaxRates = R.curry((taxRates, entries) => {
const taxRatesById = keyBy(taxRates, 'id');
// Calculate the total tax amount of invoice entries.
const filteredEntries = entries.filter((e) => e.tax_rate_id);
const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id');
return Object.keys(groupedTaxRates).map((taxRateId) => {
const taxRate = taxRatesById[taxRateId];
const taxRates = groupedTaxRates[taxRateId];
const totalTaxAmount = sumBy(taxRates, 'tax_amount');
const taxAmountFormatted = formattedAmount(totalTaxAmount, 'USD');
return {
taxRateId,
taxRate: taxRate.rate,
label: `${taxRate.name} [${taxRate.rate}%]`,
taxAmount: totalTaxAmount,
taxAmountFormatted,
};
});
});

View File

@@ -20,12 +20,12 @@ export function ExpenseFormFooterLeft() {
);
}
const DescriptionFormGroup = styled(FFormGroup)`
&.bp3-form-group {
.bp3-label {
&.bp4-form-group {
.bp4-label {
font-size: 12px;
margin-bottom: 12px;
}
.bp3-form-content {
.bp4-form-content {
margin-left: 10px;
}
}

View File

@@ -110,7 +110,7 @@ export default compose(
)(APAgingSummaryHeader);
const APAgingDrawerHeader = styled(FinancialStatementHeader)`
.bp3-drawer {
.bp4-drawer {
max-height: 520px;
}
`;

View File

@@ -14,12 +14,11 @@ import classNames from 'classnames';
import NumberFormatDropdown from '@/components/NumberFormatDropdown';
import { safeInvoke } from '@blueprintjs/core/lib/esm/common/utils';
import { useARAgingSummaryContext } from './ARAgingSummaryProvider';
import withARAgingSummaryActions from './withARAgingSummaryActions';
import withARAgingSummary from './withARAgingSummary';
import { compose } from '@/utils';
import { compose, safeInvoke } from '@/utils';
/**
* A/R Aging summary sheet - Actions bar.

View File

@@ -116,7 +116,7 @@ export default compose(
)(ARAgingSummaryHeader);
const ARAgingDrawerHeader = styled(FinancialStatementHeader)`
.bp3-drawer {
.bp4-drawer {
max-height: 520px;
}
`;

View File

@@ -125,7 +125,7 @@ export default compose(
)(BalanceSheetHeader);
const BalanceSheetFinancialHeader = styled(FinancialStatementHeader)`
.bp3-drawer {
.bp4-drawer {
max-height: 520px;
}
`;

View File

@@ -159,7 +159,7 @@ export default function BalanceSheetHeaderComparisonPanal() {
}
const BalanceSheetComparisonWrap = styled.div`
.bp3-form-group {
.bp4-form-group {
margin-bottom: 3px;
}
`;

View File

@@ -62,6 +62,7 @@ const BalanceSheetDataTable = styled(ReportDataTable)`
border-bottom: 0;
padding-top: 0.32rem;
padding-bottom: 0.32rem;
color: #252A31;
}
&.is-expanded {
.td:not(.name) .cell-inner {
@@ -72,6 +73,7 @@ const BalanceSheetDataTable = styled(ReportDataTable)`
.td {
font-weight: 500;
border-top: 1px solid #bbb;
color: #000;
}
}

View File

@@ -1,6 +1,4 @@
// @ts-nocheck
import React from 'react';
import { Row, Col } from '@/components';
import FinancialStatementDateRange from '../FinancialStatementDateRange';
import FinancialStatementsFilter from '../FinancialStatementsFilter';

View File

@@ -60,9 +60,8 @@ const CashflowStatementDataTable = styled(DataTable)`
border-bottom: 0;
padding-top: 0.32rem;
padding-bottom: 0.32rem;
color: #252a31;
}
// &.row-type--AGGREGATE,
&.row_type--ACCOUNTS {
border-top: 1px solid #bbb;
}
@@ -72,6 +71,9 @@ const CashflowStatementDataTable = styled(DataTable)`
&.row_type--TOTAL {
font-weight: 500;
.td {
color: #000;
}
&:not(:first-child) .td {
border-top: 1px solid #bbb;
}

Some files were not shown because too many files have changed in this diff Show More