mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
feat: assign default sell/purchase tax rates to items (#261)
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscr
|
|||||||
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
|
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
|
||||||
import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber';
|
import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber';
|
||||||
import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber';
|
import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber';
|
||||||
|
import { SyncItemTaxRateOnEditTaxSubscriber } from '@/services/TaxRates/SyncItemTaxRateOnEditTaxSubscriber';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -197,5 +198,7 @@ export const susbcribers = () => {
|
|||||||
// Tax Rates - Bills
|
// Tax Rates - Bills
|
||||||
BillTaxRateValidateSubscriber,
|
BillTaxRateValidateSubscriber,
|
||||||
WriteBillTaxTransactionsSubscriber,
|
WriteBillTaxTransactionsSubscriber,
|
||||||
|
|
||||||
|
SyncItemTaxRateOnEditTaxSubscriber
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
62
packages/webapp/src/components/TaxRates/TaxRatesSelect.tsx
Normal file
62
packages/webapp/src/components/TaxRates/TaxRatesSelect.tsx
Normal 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);
|
||||||
@@ -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')}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ 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,
|
||||||
@@ -61,6 +61,7 @@ function ItemEntriesTableRoot() {
|
|||||||
currencyCode,
|
currencyCode,
|
||||||
landedCost,
|
landedCost,
|
||||||
taxRates,
|
taxRates,
|
||||||
|
itemType,
|
||||||
} = useItemEntriesTableContext();
|
} = useItemEntriesTableContext();
|
||||||
|
|
||||||
// Editiable items entries columns.
|
// Editiable items entries columns.
|
||||||
@@ -68,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);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @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, groupBy } from 'lodash';
|
import { sumBy, isEmpty, last, keyBy, groupBy } from 'lodash';
|
||||||
import { useItem } from '@/hooks/query';
|
import { useItem } from '@/hooks/query';
|
||||||
@@ -116,19 +116,20 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
|
|||||||
? item.purchase_description
|
? item.purchase_description
|
||||||
: item.sell_description;
|
: item.sell_description;
|
||||||
|
|
||||||
|
// Detarmines whether the landed cost checkbox should be disabled.
|
||||||
|
const landedCostDisabled = isLandedCostDisabled(item);
|
||||||
|
|
||||||
const taxRateId =
|
const taxRateId =
|
||||||
itemType === ITEM_TYPE.PURCHASABLE
|
itemType === ITEM_TYPE.PURCHASABLE
|
||||||
? item.purchase_tax_rate_id
|
? item.purchase_tax_rate_id
|
||||||
: item.sell_tax_rate_id;
|
: item.sell_tax_rate_id;
|
||||||
|
|
||||||
// Detarmines whether the landed cost checkbox should be disabled.
|
|
||||||
const landedCostDisabled = isLandedCostDisabled(item);
|
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -164,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.
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -334,6 +334,8 @@
|
|||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user