Compare commits

..

10 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
c498a100bc Revert "creates kubernetes friendly deployment"
This reverts commit b42153bcc4.
2023-10-25 20:59:45 +02:00
Robert Koch
d313774205 adds tax number to organization 2023-10-12 12:42:36 +11:00
Robert Koch
b42153bcc4 creates kubernetes friendly deployment 2023-10-12 11:07:11 +11: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
99 changed files with 15097 additions and 353 deletions

View File

@@ -2,5 +2,6 @@
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent", "version": "independent",
"npmClient": "pnpm", "npmClient": "pnpm",
"useWorkspaces": true,
"packages": ["packages/*"] "packages": ["packages/*"]
} }

View File

@@ -2,7 +2,6 @@
"name": "bigcapital-monorepo", "name": "bigcapital-monorepo",
"private": true, "private": true,
"scripts": { "scripts": {
"bootstrap": "lerna exec pnpm install",
"dev": "lerna run dev", "dev": "lerna run dev",
"build": "lerna run build", "build": "lerna run build",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"", "dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
@@ -23,7 +22,7 @@
"lerna": "^6.4.1" "lerna": "^6.4.1"
}, },
"engines": { "engines": {
"node": "14.x" "node": "16.x || 17.x || 18.x"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

13663
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", "name": "@bigcapital/server",
"version": "0.10.1", "version": "0.10.2",
"description": "", "description": "",
"main": "src/server.ts", "main": "src/server.ts",
"scripts": { "scripts": {

View File

@@ -149,6 +149,11 @@ export default class ItemsController extends BaseController {
.trim() .trim()
.escape() .escape()
.isLength({ max: DATATYPES_LENGTH.TEXT }), .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') check('category_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .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); next(error);
} }

View File

@@ -65,6 +65,7 @@ export default class OrganizationController extends BaseController {
check('fiscal_year').exists().isIn(MONTHS), check('fiscal_year').exists().isIn(MONTHS),
check('language').exists().isString().isIn(ACCEPTED_LOCALES), check('language').exists().isString().isIn(ACCEPTED_LOCALES),
check('date_format').optional().isIn(DATE_FORMATS), check('date_format').optional().isIn(DATE_FORMATS),
check('tax_number').optional({ nullable: true }).isString().trim().escape(),
]; ];
} }

View File

@@ -115,6 +115,8 @@ export default class BillsController extends BaseController {
check('note').optional().trim().escape(), check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(), check('open').default(false).isBoolean().toBoolean(),
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),
check('entries').isArray({ min: 1 }), check('entries').isArray({ min: 1 }),
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
@@ -137,6 +139,15 @@ export default class BillsController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isNumeric() .isNumeric()
.toInt(), .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); 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 { IDynamicListFilterDTO } from './DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IBillLandedCost } from './LandedCost'; import { IBillLandedCost } from './LandedCost';
export interface IBillDTO { export interface IBillDTO {
vendorId: number; vendorId: number;
billNumber: string; billNumber: string;
@@ -15,10 +16,10 @@ export interface IBillDTO {
exchangeRate?: number; exchangeRate?: number;
open: boolean; open: boolean;
entries: IItemEntryDTO[]; entries: IItemEntryDTO[];
branchId?: number; branchId?: number;
warehouseId?: number; warehouseId?: number;
projectId?: number; projectId?: number;
isInclusiveTax?: boolean;
} }
export interface IBillEditDTO { export interface IBillEditDTO {
@@ -80,6 +81,15 @@ export interface IBill {
localAmount?: number; localAmount?: number;
locatedLandedCosts?: IBillLandedCost[]; locatedLandedCosts?: IBillLandedCost[];
amountLocal: number;
subtotal: number;
subtotalLocal: number;
subtotalExcludingTax: number;
taxAmountWithheld: number;
taxAmountWithheldLocal: number;
total: number;
totalLocal: number;
} }
export interface IBillsFilter extends IDynamicListFilterDTO { export interface IBillsFilter extends IDynamicListFilterDTO {

View File

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

View File

@@ -6,6 +6,7 @@ export interface IOrganizationSetupDTO {
fiscalYear: string; fiscalYear: string;
industry: string; industry: string;
timeZone: string; timeZone: string;
taxNumber: string;
} }
export interface IOrganizationBuildDTO { export interface IOrganizationBuildDTO {
@@ -16,6 +17,7 @@ export interface IOrganizationBuildDTO {
timezone: string; timezone: string;
fiscalYear: string; fiscalYear: string;
dateFormat?: string; dateFormat?: string;
taxNumber: string;
} }
export interface IOrganizationUpdateDTO { export interface IOrganizationUpdateDTO {
@@ -25,6 +27,7 @@ export interface IOrganizationUpdateDTO {
timezone: string; timezone: string;
fiscalYear: string; fiscalYear: string;
industry: string; industry: string;
taxNumber: string;
} }
export interface IOrganizationBuildEventPayload { export interface IOrganizationBuildEventPayload {

View File

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

View File

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

View File

@@ -13,6 +13,109 @@ export default class Bill extends mixin(TenantModel, [
CustomViewBaseModel, CustomViewBaseModel,
ModelSearchable, 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 * 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. * Invoice amount in organization base currency.
* @deprecated
* @returns {number} * @returns {number}
*/ */
get localAmount() { get localAmount() {
return this.amount * this.exchangeRate; return this.amountLocal;
} }
/** /**
@@ -231,7 +307,7 @@ export default class Bill extends mixin(TenantModel, [
* @return {number} * @return {number}
*/ */
get dueAmount() { 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} * @return {boolean}
*/ */
get isPartiallyPaid() { 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. * Retrieves the calculated amount which have not been invoiced.
*/ */
get billableAmount() { 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 ItemEntry = require('models/ItemEntry');
const BillLandedCost = require('models/BillLandedCost'); const BillLandedCost = require('models/BillLandedCost');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const TaxRateTransaction = require('models/TaxRateTransaction');
return { return {
vendor: { vendor: {
@@ -373,6 +450,21 @@ export default class Bill extends mixin(TenantModel, [
to: 'branches.id', 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 ItemEntry = require('models/ItemEntry');
const WarehouseTransferEntry = require('models/WarehouseTransferEntry'); const WarehouseTransferEntry = require('models/WarehouseTransferEntry');
const InventoryAdjustmentEntry = require('models/InventoryAdjustmentEntry'); const InventoryAdjustmentEntry = require('models/InventoryAdjustmentEntry');
const TaxRate = require('models/TaxRate');
return { return {
/** /**
@@ -178,6 +179,30 @@ export default class Item extends mixin(TenantModel, [
to: 'media.id', 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',
},
},
}; };
} }

View File

@@ -99,6 +99,13 @@ export default class ItemEntry extends TenantModel {
: getExlusiveTaxAmount(this.amount, this.taxRate); : 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. * Item entry relations.
*/ */

View File

@@ -55,6 +55,18 @@ export class CreateItem {
itemDTO.inventoryAccountId 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 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 // Validate inventory account should be modified in inventory item
// has inventory transactions. // has inventory transactions.
await this.validators.validateItemInvnetoryAccountModified( await this.validators.validateItemInvnetoryAccountModified(

View File

@@ -27,6 +27,8 @@ export class GetItem {
.withGraphFetched('category') .withGraphFetched('category')
.withGraphFetched('costAccount') .withGraphFetched('costAccount')
.withGraphFetched('itemWarehouses.warehouse') .withGraphFetched('itemWarehouses.warehouse')
.withGraphFetched('sellTaxRate')
.withGraphFetched('purchaseTaxRate')
.throwIfNotFound(); .throwIfNotFound();
return this.transformer.transform(tenantId, item, new ItemTransformer()); 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); 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', TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED', 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 = []; export const DEFAULT_VIEW_COLUMNS = [];

View File

@@ -189,6 +189,7 @@ export default class OrganizationService {
tenant.metadata?.baseCurrency tenant.metadata?.baseCurrency
); );
await tenant.saveMetadata(organizationDTO); await tenant.saveMetadata(organizationDTO);
// console.log('organizationDTO', organizationDTO);
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) { if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {
// Triggers `onOrganizationBaseCurrencyUpdated` event. // Triggers `onOrganizationBaseCurrencyUpdated` event.

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { AccountNormal, IBill, IItemEntry, ILedgerEntry } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
@Service() @Service()
export class BillGLEntries { export class BillGLEntries {
@@ -16,6 +17,9 @@ export class BillGLEntries {
@Inject() @Inject()
private ledgerStorage: LedgerStorageService; private ledgerStorage: LedgerStorageService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
/** /**
* Creates bill GL entries. * Creates bill GL entries.
* @param {number} tenantId - * @param {number} tenantId -
@@ -43,8 +47,16 @@ export class BillGLEntries {
{}, {},
trx 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. // Commit the GL enties on the storage.
await this.ledgerStorage.commit(tenantId, billLedger, trx); await this.ledgerStorage.commit(tenantId, billLedger, trx);
}; };
@@ -83,7 +95,7 @@ export class BillGLEntries {
/** /**
* Retrieves the bill common entry. * Retrieves the bill common entry.
* @param {IBill} bill * @param {IBill} bill
* @returns {ILedgerEntry} * @returns {ILedgerEntry}
*/ */
private getBillCommonEntry = (bill: IBill) => { private getBillCommonEntry = (bill: IBill) => {
@@ -119,7 +131,7 @@ export class BillGLEntries {
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => { (bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill); const commonJournalMeta = this.getBillCommonEntry(bill);
const localAmount = bill.exchangeRate * entry.amount; const localAmount = bill.exchangeRate * entry.amountExludingTax;
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
return { return {
@@ -173,7 +185,7 @@ export class BillGLEntries {
return { return {
...commonJournalMeta, ...commonJournalMeta,
credit: bill.localAmount, credit: bill.totalLocal,
accountId: payableAccountId, accountId: payableAccountId,
contactId: bill.vendorId, contactId: bill.vendorId,
accountNormal: AccountNormal.CREDIT, 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. * Retrieves the given bill GL entries.
* @param {IBill} bill * @param {IBill} bill
* @param {number} payableAccountId * @param {number} payableAccountId
* @returns {ILedgerEntry[]} * @returns {ILedgerEntry[]}
*/ */
private getBillGLEntries = ( private getBillGLEntries = (
bill: IBill, bill: IBill,
payableAccountId: number payableAccountId: number,
taxPayableAccountId: number
): ILedgerEntry[] => { ): ILedgerEntry[] => {
const payableEntry = this.getBillPayableEntry(payableAccountId, bill); const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
@@ -201,18 +260,28 @@ export class BillGLEntries {
const landedCostEntries = bill.locatedLandedCosts.map( const landedCostEntries = bill.locatedLandedCosts.map(
landedCostTransformer landedCostTransformer
); );
const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId);
// Allocate cost entries journal entries. // Allocate cost entries journal entries.
return [payableEntry, ...itemsEntries, ...landedCostEntries]; return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries];
}; };
/** /**
* Retrieves the given bill ledger. * Retrieves the given bill ledger.
* @param {IBill} bill * @param {IBill} bill
* @param {number} payableAccountId * @param {number} payableAccountId
* @returns {Ledger} * @returns {Ledger}
*/ */
private getBillLedger = (bill: IBill, payableAccountId: number) => { private getBillLedger = (
const entries = this.getBillGLEntries(bill, payableAccountId); bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
) => {
const entries = this.getBillGLEntries(
bill,
payableAccountId,
taxPayableAccountId
);
return new Ledger(entries); return new Ledger(entries);
}; };

View File

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

View File

@@ -1,27 +1,42 @@
import { IBill } from '@/interfaces'; import { IBill } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer'; import { Transformer } from '@/lib/Transformer/Transformer';
import { SaleInvoiceTaxEntryTransformer } from '@/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer';
import { formatNumber } from 'utils'; import { formatNumber } from 'utils';
export class PurchaseInvoiceTransformer extends Transformer { export class PurchaseInvoiceTransformer extends Transformer {
/** /**
* Include these attributes to sale invoice object. * Include these attributes to sale bill object.
* @returns {Array} * @returns {Array}
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return [ return [
'formattedBillDate', 'formattedBillDate',
'formattedDueDate', 'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount', 'formattedPaymentAmount',
'formattedBalance', 'formattedBalance',
'formattedDueAmount', 'formattedDueAmount',
'formattedExchangeRate', 'formattedExchangeRate',
'subtotalFormatted',
'subtotalLocalFormatted',
'subtotalExcludingTaxFormatted',
'taxAmountWithheldLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
]; ];
}; };
/** /**
* Retrieve formatted invoice date. * Excluded attributes.
* @param {IBill} invoice * @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['amount', 'amountLocal', 'localAmount'];
};
/**
* Retrieve formatted bill date.
* @param {IBill} bill
* @returns {String} * @returns {String}
*/ */
protected formattedBillDate = (bill: IBill): string => { protected formattedBillDate = (bill: IBill): string => {
@@ -29,8 +44,8 @@ export class PurchaseInvoiceTransformer extends Transformer {
}; };
/** /**
* Retrieve formatted invoice date. * Retrieve formatted bill date.
* @param {IBill} invoice * @param {IBill} bill
* @returns {String} * @returns {String}
*/ */
protected formattedDueDate = (bill: IBill): string => { protected formattedDueDate = (bill: IBill): string => {
@@ -39,7 +54,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/** /**
* Retrieve formatted bill amount. * Retrieve formatted bill amount.
* @param {IBill} invoice * @param {IBill} bill
* @returns {string} * @returns {string}
*/ */
protected formattedAmount = (bill): string => { protected formattedAmount = (bill): string => {
@@ -48,7 +63,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/** /**
* Retrieve formatted bill amount. * Retrieve formatted bill amount.
* @param {IBill} invoice * @param {IBill} bill
* @returns {string} * @returns {string}
*/ */
protected formattedPaymentAmount = (bill): string => { protected formattedPaymentAmount = (bill): string => {
@@ -59,7 +74,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/** /**
* Retrieve formatted bill amount. * Retrieve formatted bill amount.
* @param {IBill} invoice * @param {IBill} bill
* @returns {string} * @returns {string}
*/ */
protected formattedDueAmount = (bill): string => { protected formattedDueAmount = (bill): string => {
@@ -77,10 +92,90 @@ export class PurchaseInvoiceTransformer extends Transformer {
/** /**
* Retrieve the formatted exchange rate. * Retrieve the formatted exchange rate.
* @param {ISaleInvoice} invoice * @param {IBill} bill
* @returns {string} * @returns {string}
*/ */
protected formattedExchangeRate = (invoice): string => { protected formattedExchangeRate = (bill): string => {
return formatNumber(invoice.exchangeRate, { money: false }); 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, ...commonEntry,
credit: entry.taxAmount, credit: entry.taxAmount,
accountId: taxPayableAccountId, accountId: taxPayableAccountId,
index: index + 3, index: index + 1,
indexGroup: 30,
accountNormal: AccountNormal.CREDIT, accountNormal: AccountNormal.CREDIT,
taxRateId: entry.taxRateId, taxRateId: entry.taxRateId,
taxRate: entry.taxRate, taxRate: entry.taxRate,

View File

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

View File

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

View File

@@ -115,6 +115,7 @@ export class EditTaxRateService {
// Triggers `onTaxRateEdited` event. // Triggers `onTaxRateEdited` event.
await this.eventPublisher.emitAsync(events.taxRates.onEdited, { await this.eventPublisher.emitAsync(events.taxRates.onEdited, {
editTaxRateDTO, editTaxRateDTO,
oldTaxRate,
taxRate, taxRate,
tenantId, tenantId,
trx, 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,18 +1,7 @@
const path = require('path'); const path = require('path');
const webpack = require('webpack');
const dotenv = require('dotenv-webpack');
module.exports = { module.exports = {
webpack: { webpack: {
plugins: [
new dotenv(),
new webpack.DefinePlugin({
'process.env': {
MONOREPO_VERSION: JSON.stringify(require('../../lerna.json').version),
},
}),
],
alias: { alias: {
'@': path.resolve(__dirname, 'src'), '@': path.resolve(__dirname, 'src'),
}, },

View File

@@ -117,6 +117,12 @@
"resolutions": { "resolutions": {
"react-error-overlay": "6.0.9" "react-error-overlay": "6.0.9"
}, },
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",

View File

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

View File

@@ -26,6 +26,10 @@ const SelectButton = styled(Button)`
position: relative; position: relative;
padding-right: 30px; padding-right: 30px;
&.bp4-small{
padding-right: 24px;
}
&:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) { &:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) {
color: #5c7080; color: #5c7080;
} }

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

@@ -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

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

View File

@@ -34,10 +34,10 @@ export function Sidebar() {
* @returns {React.JSX} * @returns {React.JSX}
*/ */
function SidebarFooterVersion() { function SidebarFooterVersion() {
const { MONOREPO_VERSION } = process.env; const { REACT_APP_VERSION } = process.env;
if (!MONOREPO_VERSION) { if (!REACT_APP_VERSION) {
return null; 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 // @ts-nocheck
import React from 'react';
import { Button, Popover, Menu, Position } from '@blueprintjs/core'; import { Button, Popover, Menu, Position } from '@blueprintjs/core';
import { Icon } from '@/components'; import { Icon } from '@/components';

View File

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

View File

@@ -1,11 +1,8 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
FormatNumber,
T, T,
TotalLines, TotalLines,
TotalLine, TotalLine,
@@ -23,12 +20,20 @@ export function BillDetailTableFooter() {
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}> <BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine <TotalLine
title={<T id={'bill.details.subtotal'} />} title={<T id={'bill.details.subtotal'} />}
value={<FormatNumber value={bill.amount} />} value={bill.subtotal_formatted}
borderStyle={TotalLineBorderStyle.SingleDark} 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 <TotalLine
title={<T id={'bill.details.total'} />} title={<T id={'bill.details.total'} />}
value={bill.formatted_amount} value={bill.total_formatted}
borderStyle={TotalLineBorderStyle.DoubleDark} borderStyle={TotalLineBorderStyle.DoubleDark}
textStyle={TotalLineTextStyle.Bold} textStyle={TotalLineTextStyle.Bold}
/> />
@@ -39,6 +44,7 @@ export function BillDetailTableFooter() {
<TotalLine <TotalLine
title={<T id={'bill.details.due_amount'} />} title={<T id={'bill.details.due_amount'} />}
value={bill.formatted_due_amount} value={bill.formatted_due_amount}
textStyle={TotalLineTextStyle.Bold}
/> />
</BillTotalLines> </BillTotalLines>
</BillDetailsFooterRoot> </BillDetailsFooterRoot>

View File

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

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

View File

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

View File

@@ -1,8 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React, { useCallback } from 'react'; import React, { useCallback, useMemo } from 'react';
import * as R from 'ramda'; 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 { useItem } from '@/hooks/query';
import { import {
toSafeNumber, toSafeNumber,
@@ -12,6 +11,7 @@ import {
updateAutoAddNewLine, updateAutoAddNewLine,
orderingLinesIndexes, orderingLinesIndexes,
updateTableRow, updateTableRow,
formattedAmount,
} from '@/utils'; } from '@/utils';
import { useItemEntriesTableContext } from './ItemEntriesTableProvider'; import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
@@ -119,17 +119,24 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
// Detarmines whether the landed cost checkbox should be disabled. // Detarmines whether the landed cost checkbox should be disabled.
const landedCostDisabled = isLandedCostDisabled(item); const landedCostDisabled = isLandedCostDisabled(item);
const taxRateId =
itemType === ITEM_TYPE.PURCHASABLE
? item.purchase_tax_rate_id
: item.sell_tax_rate_id;
// The new row. // The new row.
const newRow = { const newRow = {
rate: price, rate: price,
description, description,
quantity: 1, quantity: 1,
tax_rate_id: taxRateId,
...(landedCost ...(landedCost
? { ? {
landed_cost: false, landed_cost: false,
landed_cost_disabled: landedCostDisabled, landed_cost_disabled: landedCostDisabled,
} }
: {}), : {}),
taxRateId,
}; };
setItemRow(null); setItemRow(null);
saveInvoke(notifyNewRow, newRow, rowIndex); saveInvoke(notifyNewRow, newRow, rowIndex);
@@ -158,13 +165,21 @@ export const composeRowsOnEditCell = R.curry(
/** /**
* Compose table rows when insert a new row to table rows. * Compose table rows when insert a new row to table rows.
*/ */
export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => { export const useComposeRowsOnNewRow = () => {
return compose( const { taxRates, isInclusiveTax } = useItemEntriesTableContext();
orderingLinesIndexes,
updateItemsEntriesTotal, return React.useMemo(() => {
updateTableRow(rowIndex, newRow), return R.curry((rowIndex, newRow, rows) => {
)(rows); return compose(
}); assignEntriesTaxAmount(isInclusiveTax),
assignEntriesTaxRate(taxRates),
orderingLinesIndexes,
updateItemsEntriesTotal,
updateTableRow(rowIndex, newRow),
)(rows);
});
}, [isInclusiveTax, taxRates]);
};
/** /**
* Associate tax rate to entries. * Associate tax rate to entries.
@@ -266,3 +281,29 @@ export const useComposeRowsOnRemoveTableRow = () => {
[minLinesNumber, defaultEntry, localValue], [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

@@ -29,14 +29,16 @@ import {
costPriceFieldShouldUpdate, costPriceFieldShouldUpdate,
costAccountFieldShouldUpdate, costAccountFieldShouldUpdate,
purchaseDescFieldShouldUpdate, purchaseDescFieldShouldUpdate,
taxRateFieldShouldUpdate,
} from './utils'; } from './utils';
import { compose, inputIntent } from '@/utils'; import { compose, inputIntent } from '@/utils';
import { TaxRatesSelect } from '@/components/TaxRates/TaxRatesSelect';
/** /**
* Item form body. * Item form body.
*/ */
function ItemFormBody({ organization: { base_currency } }) { function ItemFormBody({ organization: { base_currency } }) {
const { accounts } = useItemFormContext(); const { accounts, taxRates } = useItemFormContext();
const { values } = useFormikContext(); const { values } = useFormikContext();
return ( return (
@@ -115,6 +117,19 @@ function ItemFormBody({ organization: { base_currency } }) {
/> />
</FFormGroup> </FFormGroup>
{/*------------- Sell Tax Rate ------------- */}
<FFormGroup
name={'sell_tax_rate_id'}
label={'Tax Rate'}
inline={true}
>
<TaxRatesSelect
name={'sell_tax_rate_id'}
items={taxRates}
allowCreate
/>
</FFormGroup>
<FastField <FastField
name={'sell_description'} name={'sell_description'}
sellable={values.sellable} sellable={values.sellable}
@@ -213,6 +228,24 @@ function ItemFormBody({ organization: { base_currency } }) {
/> />
</FFormGroup> </FFormGroup>
{/*------------- Purchase Tax Rate ------------- */}
<FFormGroup
name={'purchase_tax_rate_id'}
label={'Tax Rate'}
inline={true}
fastField={true}
shouldUpdateDeps={{ taxRates }}
shouldUpdate={taxRateFieldShouldUpdate}
>
<TaxRatesSelect
name={'purchase_tax_rate_id'}
items={taxRates}
allowCreate={true}
fastField={true}
shouldUpdateDeps={{ taxRates }}
/>
</FFormGroup>
<FastField <FastField
name={'purchase_description'} name={'purchase_description'}
purchasable={values.purchasable} purchasable={values.purchasable}

View File

@@ -10,6 +10,7 @@ import {
useAccounts, useAccounts,
} from '@/hooks/query'; } from '@/hooks/query';
import { useWatchItemError } from './utils'; import { useWatchItemError } from './utils';
import { useTaxRates } from '@/hooks/query/taxRates';
const ItemFormContext = createContext(); const ItemFormContext = createContext();
@@ -30,6 +31,8 @@ function ItemFormProvider({ itemId, ...props }) {
data: { itemsCategories }, data: { itemsCategories },
} = useItemsCategories(); } = useItemsCategories();
const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates();
// Fetches the given item details. // Fetches the given item details.
const itemQuery = useItem(itemId || duplicateId, { const itemQuery = useItem(itemId || duplicateId, {
enabled: !!itemId || !!duplicateId, enabled: !!itemId || !!duplicateId,
@@ -69,6 +72,7 @@ function ItemFormProvider({ itemId, ...props }) {
accounts, accounts,
item, item,
itemsCategories, itemsCategories,
taxRates,
submitPayload, submitPayload,
isNewMode, isNewMode,
@@ -76,6 +80,7 @@ function ItemFormProvider({ itemId, ...props }) {
isAccountsLoading, isAccountsLoading,
isItemsCategoriesLoading, isItemsCategoriesLoading,
isItemLoading, isItemLoading,
isTaxRatesLoading,
createItemMutate, createItemMutate,
editItemMutate, editItemMutate,

View File

@@ -23,12 +23,14 @@ const defaultInitialValues = {
sell_price: '', sell_price: '',
cost_account_id: '', cost_account_id: '',
sell_account_id: '', sell_account_id: '',
sell_tax_rate_id: '',
inventory_account_id: '', inventory_account_id: '',
category_id: '', category_id: '',
sellable: 1, sellable: 1,
purchasable: true, purchasable: true,
sell_description: '', sell_description: '',
purchase_description: '', purchase_description: '',
purchase_tax_rate_id: '',
}; };
/** /**
@@ -187,6 +189,13 @@ export const purchaseDescFieldShouldUpdate = (newProps, oldProps) => {
); );
}; };
export const taxRateFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.shouldUpdateDeps.taxRates !== oldProps.shouldUpdateDeps.taxRates ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
export function transformItemsTableState(tableState) { export function transformItemsTableState(tableState) {
return { return {
...transformTableStateToQuery(tableState), ...transformTableStateToQuery(tableState),

View File

@@ -6,6 +6,9 @@ const Schema = Yup.object().shape({
name: Yup.string() name: Yup.string()
.required() .required()
.label(intl.get('organization_name_')), .label(intl.get('organization_name_')),
tax_number: Yup.string()
.nullable()
.label(intl.get('organization_tax_number_')),
industry: Yup.string() industry: Yup.string()
.nullable() .nullable()
.label(intl.get('organization_industry_')), .label(intl.get('organization_industry_')),

View File

@@ -59,6 +59,17 @@ export default function PreferencesGeneralForm({ isSubmitting }) {
<FInputGroup medium={'true'} name={'name'} fastField={true} /> <FInputGroup medium={'true'} name={'name'} fastField={true} />
</FFormGroup> </FFormGroup>
{/* ---------- Organization Tax Number ---------- */}
<FFormGroup
name={'tax_number'}
label={<T id={'organization_tax_number'} />}
inline={true}
helperText={<T id={'shown_on_sales_forms_and_purchase_orders'} />}
fastField={true}
>
<FInputGroup medium={'true'} name={'tax_number'} fastField={true} />
</FFormGroup>
{/* ---------- Industry ---------- */} {/* ---------- Industry ---------- */}
<FFormGroup <FFormGroup
name={'industry'} name={'industry'}

View File

@@ -23,6 +23,7 @@ const defaultValues = {
fiscal_year: '', fiscal_year: '',
date_format: '', date_format: '',
timezone: '', timezone: '',
tax_number: '',
}; };
/** /**

View File

@@ -26,6 +26,7 @@ import {
handleErrors, handleErrors,
} from './utils'; } from './utils';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import { BillFormEntriesActions } from './BillFormEntriesActions';
/** /**
* Bill form. * Bill form.
@@ -126,7 +127,10 @@ function BillForm({
<Form> <Form>
<BillFormTopBar /> <BillFormTopBar />
<BillFormHeader /> <BillFormHeader />
<BillItemsEntriesEditor /> <div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<BillFormEntriesActions />
<BillItemsEntriesEditor />
</div>
<BillFormFooter /> <BillFormFooter />
<BillFloatingActions /> <BillFloatingActions />
</Form> </Form>

View File

@@ -0,0 +1,58 @@
// @ts-nocheck
import styled from 'styled-components';
import { useFormikContext } from 'formik';
import { FFormGroup, FSelect } from '@/components';
import { InclusiveTaxOptions } from '@/constants/InclusiveTaxOptions';
import { composeEntriesOnEditInclusiveTax } from './utils';
import { EntriesActionsBar } from '@/containers/Entries/EntriesActionBar';
export function BillFormEntriesActions() {
return (
<EntriesActionsBar>
<BillExclusiveInclusiveSelect />
</EntriesActionsBar>
);
}
/**
* Bill exclusive/inclusive select.
* @returns {React.ReactNode}
*/
export function BillExclusiveInclusiveSelect(props) {
const { values, setFieldValue } = useFormikContext();
const handleItemSelect = (item) => {
const newEntries = composeEntriesOnEditInclusiveTax(
item.key,
values.entries,
);
setFieldValue('inclusive_exclusive_tax', item.key);
setFieldValue('entries', newEntries);
};
return (
<InclusiveFormGroup
name={'inclusive_exclusive_tax'}
label={'Amounts are'}
inline={true}
>
<FSelect
name={'inclusive_exclusive_tax'}
items={InclusiveTaxOptions}
textAccessor={'label'}
labelAccessor={() => ''}
valueAccessor={'key'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
buttonProps={{ small: true }}
onItemSelect={handleItemSelect}
filterable={false}
{...props}
/>
</InclusiveFormGroup>
);
}
const InclusiveFormGroup = styled(FFormGroup)`
margin-left: auto;
`;

View File

@@ -1,15 +1,14 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
T,
TotalLines, TotalLines,
TotalLine, TotalLine,
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
} from '@/components'; } from '@/components';
import { useBillTotals } from './utils'; import { useBillAggregatedTaxRates, useBillTotals } from './utils';
import { useFormikContext } from 'formik';
import { TaxType } from '@/interfaces/TaxRates';
export function BillFormFooterRight() { export function BillFormFooterRight() {
const { const {
@@ -19,26 +18,46 @@ export function BillFormFooterRight() {
formattedPaymentTotal, formattedPaymentTotal,
} = useBillTotals(); } = useBillTotals();
const {
values: { inclusive_exclusive_tax, currency_code },
} = useFormikContext();
const taxEntries = useBillAggregatedTaxRates();
return ( return (
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}> <BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine <TotalLine
title={<T id={'bill_form.label.subtotal'} />} title={
<>
{inclusive_exclusive_tax === TaxType.Inclusive
? 'Subtotal (Tax Inclusive)'
: 'Subtotal'}
</>
}
value={formattedSubtotal} value={formattedSubtotal}
borderStyle={TotalLineBorderStyle.None} borderStyle={TotalLineBorderStyle.None}
/> />
{taxEntries.map((tax, index) => (
<TotalLine
key={index}
title={tax.label}
value={tax.taxAmountFormatted}
borderStyle={TotalLineBorderStyle.None}
/>
))}
<TotalLine <TotalLine
title={<T id={'bill_form.label.total'} />} title={`Total (${currency_code})`}
value={formattedTotal} value={formattedTotal}
borderStyle={TotalLineBorderStyle.SingleDark} borderStyle={TotalLineBorderStyle.SingleDark}
textStyle={TotalLineTextStyle.Bold} textStyle={TotalLineTextStyle.Bold}
/> />
<TotalLine <TotalLine
title={<T id={'bill_form.label.total'} />} title={'Paid Amount'}
value={formattedPaymentTotal} value={formattedPaymentTotal}
borderStyle={TotalLineBorderStyle.None} borderStyle={TotalLineBorderStyle.None}
/> />
<TotalLine <TotalLine
title={<T id={'bill_form.label.total'} />} title={'Due Amount'}
value={formattedDueTotal} value={formattedDueTotal}
textStyle={TotalLineTextStyle.Bold} textStyle={TotalLineTextStyle.Bold}
/> />

View File

@@ -15,6 +15,7 @@ import {
useCreateBill, useCreateBill,
useEditBill, useEditBill,
} from '@/hooks/query'; } from '@/hooks/query';
import { useTaxRates } from '@/hooks/query/taxRates';
const BillFormContext = createContext(); const BillFormContext = createContext();
@@ -83,6 +84,9 @@ function BillFormProvider({ billId, ...props }) {
isSuccess: isBranchesSuccess, isSuccess: isBranchesSuccess,
} = useBranches({}, { enabled: isBranchFeatureCan }); } = useBranches({}, { enabled: isBranchFeatureCan });
// Fetch tax rates.
const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates();
// Fetches the projects list. // Fetches the projects list.
const { const {
data: { projects }, data: { projects },
@@ -103,7 +107,10 @@ function BillFormProvider({ billId, ...props }) {
// Determines whether the warehouse and branches are loading. // Determines whether the warehouse and branches are loading.
const isFeatureLoading = const isFeatureLoading =
isWarehouesLoading || isBranchesLoading || isProjectsLoading; isWarehouesLoading ||
isBranchesLoading ||
isProjectsLoading ||
isTaxRatesLoading;
const provider = { const provider = {
accounts, accounts,
@@ -113,6 +120,7 @@ function BillFormProvider({ billId, ...props }) {
warehouses, warehouses,
branches, branches,
projects, projects,
taxRates,
submitPayload, submitPayload,
isNewMode, isNewMode,
@@ -124,6 +132,7 @@ function BillFormProvider({ billId, ...props }) {
isFeatureLoading, isFeatureLoading,
isBranchesSuccess, isBranchesSuccess,
isWarehousesSuccess, isWarehousesSuccess,
isTaxRatesLoading,
createBillMutate, createBillMutate,
editBillMutate, editBillMutate,

View File

@@ -1,9 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable'; import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
import { FastField } from 'formik'; import { FastField } from 'formik';
import { CLASSES } from '@/constants/classes';
import { useBillFormContext } from './BillFormProvider'; import { useBillFormContext } from './BillFormProvider';
import { entriesFieldShouldUpdate } from './utils'; import { entriesFieldShouldUpdate } from './utils';
import { ITEM_TYPE } from '@/containers/Entries/utils'; import { ITEM_TYPE } from '@/containers/Entries/utils';
@@ -12,34 +9,33 @@ import { ITEM_TYPE } from '@/containers/Entries/utils';
* Bill form body. * Bill form body.
*/ */
export default function BillFormBody({ defaultBill }) { export default function BillFormBody({ defaultBill }) {
const { items } = useBillFormContext(); const { items, taxRates } = useBillFormContext();
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}> <FastField
<FastField name={'entries'}
name={'entries'} items={items}
items={items} shouldUpdate={entriesFieldShouldUpdate}
shouldUpdate={entriesFieldShouldUpdate} >
> {({
{({ form: { values, setFieldValue },
form: { values, setFieldValue }, field: { value },
field: { value }, meta: { error, touched },
meta: { error, touched }, }) => (
}) => ( <ItemsEntriesTable
<ItemsEntriesTable value={value}
entries={value} onChange={(entries) => {
onUpdateData={(entries) => { setFieldValue('entries', entries);
setFieldValue('entries', entries); }}
}} items={items}
items={items} errors={error}
errors={error} linesNumber={4}
linesNumber={4} currencyCode={values.currency_code}
currencyCode={values.currency_code} itemType={ITEM_TYPE.PURCHASABLE}
itemType={ITEM_TYPE.PURCHASABLE} taxRates={taxRates}
landedCost={true} landedCost={true}
/> />
)} )}
</FastField> </FastField>
</div>
); );
} }

View File

@@ -3,7 +3,7 @@ import React from 'react';
import moment from 'moment'; import moment from 'moment';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import * as R from 'ramda'; import * as R from 'ramda';
import { first } from 'lodash'; import { first, chain } from 'lodash';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { AppToaster } from '@/components'; import { AppToaster } from '@/components';
@@ -17,6 +17,8 @@ import {
import { import {
updateItemsEntriesTotal, updateItemsEntriesTotal,
ensureEntriesHaveEmptyLine, ensureEntriesHaveEmptyLine,
assignEntriesTaxAmount,
aggregateItemEntriesTaxRates,
} from '@/containers/Entries/utils'; } from '@/containers/Entries/utils';
import { useCurrentOrganization } from '@/hooks/state'; import { useCurrentOrganization } from '@/hooks/state';
import { import {
@@ -24,6 +26,7 @@ import {
getEntriesTotal, getEntriesTotal,
} from '@/containers/Entries/utils'; } from '@/containers/Entries/utils';
import { useBillFormContext } from './BillFormProvider'; import { useBillFormContext } from './BillFormProvider';
import { TaxType } from '@/interfaces/TaxRates';
export const MIN_LINES_NUMBER = 1; export const MIN_LINES_NUMBER = 1;
@@ -37,6 +40,9 @@ export const defaultBillEntry = {
description: '', description: '',
amount: '', amount: '',
landed_cost: false, landed_cost: false,
tax_rate_id: '',
tax_rate: '',
tax_amount: '',
}; };
// Default bill. // Default bill.
@@ -46,6 +52,7 @@ export const defaultBill = {
bill_date: moment(new Date()).format('YYYY-MM-DD'), bill_date: moment(new Date()).format('YYYY-MM-DD'),
due_date: moment(new Date()).format('YYYY-MM-DD'), due_date: moment(new Date()).format('YYYY-MM-DD'),
reference_no: '', reference_no: '',
inclusive_exclusive_tax: TaxType.Inclusive,
note: '', note: '',
open: '', open: '',
branch_id: '', branch_id: '',
@@ -82,6 +89,9 @@ export const transformToEditForm = (bill) => {
return { return {
...transformToForm(bill, defaultBill), ...transformToForm(bill, defaultBill),
inclusive_exclusive_tax: bill.is_inclusive_tax
? TaxType.Inclusive
: TaxType.Exclusive,
entries, entries,
}; };
}; };
@@ -228,11 +238,12 @@ export const useSetPrimaryWarehouseToForm = () => {
*/ */
export const useBillTotals = () => { export const useBillTotals = () => {
const { const {
values: { entries, currency_code: currencyCode }, values: { currency_code: currencyCode },
} = useFormikContext(); } = useFormikContext();
// Retrieves the bili entries total. // Retrieves the bill subtotal.
const total = React.useMemo(() => getEntriesTotal(entries), [entries]); const subtotal = useBillSubtotal();
const total = useBillTotal();
// Retrieves the formatted total money. // Retrieves the formatted total money.
const formattedTotal = React.useMemo( const formattedTotal = React.useMemo(
@@ -241,8 +252,8 @@ export const useBillTotals = () => {
); );
// Retrieves the formatted subtotal. // Retrieves the formatted subtotal.
const formattedSubtotal = React.useMemo( const formattedSubtotal = React.useMemo(
() => formattedAmount(total, currencyCode, { money: false }), () => formattedAmount(subtotal, currencyCode, { money: false }),
[total, currencyCode], [subtotal, currencyCode],
); );
// Retrieves the payment total. // Retrieves the payment total.
const paymentTotal = React.useMemo(() => 0, []); const paymentTotal = React.useMemo(() => 0, []);
@@ -288,3 +299,86 @@ export const useBillIsForeignCustomer = () => {
); );
return isForeignCustomer; return isForeignCustomer;
}; };
/**
* Re-calculates the entries tax amount when editing.
* @returns {string}
*/
export const composeEntriesOnEditInclusiveTax = (
inclusiveExclusiveTax: string,
entries,
) => {
return R.compose(
assignEntriesTaxAmount(inclusiveExclusiveTax === 'inclusive'),
)(entries);
};
/**
* Retreives the bill aggregated tax rates.
* @returns {Array}
*/
export const useBillAggregatedTaxRates = () => {
const { values } = useFormikContext();
const { taxRates } = useBillFormContext();
const aggregateTaxRates = React.useMemo(
() => aggregateItemEntriesTaxRates(taxRates),
[taxRates],
);
// Calculate the total tax amount of bill entries.
return React.useMemo(() => {
return aggregateTaxRates(values.entries);
}, [aggregateTaxRates, values.entries]);
};
/**
* Retrieves the bill subtotal.
* @returns {number}
*/
export const useBillSubtotal = () => {
const {
values: { entries },
} = useFormikContext();
// Calculate the total due amount of bill entries.
return React.useMemo(() => getEntriesTotal(entries), [entries]);
};
/**
* Retreives the bill total tax amount.
* @returns {number}
*/
export const useBillTotalTaxAmount = () => {
const { values } = useFormikContext();
return React.useMemo(() => {
return chain(values.entries)
.filter((entry) => entry.tax_amount)
.sumBy('tax_amount')
.value();
}, [values.entries]);
};
/**
* Detarmines whether the tax is exclusive.
* @returns {boolean}
*/
export const useIsBillTaxExclusive = () => {
const { values } = useFormikContext();
return values.inclusive_exclusive_tax === TaxType.Exclusive;
};
/**
* Retreives the bill total.
* @returns {number}
*/
export const useBillTotal = () => {
const subtotal = useBillSubtotal();
const totalTaxAmount = useBillTotalTaxAmount();
const isExclusiveTax = useIsBillTaxExclusive();
return R.compose(R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)))(
subtotal,
);
};

View File

@@ -9,20 +9,18 @@ import {
Tag, Tag,
ProgressBar, ProgressBar,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import clsx from 'classnames';
import { import {
FormatDateCell, FormatDateCell,
FormattedMessage as T, FormattedMessage as T,
Icon, Icon,
If, If,
Choose, Choose,
Money,
Can, Can,
} from '@/components'; } from '@/components';
import { import {
formattedAmount, formattedAmount,
safeCallback, safeCallback,
isBlank,
calculateStatus, calculateStatus,
} from '@/utils'; } from '@/utils';
import { import {
@@ -30,6 +28,7 @@ import {
PaymentMadeAction, PaymentMadeAction,
AbilitySubject, AbilitySubject,
} from '@/constants/abilityOption'; } from '@/constants/abilityOption';
import { CLASSES } from '@/constants';
/** /**
* Actions menu. * Actions menu.
@@ -101,17 +100,6 @@ export function ActionsMenu({
); );
} }
/**
* Amount accessor.
*/
export function AmountAccessor(bill) {
return !isBlank(bill.amount) ? (
<Money amount={bill.amount} currency={bill.currency_code} />
) : (
''
);
}
/** /**
* Status accessor. * Status accessor.
*/ */
@@ -198,11 +186,11 @@ export function useBillsTableColumns() {
{ {
id: 'amount', id: 'amount',
Header: intl.get('amount'), Header: intl.get('amount'),
accessor: AmountAccessor, accessor: 'total_formatted',
width: 120, width: 120,
className: 'amount',
align: 'right', align: 'right',
clickable: true, clickable: true,
className: clsx(CLASSES.FONT_BOLD),
}, },
{ {
id: 'status', id: 'status',

View File

@@ -22,14 +22,15 @@ export default function VendorCreditNoteItemsEntriesEditor() {
meta: { error, touched }, meta: { error, touched },
}) => ( }) => (
<ItemsEntriesTable <ItemsEntriesTable
entries={value} value={value}
onUpdateData={(entries) => { onChange={(entries) => {
setFieldValue('entries', entries); setFieldValue('entries', entries);
}} }}
items={items} items={items}
errors={error} errors={error}
linesNumber={4} linesNumber={4}
currencyCode={values.currency_code} currencyCode={values.currency_code}
enableTaxRates={false}
/> />
)} )}
</FastField> </FastField>

View File

@@ -26,14 +26,15 @@ export default function CreditNoteItemsEntriesEditorField() {
meta: { error, touched }, meta: { error, touched },
}) => ( }) => (
<ItemsEntriesTable <ItemsEntriesTable
entries={value} value={value}
onUpdateData={(entries) => { onChange={(entries) => {
setFieldValue('entries', entries); setFieldValue('entries', entries);
}} }}
items={items} items={items}
errors={error} errors={error}
linesNumber={4} linesNumber={4}
currencyCode={values.currency_code} currencyCode={values.currency_code}
enableTaxRates={false}
/> />
)} )}
</FastField> </FastField>

View File

@@ -26,14 +26,15 @@ export default function EstimateFormItemsEntriesField() {
meta: { error, touched }, meta: { error, touched },
}) => ( }) => (
<ItemsEntriesTable <ItemsEntriesTable
entries={value} value={value}
onUpdateData={(entries) => { onChange={(entries) => {
setFieldValue('entries', entries); setFieldValue('entries', entries);
}} }}
items={items} items={items}
errors={error} errors={error}
linesNumber={4} linesNumber={4}
currencyCode={values.currency_code} currencyCode={values.currency_code}
enableTaxRates={false}
/> />
)} )}
</FastField> </FastField>

View File

@@ -31,6 +31,15 @@ export const defaultEstimateEntry = {
amount: '', amount: '',
}; };
const defaultEstimateEntryReq = {
index: 0,
item_id: '',
rate: '',
discount: '',
quantity: '',
description: '',
};
export const defaultEstimate = { export const defaultEstimate = {
customer_id: '', customer_id: '',
estimate_date: moment(new Date()).format('YYYY-MM-DD'), estimate_date: moment(new Date()).format('YYYY-MM-DD'),
@@ -148,7 +157,9 @@ export const transfromsFormValuesToRequest = (values) => {
...(values.estimate_number_manually && { ...(values.estimate_number_manually && {
estimate_number: values.estimate_number, estimate_number: values.estimate_number,
}), }),
entries: entries.map((entry) => ({ ...omit(entry, ['amount']) })), entries: entries.map((entry) => ({
...transformToForm(entry, defaultEstimateEntryReq),
})),
}; };
}; };

View File

@@ -3,7 +3,8 @@ import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { InclusiveButtonOptions } from './constants'; import { InclusiveButtonOptions } from './constants';
import { Box, FFormGroup, FSelect } from '@/components'; import { FFormGroup, FSelect } from '@/components';
import { EntriesActionsBar } from '@/containers/Entries/EntriesActionBar';
import { composeEntriesOnEditInclusiveTax } from './utils'; import { composeEntriesOnEditInclusiveTax } from './utils';
/** /**
@@ -12,9 +13,9 @@ import { composeEntriesOnEditInclusiveTax } from './utils';
*/ */
export function InvoiceFormActions() { export function InvoiceFormActions() {
return ( return (
<InvoiceFormActionsRoot> <EntriesActionsBar>
<InvoiceExclusiveInclusiveSelect /> <InvoiceExclusiveInclusiveSelect />
</InvoiceFormActionsRoot> </EntriesActionsBar>
); );
} }
@@ -40,7 +41,7 @@ export function InvoiceExclusiveInclusiveSelect(props) {
label={'Amounts are'} label={'Amounts are'}
inline={true} inline={true}
> >
<InclusiveSelect <FSelect
name={'inclusive_exclusive_tax'} name={'inclusive_exclusive_tax'}
items={InclusiveButtonOptions} items={InclusiveButtonOptions}
textAccessor={'label'} textAccessor={'label'}
@@ -57,23 +58,5 @@ export function InvoiceExclusiveInclusiveSelect(props) {
} }
const InclusiveFormGroup = styled(FFormGroup)` const InclusiveFormGroup = styled(FFormGroup)`
margin-bottom: 0;
margin-left: auto; margin-left: auto;
&.bp4-form-group.bp4-inline label.bp4-label {
line-height: 1.25;
opacity: 0.6;
margin-right: 8px;
}
`;
const InclusiveSelect = styled(FSelect)`
.bp4-button {
padding-right: 24px;
}
`;
const InvoiceFormActionsRoot = styled(Box)`
padding-bottom: 12px;
display: flex;
`; `;

View File

@@ -11,6 +11,7 @@ import {
TotalLineTextStyle, TotalLineTextStyle,
} from '@/components'; } from '@/components';
import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils'; import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils';
import { TaxType } from '@/interfaces/TaxRates';
export function InvoiceFormFooterRight() { export function InvoiceFormFooterRight() {
// Calculate the total due amount of invoice entries. // Calculate the total due amount of invoice entries.
@@ -32,7 +33,7 @@ export function InvoiceFormFooterRight() {
<TotalLine <TotalLine
title={ title={
<> <>
{inclusive_exclusive_tax === 'inclusive' {inclusive_exclusive_tax === TaxType.Inclusive
? 'Subtotal (Tax Inclusive)' ? 'Subtotal (Tax Inclusive)'
: 'Subtotal'} : 'Subtotal'}
</> </>

View File

@@ -5,6 +5,7 @@ import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
import { useInvoiceFormContext } from './InvoiceFormProvider'; import { useInvoiceFormContext } from './InvoiceFormProvider';
import { entriesFieldShouldUpdate } from './utils'; import { entriesFieldShouldUpdate } from './utils';
import { TaxType } from '@/interfaces/TaxRates'; import { TaxType } from '@/interfaces/TaxRates';
import { ITEM_TYPE } from '@/containers/Entries/utils';
/** /**
* Invoice items entries editor field. * Invoice items entries editor field.
@@ -31,6 +32,7 @@ export default function InvoiceItemsEntriesEditorField() {
}} }}
items={items} items={items}
taxRates={taxRates} taxRates={taxRates}
itemType={ITEM_TYPE.SELLABLE}
errors={error} errors={error}
linesNumber={4} linesNumber={4}
currencyCode={values.currency_code} currencyCode={values.currency_code}

View File

@@ -1,18 +1,23 @@
// @ts-nocheck // @ts-nocheck
import React, { useCallback, useMemo } from 'react'; import React from 'react';
import { useFormikContext } from 'formik';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import moment from 'moment'; import moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { omit, first, keyBy, sumBy, groupBy } from 'lodash'; import { omit, first, sumBy } from 'lodash';
import { compose, transformToForm, repeatValue } from '@/utils'; import {
import { useFormikContext } from 'formik'; compose,
transformToForm,
import { formattedAmount, defaultFastFieldShouldUpdate } from '@/utils'; repeatValue,
formattedAmount,
defaultFastFieldShouldUpdate,
} from '@/utils';
import { ERROR } from '@/constants/errors'; import { ERROR } from '@/constants/errors';
import { AppToaster } from '@/components'; import { AppToaster } from '@/components';
import { useCurrentOrganization } from '@/hooks/state'; import { useCurrentOrganization } from '@/hooks/state';
import { import {
aggregateItemEntriesTaxRates,
assignEntriesTaxAmount, assignEntriesTaxAmount,
getEntriesTotal, getEntriesTotal,
} from '@/containers/Entries/utils'; } from '@/containers/Entries/utils';
@@ -327,28 +332,14 @@ export const useInvoiceAggregatedTaxRates = () => {
const { values } = useFormikContext(); const { values } = useFormikContext();
const { taxRates } = useInvoiceFormContext(); const { taxRates } = useInvoiceFormContext();
const taxRatesById = useMemo(() => keyBy(taxRates, 'id'), [taxRates]); const aggregateTaxRates = React.useMemo(
() => aggregateItemEntriesTaxRates(taxRates),
[taxRates],
);
// Calculate the total tax amount of invoice entries. // Calculate the total tax amount of invoice entries.
return React.useMemo(() => { return React.useMemo(() => {
const filteredEntries = values.entries.filter((e) => e.tax_rate_id); return aggregateTaxRates(values.entries);
const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id'); }, [aggregateTaxRates, values.entries]);
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,
};
});
}, [values.entries]);
}; };
/** /**

View File

@@ -19,14 +19,15 @@ export default function ReceiptItemsEntriesEditor({ defaultReceipt }) {
meta: { error, touched }, meta: { error, touched },
}) => ( }) => (
<ItemsEntriesTable <ItemsEntriesTable
entries={value} value={value}
onUpdateData={(entries) => { onChange={(entries) => {
setFieldValue('entries', entries); setFieldValue('entries', entries);
}} }}
items={items} items={items}
errors={error} errors={error}
linesNumber={4} linesNumber={4}
currencyCode={values.currency_code} currencyCode={values.currency_code}
enableTaxRates={false}
/> />
)} )}
</FastField> </FastField>

View File

@@ -32,6 +32,15 @@ export const defaultReceiptEntry = {
amount: '', amount: '',
}; };
const defaultReceiptEntryReq = {
index: 0,
item_id: '',
rate: '',
discount: '',
quantity: '',
description: '',
};
export const defaultReceipt = { export const defaultReceipt = {
customer_id: '', customer_id: '',
deposit_account_id: '', deposit_account_id: '',
@@ -140,7 +149,9 @@ export const transformFormValuesToRequest = (values) => {
...(values.receipt_number_manually && { ...(values.receipt_number_manually && {
receipt_number: values.receipt_number, receipt_number: values.receipt_number,
}), }),
entries: entries.map((entry) => ({ ...omit(entry, ['amount']) })), entries: entries.map((entry) => ({
...transformToForm(entry, defaultReceiptEntryReq),
})),
closed: false, closed: false,
}; };
}; };

View File

@@ -43,6 +43,15 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
<FInputGroup name={'name'} fastField={true} /> <FInputGroup name={'name'} fastField={true} />
</FFormGroup> </FFormGroup>
{/* ---------- Organization Tax Number ---------- */}
<FFormGroup
name={'tax_number'}
label={<T id={'organization_tax_number'} />}
fastField={true}
>
<FInputGroup name={'tax_number'} fastField={true} />
</FFormGroup>
{/* ---------- Location ---------- */} {/* ---------- Location ---------- */}
<FFormGroup <FFormGroup
name={'location'} name={'location'}

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { DialogContent } from '@/components'; import { DialogContent } from '@/components';
import { useTaxRate, useTaxRates } from '@/hooks/query/taxRates'; import { useTaxRate } from '@/hooks/query/taxRates';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
const TaxRateFormDialogContext = React.createContext(); const TaxRateFormDialogContext = React.createContext();

View File

@@ -59,6 +59,8 @@ export function useEditTaxRate(props) {
onSuccess: (res, id) => { onSuccess: (res, id) => {
commonInvalidateQueries(queryClient); commonInvalidateQueries(queryClient);
queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]); queryClient.invalidateQueries([QUERY_TYPES.TAX_RATES, id]);
queryClient.invalidateQueries(QUERY_TYPES.ITEM);
queryClient.invalidateQueries(QUERY_TYPES.ITEMS);
}, },
...props, ...props,
}, },

View File

@@ -37,6 +37,7 @@
"success": "Success", "success": "Success",
"register_a_new_organization": "Register a New Organization.", "register_a_new_organization": "Register a New Organization.",
"organization_name": "Organization Name", "organization_name": "Organization Name",
"organization_tax_number": "Organization Tax Number",
"email": "Email", "email": "Email",
"email_address": "Email Address", "email_address": "Email Address",
"register": "Register", "register": "Register",
@@ -334,9 +335,12 @@
"currency_name_": "Currency name", "currency_name_": "Currency name",
"cost_account_id": "Cost account", "cost_account_id": "Cost account",
"sell_account_id": "Sell account", "sell_account_id": "Sell account",
"item.details.sell_tax_rate": "Sell Tax Rate",
"item.details.purchase_tax_rate": "Purchase Tax Rate",
"item_type_": "Item type", "item_type_": "Item type",
"item_name_": "Item name", "item_name_": "Item name",
"organization_industry_": "Organization industry", "organization_industry_": "Organization industry",
"organization_tax_number_": "Organization tax number",
"base_currency_": "Base currency", "base_currency_": "Base currency",
"date_format_": "Date format", "date_format_": "Date format",
"category_name_": "Category name", "category_name_": "Category name",

View File

@@ -44,7 +44,8 @@ $sidebar-submenu-item-bg-color: rgba(255, 255, 255, 0.2);
$form-check-input-checked-color: #fff; $form-check-input-checked-color: #fff;
$form-check-input-checked-bg-color: $blue1; $form-check-input-checked-bg-color: $blue1;
$form-check-input-checked-bg-image: "" !default; $form-check-input-checked-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 16 16' enable-background='new 0 0 16 16' xml:space='preserve'><g id='small_tick_1_'><g><path fill='#{$form-check-input-checked-color}' fill-rule='evenodd' clip-rule='evenodd' d='M12,5c-0.28,0-0.53,0.11-0.71,0.29L7,9.59L4.71,7.29C4.53,7.11,4.28,7,4,7C3.45,7,3,7.45,3,8c0,0.28,0.11,0.53,0.29,0.71l3,3C6.47,11.89,6.72,12,7,12s0.53-0.11,0.71-0.29l5-5C12.89,6.53,13,6.28,13,6C13,5.45,12.55,5,12,5z'/></g></g></svg>") !default;
$form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 16 16' enable-background='new 0 0 16 16' xml:space='preserve'><g id='small_tick_1_'><g><path fill='#{$form-check-input-checked-color}' fill-rule='evenodd' clip-rule='evenodd' d='M11,7H5C4.45,7,4,7.45,4,8c0,0.55,0.45,1,1,1h6c0.55,0,1-0.45,1-1C12,7.45,11.55,7,11,7z'/></g></g></svg>") !default;
// z-indexs // z-indexs
$zindex-dashboard-splash-screen: 39; $zindex-dashboard-splash-screen: 39;

View File

@@ -1,4 +1,4 @@
@import 'src/style/Base.scss'; @import 'src/style/_base.scss';
.bigcapital-datatable { .bigcapital-datatable {
display: block; display: block;

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
// Dialog // Dialog
.#{$ns}-dialog{ .#{$ns}-dialog{

View File

@@ -1,4 +1,4 @@
@import '../Base.scss'; @import '../_base.scss';
.bp4-drawer { .bp4-drawer {
.bp4-drawer-header { .bp4-drawer-header {

View File

@@ -1,4 +1,4 @@
@import '../../../Base.scss'; @import '../../../_base.scss';
.view-detail-drawer { .view-detail-drawer {
.bp4-tabs { .bp4-tabs {

View File

@@ -1,4 +1,4 @@
@import 'src/style/Base.scss'; @import 'src/style/_base.scss';
.sidebar { .sidebar {
background: $sidebar-background; background: $sidebar-background;

View File

@@ -21,12 +21,9 @@ label.bp4-label {
.required { .required {
color: red; color: red;
} }
.bp4-form-group.bp4-inline & { .bp4-form-group.bp4-inline & {
margin: 0 10px 0 0; margin: 0 10px 0 0;
line-height: 1.6; line-height: 30px;
padding-top: calc(0.3rem + 1px);
padding-bottom: calc(0.3rem + 1px);
} }
} }

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
// Account Form Dialog. // Account Form Dialog.
// ---------------------------- // ----------------------------

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
.dashboard__insider--accounts-chart { .dashboard__insider--accounts-chart {
.bigcapital-datatable { .bigcapital-datatable {

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
.page-form--customer { .page-form--customer {
$self: '.page-form'; $self: '.page-form';

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
$dashboard-views-bar-height: 44px; $dashboard-views-bar-height: 44px;
.dashboard { .dashboard {

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
.dashboard-content--preferences { .dashboard-content--preferences {
flex-direction: row; flex-direction: row;

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
// Preferences sidebar. // Preferences sidebar.
// ----------------------------- // -----------------------------

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
// Preferences topbar. // Preferences topbar.
// ----------------------------- // -----------------------------

View File

@@ -1,4 +1,4 @@
@import "../../Base.scss"; @import "../../_base.scss";
.setup-page { .setup-page {
max-width: 1600px; max-width: 1600px;

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
.page-form--vendor { .page-form--vendor {
$self: '.page-form'; $self: '.page-form';

View File

@@ -1,4 +1,4 @@
@import '../../Base.scss'; @import '../../_base.scss';
body.page-vendor-new, body.page-vendor-new,
body.page-vendor-edit{ body.page-vendor-edit{

View File

@@ -1,6 +1,6 @@
{ {
"installCommand": "npm install && npm run bootstrap", "installCommand": "pnpm install",
"buildCommand": "CI='' npm run build:webapp", "buildCommand": "CI='' pnpm run build:webapp",
"outputDirectory": "packages/webapp/build", "outputDirectory": "packages/webapp/build",
"rewrites": [{ "rewrites": [{
"source": "/api/:slug*", "source": "/api/:slug*",