mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: Item validate cost, income and inventory account type.
feat: Style sales and purchases forms - 80% progress. feat: Validate purchase-able and sell-able items in invoices and bills. feat: Fix bugs in inventory FIFO/LIFO cost methods.
This commit is contained in:
4
client/src/components/classes.js
Normal file
4
client/src/components/classes.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export default {
|
||||||
|
DATATABLE_EDITOR: 'datatable-editor',
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import * as Yup from 'yup';
|
|||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
|
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
|
||||||
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
@@ -291,15 +292,22 @@ function BillForm({
|
|||||||
onClickAddNewRow={onClickAddNewRow}
|
onClickAddNewRow={onClickAddNewRow}
|
||||||
onClickClearAllLines={onClickCleanAllLines}
|
onClickClearAllLines={onClickCleanAllLines}
|
||||||
/>
|
/>
|
||||||
<FormGroup label={<T id={'note'} />} className={'form-group--'}>
|
<Row>
|
||||||
<TextArea growVertically={true} {...formik.getFieldProps('note')} />
|
<Col>
|
||||||
</FormGroup>
|
<FormGroup label={<T id={'note'} />} className={'form-group--'}>
|
||||||
<Dragzone
|
<TextArea growVertically={true} {...formik.getFieldProps('note')} />
|
||||||
initialFiles={initialAttachmentFiles}
|
</FormGroup>
|
||||||
onDrop={handleDropFiles}
|
</Col>
|
||||||
onDeleteFile={handleDeleteFile}
|
|
||||||
hint={'Attachments: Maxiumum size: 20MB'}
|
<Col>
|
||||||
/>
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</form>
|
</form>
|
||||||
<BillFormFooter
|
<BillFormFooter
|
||||||
formik={formik}
|
formik={formik}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function BillFormFooter({
|
|||||||
bill,
|
bill,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={'form__floating-footer'}>
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
|
|||||||
@@ -76,12 +76,10 @@ function BillFormHeader({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(vendorsCurrentPage, 'vendorsCurrentPage');
|
|
||||||
console.log(vendorItems, 'vendorItems');
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="page-form page-form--bill">
|
||||||
<div>
|
<div className={'page-form__primary-section'}>
|
||||||
{/* vendor account name */}
|
{/* Vendor account name */}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'vendor_name'} />}
|
label={<T id={'vendor_name'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import moment from 'moment';
|
|||||||
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
|
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
|
||||||
|
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
import { pick, omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
|
||||||
import BillFormHeader from './BillFormHeader';
|
import BillFormHeader from './BillFormHeader';
|
||||||
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
|
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
|
||||||
@@ -273,15 +274,24 @@ function BillForm({
|
|||||||
onClickAddNewRow={onClickAddNewRow}
|
onClickAddNewRow={onClickAddNewRow}
|
||||||
onClickClearAllLines={onClickCleanAllLines}
|
onClickClearAllLines={onClickCleanAllLines}
|
||||||
/>
|
/>
|
||||||
<FormGroup label={<T id={'note'} />} className={'form-group--'}>
|
|
||||||
<TextArea growVertically={true} {...formik.getFieldProps('note')} />
|
<Row>
|
||||||
</FormGroup>
|
<Col>
|
||||||
<Dragzone
|
<FormGroup label={<T id={'note'} />} className={'form-group--'}>
|
||||||
initialFiles={initialAttachmentFiles}
|
<TextArea growVertically={true} {...formik.getFieldProps('note')} />
|
||||||
onDrop={handleDropFiles}
|
</FormGroup>
|
||||||
onDeleteFile={handleDeleteFile}
|
</Col>
|
||||||
hint={'Attachments: Maxiumum size: 20MB'}
|
|
||||||
/>
|
<Col>
|
||||||
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<BillFormFooter
|
<BillFormFooter
|
||||||
formik={formik}
|
formik={formik}
|
||||||
onSubmit={handleSubmitClick}
|
onSubmit={handleSubmitClick}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||||
|
import { omit } from 'lodash';
|
||||||
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
|
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import CLASSES from 'components/classes';
|
||||||
import DataTable from 'components/DataTable';
|
import DataTable from 'components/DataTable';
|
||||||
import Icon from 'components/Icon';
|
import Icon from 'components/Icon';
|
||||||
|
|
||||||
import { compose, formattedAmount } from 'utils';
|
|
||||||
import {
|
import {
|
||||||
InputGroupCell,
|
InputGroupCell,
|
||||||
MoneyFieldCell,
|
MoneyFieldCell,
|
||||||
@@ -14,7 +15,7 @@ import {
|
|||||||
} from 'components/DataTableCells';
|
} from 'components/DataTableCells';
|
||||||
|
|
||||||
import withItems from 'containers/Items/withItems';
|
import withItems from 'containers/Items/withItems';
|
||||||
import { omit } from 'lodash';
|
import { compose, formattedAmount } from 'utils';
|
||||||
|
|
||||||
const ActionsCellRenderer = ({
|
const ActionsCellRenderer = ({
|
||||||
row: { index },
|
row: { index },
|
||||||
@@ -92,6 +93,7 @@ function EstimateTable({
|
|||||||
width: 40,
|
width: 40,
|
||||||
disableResizing: true,
|
disableResizing: true,
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
|
className: 'index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: formatMessage({ id: 'product_and_service' }),
|
Header: formatMessage({ id: 'product_and_service' }),
|
||||||
@@ -108,6 +110,7 @@ function EstimateTable({
|
|||||||
Cell: InputGroupCell,
|
Cell: InputGroupCell,
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
className: 'description',
|
className: 'description',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -123,7 +126,7 @@ function EstimateTable({
|
|||||||
accessor: 'rate',
|
accessor: 'rate',
|
||||||
Cell: TotalEstimateCellRederer(MoneyFieldCell, 'rate'),
|
Cell: TotalEstimateCellRederer(MoneyFieldCell, 'rate'),
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
width: 150,
|
width: 100,
|
||||||
className: 'rate',
|
className: 'rate',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -230,8 +233,9 @@ function EstimateTable({
|
|||||||
updateData: handleUpdateData,
|
updateData: handleUpdateData,
|
||||||
removeRow: handleRemoveRow,
|
removeRow: handleRemoveRow,
|
||||||
}}
|
}}
|
||||||
|
className={CLASSES.DATATABLE_EDITOR}
|
||||||
/>
|
/>
|
||||||
<div className={'mt1'}>
|
<div className={'datatable-editor-actions mt1'}>
|
||||||
<Button
|
<Button
|
||||||
small={true}
|
small={true}
|
||||||
className={'button--secondary button--new-line'}
|
className={'button--secondary button--new-line'}
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Intent, FormGroup, TextArea, Button } from '@blueprintjs/core';
|
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
import { pick, omit } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { queryCache } from 'react-query';
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
|
||||||
import EstimateFormHeader from './EstimateFormHeader';
|
import EstimateFormHeader from './EstimateFormHeader';
|
||||||
import EstimatesItemsTable from './EntriesItemsTable';
|
import EstimatesItemsTable from './EntriesItemsTable';
|
||||||
@@ -103,13 +102,11 @@ const EstimateForm = ({
|
|||||||
.min(1)
|
.min(1)
|
||||||
.max(1024)
|
.max(1024)
|
||||||
.label(formatMessage({ id: 'note' })),
|
.label(formatMessage({ id: 'note' })),
|
||||||
|
|
||||||
terms_conditions: Yup.string()
|
terms_conditions: Yup.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(1024)
|
.max(1024)
|
||||||
.label(formatMessage({ id: 'note' })),
|
.label(formatMessage({ id: 'note' })),
|
||||||
|
|
||||||
entries: Yup.array().of(
|
entries: Yup.array().of(
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
quantity: Yup.number().nullable(),
|
quantity: Yup.number().nullable(),
|
||||||
@@ -320,27 +317,35 @@ const EstimateForm = ({
|
|||||||
formik={formik}
|
formik={formik}
|
||||||
// defaultRow={defaultEstimate}
|
// defaultRow={defaultEstimate}
|
||||||
/>
|
/>
|
||||||
<FormGroup
|
|
||||||
label={<T id={'customer_note'} />}
|
<Row>
|
||||||
className={'form-group--customer_note'}
|
<Col>
|
||||||
>
|
<FormGroup
|
||||||
<TextArea growVertically={true} {...formik.getFieldProps('note')} />
|
label={<T id={'customer_note'} />}
|
||||||
</FormGroup>
|
className={'form-group--customer_note'}
|
||||||
<FormGroup
|
>
|
||||||
label={<T id={'terms_conditions'} />}
|
<TextArea growVertically={true} {...formik.getFieldProps('note')} />
|
||||||
className={'form-group--terms_conditions'}
|
</FormGroup>
|
||||||
>
|
<FormGroup
|
||||||
<TextArea
|
label={<T id={'terms_conditions'} />}
|
||||||
growVertically={true}
|
className={'form-group--terms_conditions'}
|
||||||
{...formik.getFieldProps('terms_conditions')}
|
>
|
||||||
/>
|
<TextArea
|
||||||
</FormGroup>
|
growVertically={true}
|
||||||
<Dragzone
|
{...formik.getFieldProps('terms_conditions')}
|
||||||
initialFiles={initialAttachmentFiles}
|
/>
|
||||||
onDrop={handleDropFiles}
|
</FormGroup>
|
||||||
onDeleteFile={handleDeleteFile}
|
</Col>
|
||||||
hint={'Attachments: Maxiumum size: 20MB'}
|
|
||||||
/>
|
<Col>
|
||||||
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</form>
|
</form>
|
||||||
<EstimateFormFooter
|
<EstimateFormFooter
|
||||||
formik={formik}
|
formik={formik}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function EstimateFormFooter({
|
|||||||
estimate,
|
estimate,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={'estimate-form__floating-footer'}>
|
<div className={'form__floating-footer'}>
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
|
|||||||
@@ -68,13 +68,16 @@ function EstimateFormHeader({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'estimate-form'}>
|
<div className={'page-form page-form--estimate'}>
|
||||||
<div className={'estimate-form__primary-section'}>
|
<div className={'page-form__primary-section'}>
|
||||||
{/* customer name */}
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'customer_name'} />}
|
label={<T id={'customer_name'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames(
|
||||||
|
'form-group--select-list',
|
||||||
|
'form-group--customer',
|
||||||
|
Classes.FILL,
|
||||||
|
)}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
intent={errors.customer_id && touched.customer_id && Intent.DANGER}
|
intent={errors.customer_id && touched.customer_id && Intent.DANGER}
|
||||||
helperText={
|
helperText={
|
||||||
@@ -94,7 +97,7 @@ function EstimateFormHeader({
|
|||||||
labelProp={'display_name'}
|
labelProp={'display_name'}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{/* estimate_date */}
|
|
||||||
<Row>
|
<Row>
|
||||||
<Col
|
<Col
|
||||||
|
|
||||||
@@ -104,7 +107,11 @@ function EstimateFormHeader({
|
|||||||
label={<T id={'estimate_date'} />}
|
label={<T id={'estimate_date'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames(
|
||||||
|
'form-group--select-list',
|
||||||
|
Classes.FILL,
|
||||||
|
'form-group--estimate-date'
|
||||||
|
)}
|
||||||
intent={
|
intent={
|
||||||
errors.estimate_date && touched.estimate_date && Intent.DANGER
|
errors.estimate_date && touched.estimate_date && Intent.DANGER
|
||||||
}
|
}
|
||||||
@@ -127,7 +134,11 @@ function EstimateFormHeader({
|
|||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'expiration_date'} />}
|
label={<T id={'expiration_date'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames(
|
||||||
|
'form-group--select-list',
|
||||||
|
'form-group--expiration-date',
|
||||||
|
Classes.FILL
|
||||||
|
)}
|
||||||
intent={
|
intent={
|
||||||
errors.expiration_date &&
|
errors.expiration_date &&
|
||||||
touched.expiration_date &&
|
touched.expiration_date &&
|
||||||
@@ -147,11 +158,12 @@ function EstimateFormHeader({
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
{/* Estimate */}
|
|
||||||
|
{/*- Estimate -*/}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'estimate'} />}
|
label={<T id={'estimate'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
className={('form-group--estimate', Classes.FILL)}
|
className={('form-group--estimate-number', Classes.FILL)}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
intent={
|
intent={
|
||||||
errors.estimate_number && touched.estimate_number && Intent.DANGER
|
errors.estimate_number && touched.estimate_number && Intent.DANGER
|
||||||
@@ -168,6 +180,7 @@ function EstimateFormHeader({
|
|||||||
{...getFieldProps('estimate_number')}
|
{...getFieldProps('estimate_number')}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'reference'} />}
|
label={<T id={'reference'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function Estimates({
|
|||||||
requestFetchCustomers({}),
|
requestFetchCustomers({}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//
|
||||||
const handleFormSubmit = useCallback(
|
const handleFormSubmit = useCallback(
|
||||||
(payload) => {
|
(payload) => {
|
||||||
payload.redirect && history.push('/estimates');
|
payload.redirect && history.push('/estimates');
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default (mapState) => {
|
|||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const query = getEstimatesTableQuery(state, props);
|
const query = getEstimatesTableQuery(state, props);
|
||||||
|
|
||||||
const mapped = {
|
const mapped = {
|
||||||
estimatesCurrentPage: getEstimatesItems(state, props, query),
|
estimatesCurrentPage: getEstimatesItems(state, props, query),
|
||||||
estimateViews: getResourceViews(state, props, 'sales_estimates'),
|
estimateViews: getResourceViews(state, props, 'sales_estimates'),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import moment from 'moment';
|
|||||||
import { Intent, FormGroup, TextArea, Button } from '@blueprintjs/core';
|
import { Intent, FormGroup, TextArea, Button } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
|
||||||
import InvoiceFormHeader from './InvoiceFormHeader';
|
import InvoiceFormHeader from './InvoiceFormHeader';
|
||||||
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
|
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
|
||||||
@@ -308,31 +309,38 @@ function InvoiceForm({
|
|||||||
onClickClearAllLines={handleClearAllLines}
|
onClickClearAllLines={handleClearAllLines}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
/>
|
/>
|
||||||
<FormGroup
|
<Row>
|
||||||
label={<T id={'invoice_message'} />}
|
<Col>
|
||||||
className={'form-group--customer_note'}
|
<FormGroup
|
||||||
>
|
label={<T id={'invoice_message'} />}
|
||||||
<TextArea
|
className={'form-group--customer_note'}
|
||||||
growVertically={true}
|
>
|
||||||
{...formik.getFieldProps('invoice_message')}
|
<TextArea
|
||||||
/>
|
growVertically={true}
|
||||||
</FormGroup>
|
{...formik.getFieldProps('invoice_message')}
|
||||||
<FormGroup
|
/>
|
||||||
label={<T id={'terms_conditions'} />}
|
</FormGroup>
|
||||||
className={'form-group--terms_conditions'}
|
<FormGroup
|
||||||
>
|
label={<T id={'terms_conditions'} />}
|
||||||
<TextArea
|
className={'form-group--terms_conditions'}
|
||||||
growVertically={true}
|
>
|
||||||
{...formik.getFieldProps('terms_conditions')}
|
<TextArea
|
||||||
/>
|
growVertically={true}
|
||||||
</FormGroup>
|
{...formik.getFieldProps('terms_conditions')}
|
||||||
<Dragzone
|
/>
|
||||||
initialFiles={initialAttachmentFiles}
|
</FormGroup>
|
||||||
onDrop={handleDropFiles}
|
</Col>
|
||||||
onDeleteFile={handleDeleteFile}
|
|
||||||
hint={'Attachments: Maxiumum size: 20MB'}
|
<Col>
|
||||||
/>
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
/></Col>
|
||||||
|
</Row>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<InvoiceFormFooter
|
<InvoiceFormFooter
|
||||||
formik={formik}
|
formik={formik}
|
||||||
onSubmitClick={handleSubmitClick}
|
onSubmitClick={handleSubmitClick}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function EstimateFormFooter({
|
|||||||
invoice,
|
invoice,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={'estimate-form__floating-footer'}>
|
<div className={'form__floating-footer'}>
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ function InvoiceFormHeader({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'invoice-form'}>
|
<div class="page-form page-form--invoice">
|
||||||
<div className={'invoice__primary-section'}>
|
<div className={'page-form__primary-section'}>
|
||||||
{/* customer name */}
|
{/* customer name */}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'customer_name'} />}
|
label={<T id={'customer_name'} />}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import moment from 'moment';
|
|||||||
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
|
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
|
||||||
import ReceiptFromHeader from './ReceiptFormHeader';
|
import ReceiptFromHeader from './ReceiptFormHeader';
|
||||||
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
|
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
|
||||||
@@ -296,31 +297,38 @@ function ReceiptForm({
|
|||||||
onClickClearAllLines={handleClearAllLines}
|
onClickClearAllLines={handleClearAllLines}
|
||||||
formik={formik}
|
formik={formik}
|
||||||
/>
|
/>
|
||||||
<FormGroup
|
|
||||||
label={<T id={'receipt_message'} />}
|
|
||||||
className={'form-group--'}
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
growVertically={true}
|
|
||||||
{...formik.getFieldProps('receipt_message')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'statement'} />}
|
|
||||||
className={'form-group--statement'}
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
growVertically={true}
|
|
||||||
{...formik.getFieldProps('statement')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<Dragzone
|
<Row>
|
||||||
initialFiles={initialAttachmentFiles}
|
<Col>
|
||||||
onDrop={handleDropFiles}
|
<FormGroup
|
||||||
onDeleteFile={handleDeleteFile}
|
label={<T id={'receipt_message'} />}
|
||||||
hint={'Attachments: Maxiumum size: 20MB'}
|
className={'form-group--'}
|
||||||
/>
|
>
|
||||||
|
<TextArea
|
||||||
|
growVertically={true}
|
||||||
|
{...formik.getFieldProps('receipt_message')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'statement'} />}
|
||||||
|
className={'form-group--statement'}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
growVertically={true}
|
||||||
|
{...formik.getFieldProps('statement')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col>
|
||||||
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</form>
|
</form>
|
||||||
<ReceiptFormFooter
|
<ReceiptFormFooter
|
||||||
formik={formik}
|
formik={formik}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function ReceiptFormFooter({
|
|||||||
receipt,
|
receipt,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={'estimate-form__floating-footer'}>
|
<div className={'form__floating-footer'}>
|
||||||
<Button
|
<Button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
|
|||||||
@@ -83,13 +83,17 @@ function ReceiptFormHeader({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="page-form receipt-form">
|
||||||
<div>
|
<div class="page-form__primary-section">
|
||||||
{/* customer name */}
|
{/*- Customer name -*/}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'customer_name'} />}
|
label={<T id={'customer_name'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames(
|
||||||
|
'form-group--select-list',
|
||||||
|
Classes.FILL,
|
||||||
|
'form-group--customer',
|
||||||
|
)}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
intent={errors.customer_id && touched.customer_id && Intent.DANGER}
|
intent={errors.customer_id && touched.customer_id && Intent.DANGER}
|
||||||
helperText={
|
helperText={
|
||||||
@@ -110,10 +114,11 @@ function ReceiptFormHeader({
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
{/*- Deposit account -*/}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'deposit_account'} />}
|
label={<T id={'deposit_account'} />}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'form-group--deposit_account_id',
|
'form-group--deposit-account',
|
||||||
'form-group--select-list',
|
'form-group--select-list',
|
||||||
Classes.FILL,
|
Classes.FILL,
|
||||||
)}
|
)}
|
||||||
@@ -172,6 +177,7 @@ function ReceiptFormHeader({
|
|||||||
/>
|
/>
|
||||||
</FormGroup> */}
|
</FormGroup> */}
|
||||||
|
|
||||||
|
{/*- Reference -*/}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'reference'} />}
|
label={<T id={'reference'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
@@ -185,6 +191,8 @@ function ReceiptFormHeader({
|
|||||||
{...getFieldProps('reference_no')}
|
{...getFieldProps('reference_no')}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
{/*- Send to email -*/}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={<T id={'send_to_email'} />}
|
label={<T id={'send_to_email'} />}
|
||||||
inline={true}
|
inline={true}
|
||||||
|
|||||||
@@ -59,10 +59,13 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
|||||||
@import 'pages/invite-user.scss';
|
@import 'pages/invite-user.scss';
|
||||||
@import 'pages/exchange-rate.scss';
|
@import 'pages/exchange-rate.scss';
|
||||||
@import 'pages/customer.scss';
|
@import 'pages/customer.scss';
|
||||||
@import 'pages/estimate.scss';
|
@import 'pages/estimates';
|
||||||
|
@import 'pages/receipts';
|
||||||
|
@import 'pages/invoices';
|
||||||
|
|
||||||
// Views
|
|
||||||
@import 'views/filter-dropdown';
|
// Views
|
||||||
|
@import 'views/filter-dropdown';
|
||||||
@import 'views/sidebar';
|
@import 'views/sidebar';
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
|
|||||||
@@ -1,379 +0,0 @@
|
|||||||
.estimate-form {
|
|
||||||
padding-bottom: 30px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
.bp3-form-group {
|
|
||||||
width: 100%;
|
|
||||||
margin: 25px 20px 15px;
|
|
||||||
}
|
|
||||||
.bp3-label {
|
|
||||||
margin: 0 20px 0;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #444;
|
|
||||||
width: 130px;
|
|
||||||
}
|
|
||||||
.bp3-form-content {
|
|
||||||
width: 35%;
|
|
||||||
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
|
|
||||||
width: 120%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__table {
|
|
||||||
padding: 15px 15px 0;
|
|
||||||
|
|
||||||
.bp3-form-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.table {
|
|
||||||
border: 1px dotted rgb(195, 195, 195);
|
|
||||||
border-bottom: transparent;
|
|
||||||
border-left: transparent;
|
|
||||||
|
|
||||||
.th,
|
|
||||||
.td {
|
|
||||||
border-left: 1px dotted rgb(195, 195, 195);
|
|
||||||
|
|
||||||
&.index {
|
|
||||||
> span,
|
|
||||||
> div {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.thead {
|
|
||||||
.tr .th {
|
|
||||||
padding: 10px 10px;
|
|
||||||
background-color: #f2f5fa;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tbody {
|
|
||||||
.tr .td {
|
|
||||||
padding: 7px;
|
|
||||||
border-bottom: 1px dotted rgb(195, 195, 195);
|
|
||||||
min-height: 46px;
|
|
||||||
|
|
||||||
&.index {
|
|
||||||
background-color: #f2f5fa;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tr {
|
|
||||||
.bp3-form-group .bp3-input,
|
|
||||||
.form-group--select-list .bp3-button {
|
|
||||||
border-radius: 3px;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
|
||||||
.form-group--select-list:not(.bp3-intent-danger) .bp3-button {
|
|
||||||
border-color: #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
.td {
|
|
||||||
border-bottom: transparent;
|
|
||||||
|
|
||||||
.bp3-button,
|
|
||||||
.bp3-input-group {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.td.actions {
|
|
||||||
.bp3-button {
|
|
||||||
background-color: transparent;
|
|
||||||
color: #e68f8e;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #c23030;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.row--total {
|
|
||||||
.td.amount {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.th {
|
|
||||||
color: #444;
|
|
||||||
font-weight: 600;
|
|
||||||
border-bottom: 1px dotted #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td {
|
|
||||||
border-bottom: 1px dotted #999;
|
|
||||||
|
|
||||||
&.description {
|
|
||||||
.bp3-form-group {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions.td {
|
|
||||||
.bp3-button {
|
|
||||||
background: transparent;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__floating-footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
padding: 18px 18px;
|
|
||||||
border-top: 1px solid #ececec;
|
|
||||||
|
|
||||||
.has-mini-sidebar & {
|
|
||||||
left: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bp3-button {
|
|
||||||
&.button--clear-lines {
|
|
||||||
background-color: #fcefef;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.button--clear-lines,
|
|
||||||
.button--new-line {
|
|
||||||
padding-left: 14px;
|
|
||||||
padding-right: 14px;
|
|
||||||
}
|
|
||||||
.dropzone-container {
|
|
||||||
margin-top: 0;
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
.dropzone {
|
|
||||||
width: 300px;
|
|
||||||
height: 75px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group--description {
|
|
||||||
.bp3-label {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
.bp3-form-content {
|
|
||||||
// width: 280px;
|
|
||||||
textarea {
|
|
||||||
width: 450px;
|
|
||||||
min-height: 75px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// .estimate-form {
|
|
||||||
// padding-bottom: 30px;
|
|
||||||
// display: flex;
|
|
||||||
// flex-direction: column;
|
|
||||||
|
|
||||||
// .bp3-form-group {
|
|
||||||
// margin: 25px 20px 15px;
|
|
||||||
// width: 100%;
|
|
||||||
// .bp3-label {
|
|
||||||
// font-weight: 500;
|
|
||||||
// font-size: 13px;
|
|
||||||
// color: #444;
|
|
||||||
// width: 130px;
|
|
||||||
// }
|
|
||||||
// .bp3-form-content {
|
|
||||||
// // width: 400px;
|
|
||||||
// width: 45%;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // .expense-form-footer {
|
|
||||||
// // display: flex;
|
|
||||||
// // padding: 30px 25px 0;
|
|
||||||
// // justify-content: space-between;
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// &__primary-section {
|
|
||||||
// background: #fbfbfb;
|
|
||||||
// }
|
|
||||||
// &__table {
|
|
||||||
// padding: 15px 15px 0;
|
|
||||||
|
|
||||||
// .bp3-form-group {
|
|
||||||
// margin-bottom: 0;
|
|
||||||
// }
|
|
||||||
// .table {
|
|
||||||
// border: 1px dotted rgb(195, 195, 195);
|
|
||||||
// border-bottom: transparent;
|
|
||||||
// border-left: transparent;
|
|
||||||
|
|
||||||
// .th,
|
|
||||||
// .td {
|
|
||||||
// border-left: 1px dotted rgb(195, 195, 195);
|
|
||||||
|
|
||||||
// &.index {
|
|
||||||
// > span,
|
|
||||||
// > div {
|
|
||||||
// text-align: center;
|
|
||||||
// width: 100%;
|
|
||||||
// font-weight: 500;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .thead {
|
|
||||||
// .tr .th {
|
|
||||||
// padding: 10px 10px;
|
|
||||||
// background-color: #f2f5fa;
|
|
||||||
// font-size: 14px;
|
|
||||||
// font-weight: 500;
|
|
||||||
// color: #333;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .tbody {
|
|
||||||
// .tr .td {
|
|
||||||
// padding: 7px;
|
|
||||||
// border-bottom: 1px dotted rgb(195, 195, 195);
|
|
||||||
// min-height: 46px;
|
|
||||||
|
|
||||||
// &.index {
|
|
||||||
// background-color: #f2f5fa;
|
|
||||||
// text-align: center;
|
|
||||||
|
|
||||||
// > span {
|
|
||||||
// margin-top: auto;
|
|
||||||
// margin-bottom: auto;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .tr {
|
|
||||||
// .bp3-form-group .bp3-input,
|
|
||||||
// .form-group--select-list .bp3-button {
|
|
||||||
// border-radius: 3px;
|
|
||||||
// padding-left: 8px;
|
|
||||||
// padding-right: 8px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
|
||||||
// .form-group--select-list:not(.bp3-intent-danger) .bp3-button {
|
|
||||||
// border-color: #e5e5e5;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// &:last-of-type {
|
|
||||||
// .td {
|
|
||||||
// border-bottom: transparent;
|
|
||||||
|
|
||||||
// .bp3-button,
|
|
||||||
// .bp3-input-group {
|
|
||||||
// display: none;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .td.actions {
|
|
||||||
// .bp3-button {
|
|
||||||
// background-color: transparent;
|
|
||||||
// color: #e68f8e;
|
|
||||||
|
|
||||||
// &:hover {
|
|
||||||
// color: #c23030;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// &.row--total {
|
|
||||||
// .td.amount {
|
|
||||||
// font-weight: bold;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .th {
|
|
||||||
// color: #444;
|
|
||||||
// font-weight: 600;
|
|
||||||
// border-bottom: 1px dotted #666;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .td {
|
|
||||||
// border-bottom: 1px dotted #999;
|
|
||||||
|
|
||||||
// &.description {
|
|
||||||
// .bp3-form-group {
|
|
||||||
// width: 100%;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .actions.td {
|
|
||||||
// .bp3-button {
|
|
||||||
// background: transparent;
|
|
||||||
// margin: 0;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// &__floating-footer {
|
|
||||||
// position: fixed;
|
|
||||||
// bottom: 0;
|
|
||||||
// width: 100%;
|
|
||||||
// background: #fff;
|
|
||||||
// padding: 18px 18px;
|
|
||||||
// border-top: 1px solid #ececec;
|
|
||||||
|
|
||||||
// .has-mini-sidebar & {
|
|
||||||
// left: 50px;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .bp3-button {
|
|
||||||
// &.button--clear-lines {
|
|
||||||
// background-color: #fcefef;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .button--clear-lines,
|
|
||||||
// .button--new-line {
|
|
||||||
// padding-left: 14px;
|
|
||||||
// padding-right: 14px;
|
|
||||||
// }
|
|
||||||
// .dropzone-container {
|
|
||||||
// margin-top: 0;
|
|
||||||
// align-self: flex-end;
|
|
||||||
// }
|
|
||||||
// .dropzone {
|
|
||||||
// width: 300px;
|
|
||||||
// height: 75px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .form-group--description {
|
|
||||||
// .bp3-label {
|
|
||||||
// font-weight: 500;
|
|
||||||
// font-size: 13px;
|
|
||||||
// color: #444;
|
|
||||||
// }
|
|
||||||
// .bp3-form-content {
|
|
||||||
// // width: 280px;
|
|
||||||
// textarea {
|
|
||||||
// width: 450px;
|
|
||||||
// min-height: 75px;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
159
client/src/style/pages/estimates.scss
Normal file
159
client/src/style/pages/estimates.scss
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
.page-form{
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.bp3-form-group{
|
||||||
|
|
||||||
|
.bp3-label{
|
||||||
|
width: 100%;
|
||||||
|
max-width: 170px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
.bp3-form-content{
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__primary-section {
|
||||||
|
background: #fbfbfb;
|
||||||
|
margin: -15px -15px 25px;
|
||||||
|
padding: 30px 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group{
|
||||||
|
|
||||||
|
&--customer{
|
||||||
|
|
||||||
|
.bp3-form-content{
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.datatable-editor {
|
||||||
|
padding: 15px 15px 0;
|
||||||
|
|
||||||
|
&-actions{
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
.bp3-button.button--clear-lines {
|
||||||
|
background-color: #fcefef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
border: 1px dotted rgb(195, 195, 195);
|
||||||
|
border-bottom: transparent;
|
||||||
|
border-left: transparent;
|
||||||
|
|
||||||
|
.th,
|
||||||
|
.td {
|
||||||
|
border-left: 1px dotted rgb(195, 195, 195);
|
||||||
|
|
||||||
|
&.index {
|
||||||
|
> span,
|
||||||
|
> div {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thead {
|
||||||
|
.tr .th {
|
||||||
|
padding: 10px 10px;
|
||||||
|
background-color: #f2f5fa;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbody {
|
||||||
|
.tr .td {
|
||||||
|
padding: 7px;
|
||||||
|
border-bottom: 1px dotted rgb(195, 195, 195);
|
||||||
|
min-height: 46px;
|
||||||
|
|
||||||
|
&.index {
|
||||||
|
background-color: #f2f5fa;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tr {
|
||||||
|
.bp3-form-group .bp3-input,
|
||||||
|
.form-group--select-list .bp3-button {
|
||||||
|
border-radius: 3px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
||||||
|
.form-group--select-list:not(.bp3-intent-danger) .bp3-button {
|
||||||
|
border-color: #E5E5E5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
.td {
|
||||||
|
border-bottom: transparent;
|
||||||
|
|
||||||
|
.bp3-button,
|
||||||
|
.bp3-input-group {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.td.actions {
|
||||||
|
.bp3-button {
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
svg{
|
||||||
|
color: #e68f8e;
|
||||||
|
}
|
||||||
|
&:hover svg{
|
||||||
|
color: #c23030;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.row--total {
|
||||||
|
|
||||||
|
.td.amount{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.th {
|
||||||
|
color: #444;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px dotted #666;
|
||||||
|
}
|
||||||
|
.td {
|
||||||
|
border-bottom: 1px dotted #999;
|
||||||
|
|
||||||
|
&.description{
|
||||||
|
.bp3-form-group{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.actions.td {
|
||||||
|
.bp3-button {
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
client/src/style/pages/invoices.scss
Normal file
0
client/src/style/pages/invoices.scss
Normal file
0
client/src/style/pages/receipts.scss
Normal file
0
client/src/style/pages/receipts.scss
Normal file
@@ -135,6 +135,26 @@ exports.seed = (knex) => {
|
|||||||
index: 1,
|
index: 1,
|
||||||
active: 1,
|
active: 1,
|
||||||
description: 1,
|
description: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
name: 'Inventory Asset',
|
||||||
|
account_type_id: 14,
|
||||||
|
predefined: 1,
|
||||||
|
parent_account_id: null,
|
||||||
|
index: 1,
|
||||||
|
active: 1,
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
name: 'Sales of Product Income',
|
||||||
|
account_type_id: 7,
|
||||||
|
predefined: 1,
|
||||||
|
parent_account_id: null,
|
||||||
|
index: 1,
|
||||||
|
active: 1,
|
||||||
|
description: '',
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { check, param, query, oneOf, ValidationChain } from 'express-validator';
|
import { check, param, query, ValidationChain } from 'express-validator';
|
||||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
import validateMiddleware from '@/http/middleware/validateMiddleware';
|
||||||
import ItemsService from '@/services/Items/ItemsService';
|
import ItemsService from '@/services/Items/ItemsService';
|
||||||
@@ -124,9 +124,6 @@ export default class ItemsController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate specific item params schema.
|
* Validate specific item params schema.
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
*/
|
||||||
static get validateSpecificItemSchema(): ValidationChain[] {
|
static get validateSpecificItemSchema(): ValidationChain[] {
|
||||||
return [
|
return [
|
||||||
@@ -135,6 +132,9 @@ export default class ItemsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate list query schema
|
||||||
|
*/
|
||||||
static get validateListQuerySchema() {
|
static get validateListQuerySchema() {
|
||||||
return [
|
return [
|
||||||
query('column_sort_order').optional().isIn(['created_at', 'name', 'amount', 'sku']),
|
query('column_sort_order').optional().isIn(['created_at', 'name', 'amount', 'sku']),
|
||||||
@@ -221,16 +221,21 @@ export default class ItemsController {
|
|||||||
* @param {Function} next
|
* @param {Function} next
|
||||||
*/
|
*/
|
||||||
static async validateCostAccountExistance(req: Request, res: Response, next: Function) {
|
static async validateCostAccountExistance(req: Request, res: Response, next: Function) {
|
||||||
const { Account } = req.models;
|
const { Account, AccountType } = req.models;
|
||||||
const item = req.body;
|
const item = req.body;
|
||||||
|
|
||||||
if (item.cost_account_id) {
|
if (item.cost_account_id) {
|
||||||
const foundAccount = await Account.query().findById(item.cost_account_id);
|
const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold');
|
||||||
|
const foundAccount = await Account.query().findById(item.cost_account_id)
|
||||||
|
|
||||||
if (!foundAccount) {
|
if (!foundAccount) {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
|
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
|
||||||
});
|
});
|
||||||
|
} else if (foundAccount.accountTypeId !== COGSType.id) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
@@ -243,16 +248,21 @@ export default class ItemsController {
|
|||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
static async validateSellAccountExistance(req: Request, res: Response, next: Function) {
|
static async validateSellAccountExistance(req: Request, res: Response, next: Function) {
|
||||||
const { Account } = req.models;
|
const { Account, AccountType } = req.models;
|
||||||
const item = req.body;
|
const item = req.body;
|
||||||
|
|
||||||
if (item.sell_account_id) {
|
if (item.sell_account_id) {
|
||||||
|
const incomeType = await AccountType.query().findOne('key', 'income');
|
||||||
const foundAccount = await Account.query().findById(item.sell_account_id);
|
const foundAccount = await Account.query().findById(item.sell_account_id);
|
||||||
|
|
||||||
if (!foundAccount) {
|
if (!foundAccount) {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
|
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
|
||||||
});
|
});
|
||||||
|
} else if (foundAccount.accountTypeId !== incomeType.id) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
@@ -265,16 +275,21 @@ export default class ItemsController {
|
|||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
static async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
|
static async validateInventoryAccountExistance(req: Request, res: Response, next: Function) {
|
||||||
const { Account } = req.models;
|
const { Account, AccountType } = req.models;
|
||||||
const item = req.body;
|
const item = req.body;
|
||||||
|
|
||||||
if (item.inventory_account_id) {
|
if (item.inventory_account_id) {
|
||||||
|
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
|
||||||
const foundAccount = await Account.query().findById(item.inventory_account_id);
|
const foundAccount = await Account.query().findById(item.inventory_account_id);
|
||||||
|
|
||||||
if (!foundAccount) {
|
if (!foundAccount) {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200}],
|
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200}],
|
||||||
});
|
});
|
||||||
|
} else if (otherAsset.id !== foundAccount.accountTypeId) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default class BillsController extends BaseController {
|
|||||||
asyncMiddleware(this.validateVendorExistance),
|
asyncMiddleware(this.validateVendorExistance),
|
||||||
asyncMiddleware(this.validateItemsIds),
|
asyncMiddleware(this.validateItemsIds),
|
||||||
asyncMiddleware(this.validateBillNumberExists),
|
asyncMiddleware(this.validateBillNumberExists),
|
||||||
|
asyncMiddleware(this.validateNonPurchasableEntriesItems),
|
||||||
asyncMiddleware(this.newBill)
|
asyncMiddleware(this.newBill)
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
@@ -35,6 +36,7 @@ export default class BillsController extends BaseController {
|
|||||||
asyncMiddleware(this.validateVendorExistance),
|
asyncMiddleware(this.validateVendorExistance),
|
||||||
asyncMiddleware(this.validateItemsIds),
|
asyncMiddleware(this.validateItemsIds),
|
||||||
asyncMiddleware(this.validateEntriesIdsExistance),
|
asyncMiddleware(this.validateEntriesIdsExistance),
|
||||||
|
asyncMiddleware(this.validateNonPurchasableEntriesItems),
|
||||||
asyncMiddleware(this.editBill)
|
asyncMiddleware(this.editBill)
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
@@ -201,6 +203,32 @@ export default class BillsController extends BaseController {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the entries items that not purchase-able.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {Function} next
|
||||||
|
*/
|
||||||
|
static async validateNonPurchasableEntriesItems(req, res, next) {
|
||||||
|
const { Item } = req.models;
|
||||||
|
const bill = { ...req.body };
|
||||||
|
const itemsIds = bill.entries.map(e => e.item_id);
|
||||||
|
|
||||||
|
const purchasbleItems = await Item.query()
|
||||||
|
.where('purchasable', true)
|
||||||
|
.whereIn('id', itemsIds);
|
||||||
|
|
||||||
|
const purchasbleItemsIds = purchasbleItems.map((item) => item.id);
|
||||||
|
const notPurchasableItems = difference(itemsIds, purchasbleItemsIds);
|
||||||
|
|
||||||
|
if (notPurchasableItems.length > 0) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'NOT.PURCHASE.ABLE.ITEMS', code: 600 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new bill and records journal transactions.
|
* Creates a new bill and records journal transactions.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import CustomersService from '@/services/Customers/CustomersService';
|
|||||||
import DynamicListing from '@/services/DynamicListing/DynamicListing';
|
import DynamicListing from '@/services/DynamicListing/DynamicListing';
|
||||||
import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder';
|
import DynamicListingBuilder from '@/services/DynamicListing/DynamicListingBuilder';
|
||||||
import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/hasDynamicListing';
|
import { dynamicListingErrorsToResponse } from '@/services/DynamicListing/hasDynamicListing';
|
||||||
import { Customer } from '../../../models';
|
import { Customer, Item } from '../../../models';
|
||||||
|
|
||||||
export default class SaleInvoicesController {
|
export default class SaleInvoicesController {
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +27,7 @@ export default class SaleInvoicesController {
|
|||||||
asyncMiddleware(this.validateInvoiceCustomerExistance),
|
asyncMiddleware(this.validateInvoiceCustomerExistance),
|
||||||
asyncMiddleware(this.validateInvoiceNumberUnique),
|
asyncMiddleware(this.validateInvoiceNumberUnique),
|
||||||
asyncMiddleware(this.validateInvoiceItemsIdsExistance),
|
asyncMiddleware(this.validateInvoiceItemsIdsExistance),
|
||||||
|
asyncMiddleware(this.validateNonSellableEntriesItems),
|
||||||
asyncMiddleware(this.newSaleInvoice)
|
asyncMiddleware(this.newSaleInvoice)
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
@@ -42,6 +43,7 @@ export default class SaleInvoicesController {
|
|||||||
asyncMiddleware(this.validateInvoiceItemsIdsExistance),
|
asyncMiddleware(this.validateInvoiceItemsIdsExistance),
|
||||||
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance),
|
asyncMiddleware(this.valdiateInvoiceEntriesIdsExistance),
|
||||||
asyncMiddleware(this.validateEntriesIdsExistance),
|
asyncMiddleware(this.validateEntriesIdsExistance),
|
||||||
|
asyncMiddleware(this.validateNonSellableEntriesItems),
|
||||||
asyncMiddleware(this.editSaleInvoice)
|
asyncMiddleware(this.editSaleInvoice)
|
||||||
);
|
);
|
||||||
router.delete(
|
router.delete(
|
||||||
@@ -257,6 +259,32 @@ export default class SaleInvoicesController {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the entries items that not sellable.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {Function} next
|
||||||
|
*/
|
||||||
|
static async validateNonSellableEntriesItems(req, res, next) {
|
||||||
|
const { Item } = req.models;
|
||||||
|
const saleInvoice = { ...req.body };
|
||||||
|
const itemsIds = saleInvoice.entries.map(e => e.item_id);
|
||||||
|
|
||||||
|
const sellableItems = await Item.query()
|
||||||
|
.where('sellable', true)
|
||||||
|
.whereIn('id', itemsIds);
|
||||||
|
|
||||||
|
const sellableItemsIds = sellableItems.map((item) => item.id);
|
||||||
|
const notSellableItems = difference(itemsIds, sellableItemsIds);
|
||||||
|
|
||||||
|
if (notSellableItems.length > 0) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'NOT.SELLABLE.ITEMS', code: 600 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sale invoice.
|
* Creates a new sale invoice.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default class ComputeItemCostJob {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await InventoryService.computeItemCost(startingDate, itemId, costMethod);
|
await InventoryService.computeItemCost(startingDate, itemId, costMethod);
|
||||||
Logger.log(`Compute item cost: ${job.attrs.data}`);
|
Logger.debug(`Compute item cost: ${job.attrs.data}`);
|
||||||
done();
|
done();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
|
import moment from 'moment';
|
||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
|
|
||||||
export default class InventoryCostLotTracker extends TenantModel {
|
export default class InventoryCostLotTracker extends TenantModel {
|
||||||
@@ -16,6 +17,27 @@ export default class InventoryCostLotTracker extends TenantModel {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
filterDateRange(query, startDate, endDate, type = 'day') {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||||
|
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
query.where('date', '>=', fromDate);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
query.where('date', '<=', toDate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
|
import moment from 'moment';
|
||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
|
|
||||||
export default class InventoryTransaction extends TenantModel {
|
export default class InventoryTransaction extends TenantModel {
|
||||||
@@ -16,6 +17,28 @@ export default class InventoryTransaction extends TenantModel {
|
|||||||
return ['createdAt', 'updatedAt'];
|
return ['createdAt', 'updatedAt'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
filterDateRange(query, startDate, endDate, type = 'day') {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||||
|
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
query.where('date', '>=', fromDate);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
query.where('date', '<=', toDate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,6 +21,22 @@ interface IInventoryCostEntity {
|
|||||||
income: number,
|
income: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface NonInventoryJEntries {
|
||||||
|
date: Date,
|
||||||
|
|
||||||
|
referenceType: string,
|
||||||
|
referenceId: number,
|
||||||
|
|
||||||
|
receivable: number,
|
||||||
|
payable: number,
|
||||||
|
|
||||||
|
incomeAccountId: number,
|
||||||
|
income: number,
|
||||||
|
|
||||||
|
costAccountId: number,
|
||||||
|
cost: number,
|
||||||
|
};
|
||||||
|
|
||||||
export default class JournalCommands{
|
export default class JournalCommands{
|
||||||
journal: JournalPoster;
|
journal: JournalPoster;
|
||||||
|
|
||||||
@@ -64,6 +80,50 @@ export default class JournalCommands{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async nonInventoryEntries(
|
||||||
|
transactions: NonInventoryJEntries[]
|
||||||
|
) {
|
||||||
|
const receivableAccount = { id: 10 };
|
||||||
|
const payableAccount = {id: 11};
|
||||||
|
|
||||||
|
transactions.forEach((trans: NonInventoryJEntries) => {
|
||||||
|
const commonEntry = {
|
||||||
|
date: trans.date,
|
||||||
|
referenceId: trans.referenceId,
|
||||||
|
referenceType: trans.referenceType,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch(trans.referenceType) {
|
||||||
|
case 'Bill':
|
||||||
|
const payableEntry: JournalEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
credit: trans.payable,
|
||||||
|
account: payableAccount.id,
|
||||||
|
});
|
||||||
|
const costEntry: JournalEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
});
|
||||||
|
this.journal.credit(payableEntry);
|
||||||
|
this.journal.debit(costEntry);
|
||||||
|
break;
|
||||||
|
case 'SaleInvoice':
|
||||||
|
const receivableEntry: JournalEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
debit: trans.receivable,
|
||||||
|
account: receivableAccount.id,
|
||||||
|
});
|
||||||
|
const saleIncomeEntry: JournalEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
credit: trans.income,
|
||||||
|
account: trans.incomeAccountId,
|
||||||
|
});
|
||||||
|
this.journal.debit(receivableEntry);
|
||||||
|
this.journal.credit(saleIncomeEntry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} referenceType -
|
* @param {string} referenceType -
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export default class InventoryService {
|
|||||||
entries: [],
|
entries: [],
|
||||||
deleteOld: boolean,
|
deleteOld: boolean,
|
||||||
) {
|
) {
|
||||||
const storedOpers: any = [];
|
|
||||||
const entriesItemsIds = entries.map((e: any) => e.item_id);
|
const entriesItemsIds = entries.map((e: any) => e.item_id);
|
||||||
const inventoryItems = await Item.tenant()
|
const inventoryItems = await Item.tenant()
|
||||||
.query()
|
.query()
|
||||||
@@ -79,15 +78,11 @@ export default class InventoryService {
|
|||||||
entry.transactionType,
|
entry.transactionType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const oper = InventoryTransaction.tenant().query().insert({
|
await InventoryTransaction.tenant().query().insert({
|
||||||
...entry,
|
...entry,
|
||||||
lotNumber: entry.lotNumber,
|
lotNumber: entry.lotNumber,
|
||||||
});
|
});
|
||||||
storedOpers.push(oper);
|
|
||||||
});
|
});
|
||||||
return Promise.all([
|
|
||||||
...storedOpers,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { omit, pick, chain } from 'lodash';
|
import { omit, pick, chain } from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
import {
|
import {
|
||||||
InventoryTransaction,
|
InventoryTransaction,
|
||||||
InventoryLotCostTracker,
|
InventoryLotCostTracker,
|
||||||
@@ -19,6 +20,13 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
itemId: number;
|
itemId: number;
|
||||||
costMethod: TCostMethod;
|
costMethod: TCostMethod;
|
||||||
itemsById: Map<number, any>;
|
itemsById: Map<number, any>;
|
||||||
|
inventoryINTrans: any;
|
||||||
|
inventoryByItem: any;
|
||||||
|
costLotsTransactions: IInventoryLotCost[];
|
||||||
|
inTransactions: any[];
|
||||||
|
outTransactions: IInventoryTransaction[];
|
||||||
|
revertInvoiceTrans: any[];
|
||||||
|
revertJEntriesTransactions: IInventoryTransaction[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
@@ -30,6 +38,19 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
this.startingDate = startingDate;
|
this.startingDate = startingDate;
|
||||||
this.itemId = itemId;
|
this.itemId = itemId;
|
||||||
this.costMethod = costMethod;
|
this.costMethod = costMethod;
|
||||||
|
|
||||||
|
// Collect cost lots transactions to insert them to the storage in bulk.
|
||||||
|
this.costLotsTransactions= [];
|
||||||
|
// Collect inventory transactions by item id.
|
||||||
|
this.inventoryByItem = {};
|
||||||
|
// Collection `IN` inventory tranaction by transaction id.
|
||||||
|
this.inventoryINTrans = {};
|
||||||
|
// Collects `IN` transactions.
|
||||||
|
this.inTransactions = [];
|
||||||
|
// Collects `OUT` transactions.
|
||||||
|
this.outTransactions = [];
|
||||||
|
// Collects journal entries reference id and type that should be reverted.
|
||||||
|
this.revertInvoiceTrans = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,48 +76,24 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
*/
|
*/
|
||||||
public async computeItemCost(): Promise<any> {
|
public async computeItemCost(): Promise<any> {
|
||||||
await this.revertInventoryLots(this.startingDate);
|
await this.revertInventoryLots(this.startingDate);
|
||||||
|
await this.fetchInvINTransactions();
|
||||||
|
await this.fetchInvOUTTransactions();
|
||||||
|
await this.fetchRevertInvJReferenceIds();
|
||||||
|
await this.fetchItemsMapped();
|
||||||
|
|
||||||
const afterInvTransactions: IInventoryTransaction[] =
|
this.trackingInventoryINLots(this.inTransactions);
|
||||||
await InventoryTransaction.tenant()
|
this.trackingInventoryOUTLots(this.outTransactions);
|
||||||
.query()
|
|
||||||
.where('date', '>=', this.startingDate)
|
|
||||||
.orderBy('date', 'ASC')
|
|
||||||
.orderBy('lot_number', 'ASC')
|
|
||||||
.where('item_id', this.itemId)
|
|
||||||
.withGraphFetched('item');
|
|
||||||
|
|
||||||
const availiableINLots: IInventoryLotCost[] =
|
|
||||||
await InventoryLotCostTracker.tenant()
|
|
||||||
.query()
|
|
||||||
.where('date', '<', this.startingDate)
|
|
||||||
.orderBy('date', 'ASC')
|
|
||||||
.orderBy('lot_number', 'ASC')
|
|
||||||
.where('item_id', this.itemId)
|
|
||||||
.where('direction', 'IN')
|
|
||||||
.whereNot('remaining', 0);
|
|
||||||
|
|
||||||
const merged = [
|
|
||||||
...availiableINLots.map((trans) => ({ lotTransId: trans.id, ...trans })),
|
|
||||||
...afterInvTransactions.map((trans) => ({ invTransId: trans.id, ...trans })),
|
|
||||||
];
|
|
||||||
const itemsIds = chain(merged).map(e => e.itemId).uniq().value();
|
|
||||||
|
|
||||||
const storedItems = await Item.tenant()
|
|
||||||
.query()
|
|
||||||
.where('type', 'inventory')
|
|
||||||
.whereIn('id', itemsIds);
|
|
||||||
|
|
||||||
this.itemsById = new Map(storedItems.map((item: any) => [item.id, item]));
|
|
||||||
|
|
||||||
// Re-tracking the inventory `IN` and `OUT` lots costs.
|
// Re-tracking the inventory `IN` and `OUT` lots costs.
|
||||||
const trackedInvLotsCosts = this.trackingInventoryLotsCost(merged);
|
const storedTrackedInvLotsOper = this.storeInventoryLotsCost(
|
||||||
const storedTrackedInvLotsOper = this.storeInventoryLotsCost(trackedInvLotsCosts);
|
this.costLotsTransactions,
|
||||||
|
);
|
||||||
|
|
||||||
// Remove and revert accounts balance journal entries from inventory transactions.
|
// Remove and revert accounts balance journal entries from inventory transactions.
|
||||||
const revertJEntriesOper = this.revertJournalEntries(afterInvTransactions);
|
const revertJEntriesOper = this.revertJournalEntries(this.revertJEntriesTransactions);
|
||||||
|
|
||||||
// Records the journal entries operation.
|
// Records the journal entries operation.
|
||||||
this.recordJournalEntries(trackedInvLotsCosts);
|
this.recordJournalEntries(this.costLotsTransactions);
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
storedTrackedInvLotsOper,
|
storedTrackedInvLotsOper,
|
||||||
@@ -110,6 +107,84 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetched inventory transactions that has date from the starting date and
|
||||||
|
* fetches availiable IN LOTs transactions that has remaining bigger than zero.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async fetchInvINTransactions() {
|
||||||
|
const commonBuilder = (builder: any) => {
|
||||||
|
builder.where('direction', 'IN');
|
||||||
|
builder.orderBy('date', 'ASC');
|
||||||
|
builder.where('item_id', this.itemId);
|
||||||
|
};
|
||||||
|
const afterInvTransactions: IInventoryTransaction[] =
|
||||||
|
await InventoryTransaction.tenant()
|
||||||
|
.query()
|
||||||
|
.modify('filterDateRange', this.startingDate)
|
||||||
|
.orderBy('lot_number', (this.costMethod === 'LIFO') ? 'DESC' : 'ASC')
|
||||||
|
.onBuild(commonBuilder)
|
||||||
|
.withGraphFetched('item');
|
||||||
|
|
||||||
|
const availiableINLots: IInventoryLotCost[] =
|
||||||
|
await InventoryLotCostTracker.tenant()
|
||||||
|
.query()
|
||||||
|
.modify('filterDateRange', null, this.startingDate)
|
||||||
|
.orderBy('date', 'ASC')
|
||||||
|
.orderBy('lot_number', 'ASC')
|
||||||
|
.onBuild(commonBuilder)
|
||||||
|
.whereNot('remaining', 0);
|
||||||
|
|
||||||
|
this.inTransactions = [
|
||||||
|
...availiableINLots.map((trans) => ({ lotTransId: trans.id, ...trans })),
|
||||||
|
...afterInvTransactions.map((trans) => ({ invTransId: trans.id, ...trans })),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches inventory OUT transactions that has date from the starting date.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async fetchInvOUTTransactions() {
|
||||||
|
const afterOUTTransactions: IInventoryTransaction[] =
|
||||||
|
await InventoryTransaction.tenant()
|
||||||
|
.query()
|
||||||
|
.modify('filterDateRange', this.startingDate)
|
||||||
|
.orderBy('date', 'ASC')
|
||||||
|
.orderBy('lot_number', 'ASC')
|
||||||
|
.where('item_id', this.itemId)
|
||||||
|
.where('direction', 'OUT')
|
||||||
|
.withGraphFetched('item');
|
||||||
|
|
||||||
|
this.outTransactions = [ ...afterOUTTransactions ];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchItemsMapped() {
|
||||||
|
const itemsIds = chain(this.inTransactions).map((e) => e.itemId).uniq().value();
|
||||||
|
const storedItems = await Item.tenant()
|
||||||
|
.query()
|
||||||
|
.where('type', 'inventory')
|
||||||
|
.whereIn('id', itemsIds);
|
||||||
|
|
||||||
|
this.itemsById = new Map(storedItems.map((item: any) => [item.id, item]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the inventory transactions that should revert its journal entries.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async fetchRevertInvJReferenceIds() {
|
||||||
|
const revertJEntriesTransactions: IInventoryTransaction[] =
|
||||||
|
await InventoryTransaction.tenant()
|
||||||
|
.query()
|
||||||
|
.select(['transactionId', 'transactionType'])
|
||||||
|
.modify('filterDateRange', this.startingDate)
|
||||||
|
.where('direction', 'OUT')
|
||||||
|
.where('item_id', this.itemId);
|
||||||
|
|
||||||
|
this.revertJEntriesTransactions = revertJEntriesTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revert the inventory lots to the given date by removing the inventory lots
|
* Revert the inventory lots to the given date by removing the inventory lots
|
||||||
* transactions after the given date and increment the remaining that
|
* transactions after the given date and increment the remaining that
|
||||||
@@ -121,14 +196,14 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
const asyncOpers: any[] = [];
|
const asyncOpers: any[] = [];
|
||||||
const inventoryLotsTrans = await InventoryLotCostTracker.tenant()
|
const inventoryLotsTrans = await InventoryLotCostTracker.tenant()
|
||||||
.query()
|
.query()
|
||||||
|
.modify('filterDateRange', this.startingDate)
|
||||||
.orderBy('date', 'DESC')
|
.orderBy('date', 'DESC')
|
||||||
.where('item_id', this.itemId)
|
.where('item_id', this.itemId)
|
||||||
.where('date', '>=', startingDate)
|
|
||||||
.where('direction', 'OUT');
|
.where('direction', 'OUT');
|
||||||
|
|
||||||
const deleteInvLotsTrans = InventoryLotCostTracker.tenant()
|
const deleteInvLotsTrans = InventoryLotCostTracker.tenant()
|
||||||
.query()
|
.query()
|
||||||
.where('date', '>=', startingDate)
|
.modify('filterDateRange', this.startingDate)
|
||||||
.where('item_id', this.itemId)
|
.where('item_id', this.itemId)
|
||||||
.delete();
|
.delete();
|
||||||
|
|
||||||
@@ -151,13 +226,10 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
* @param {} inventoryLots
|
* @param {} inventoryLots
|
||||||
*/
|
*/
|
||||||
async revertJournalEntries(
|
async revertJournalEntries(
|
||||||
inventoryLots: IInventoryLotCost[],
|
transactions: IInventoryLotCost[],
|
||||||
) {
|
) {
|
||||||
const invoiceTransactions = inventoryLots
|
|
||||||
.filter(e => e.transactionType === 'SaleInvoice');
|
|
||||||
|
|
||||||
return this.journalCommands
|
return this.journalCommands
|
||||||
.revertEntriesFromInventoryTransactions(invoiceTransactions);
|
.revertEntriesFromInventoryTransactions(transactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,23 +309,17 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracking the given inventory transactions to lots costs transactions.
|
* Tracking inventory `IN` lots transactions.
|
||||||
* @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions.
|
* @public
|
||||||
* @return {IInventoryLotCost[]}
|
* @param {IInventoryTransaction[]} inventoryTransactions -
|
||||||
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
public trackingInventoryLotsCost(
|
public trackingInventoryINLots(
|
||||||
inventoryTransactions: IInventoryTransaction[],
|
inventoryTransactions: IInventoryTransaction[],
|
||||||
) : IInventoryLotCost {
|
) {
|
||||||
// Collect cost lots transactions to insert them to the storage in bulk.
|
|
||||||
const costLotsTransactions: IInventoryLotCost[] = [];
|
|
||||||
// Collect inventory transactions by item id.
|
|
||||||
const inventoryByItem: any = {};
|
|
||||||
// Collection `IN` inventory tranaction by transaction id.
|
|
||||||
const inventoryINTrans: any = {};
|
|
||||||
|
|
||||||
inventoryTransactions.forEach((transaction: IInventoryTransaction) => {
|
inventoryTransactions.forEach((transaction: IInventoryTransaction) => {
|
||||||
const { itemId, id } = transaction;
|
const { itemId, id } = transaction;
|
||||||
(inventoryByItem[itemId] || (inventoryByItem[itemId] = []));
|
(this.inventoryByItem[itemId] || (this.inventoryByItem[itemId] = []));
|
||||||
|
|
||||||
const commonLotTransaction: IInventoryLotCost = {
|
const commonLotTransaction: IInventoryLotCost = {
|
||||||
...pick(transaction, [
|
...pick(transaction, [
|
||||||
@@ -261,62 +327,91 @@ export default class InventoryCostLotTracker implements IInventoryCostMethod {
|
|||||||
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
|
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
// Record inventory `IN` cost lot transaction.
|
this.inventoryByItem[itemId].push(id);
|
||||||
if (transaction.direction === 'IN') {
|
this.inventoryINTrans[id] = {
|
||||||
inventoryByItem[itemId].push(id);
|
...commonLotTransaction,
|
||||||
inventoryINTrans[id] = {
|
decrement: 0,
|
||||||
...commonLotTransaction,
|
remaining: commonLotTransaction.remaining || commonLotTransaction.quantity,
|
||||||
decrement: 0,
|
};
|
||||||
remaining: commonLotTransaction.remaining || commonLotTransaction.quantity,
|
this.costLotsTransactions.push(this.inventoryINTrans[id]);
|
||||||
};
|
|
||||||
costLotsTransactions.push(inventoryINTrans[id]);
|
|
||||||
|
|
||||||
// Record inventory 'OUT' cost lots from 'IN' transactions.
|
|
||||||
} else if (transaction.direction === 'OUT') {
|
|
||||||
let invRemaining = transaction.quantity;
|
|
||||||
const idsShouldDel: number[] = [];
|
|
||||||
|
|
||||||
inventoryByItem?.[itemId]?.some((
|
|
||||||
_invTransactionId: number,
|
|
||||||
) => {
|
|
||||||
const _invINTransaction = inventoryINTrans[_invTransactionId];
|
|
||||||
if (invRemaining <= 0) { return true; }
|
|
||||||
|
|
||||||
// Detarmines the 'OUT' lot tranasctions whether bigger than 'IN' remaining transaction.
|
|
||||||
const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0;
|
|
||||||
const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining;
|
|
||||||
const maxDecrement = Math.min(decrement, invRemaining);
|
|
||||||
|
|
||||||
_invINTransaction.decrement += maxDecrement;
|
|
||||||
_invINTransaction.remaining = Math.max(
|
|
||||||
_invINTransaction.remaining - maxDecrement,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
invRemaining = Math.max(invRemaining - maxDecrement, 0);
|
|
||||||
|
|
||||||
costLotsTransactions.push({
|
|
||||||
...commonLotTransaction,
|
|
||||||
quantity: maxDecrement,
|
|
||||||
lotNumber: _invINTransaction.lotNumber,
|
|
||||||
});
|
|
||||||
// Pop the 'IN' lots that has zero remaining.
|
|
||||||
if (_invINTransaction.remaining === 0) {
|
|
||||||
idsShouldDel.push(_invTransactionId);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (invRemaining > 0) {
|
|
||||||
costLotsTransactions.push({
|
|
||||||
...commonLotTransaction,
|
|
||||||
quantity: invRemaining,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Remove the IN transactions that has zero remaining amount.
|
|
||||||
inventoryByItem[itemId] = inventoryByItem?.[itemId]
|
|
||||||
?.filter((transId: number) => idsShouldDel.indexOf(transId) === -1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return costLotsTransactions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracking inventory `OUT` lots transactions.
|
||||||
|
* @public
|
||||||
|
* @param {IInventoryTransaction[]} inventoryTransactions -
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
public trackingInventoryOUTLots(
|
||||||
|
inventoryTransactions: IInventoryTransaction[],
|
||||||
|
) {
|
||||||
|
inventoryTransactions.forEach((transaction: IInventoryTransaction) => {
|
||||||
|
const { itemId, id } = transaction;
|
||||||
|
(this.inventoryByItem[itemId] || (this.inventoryByItem[itemId] = []));
|
||||||
|
|
||||||
|
const commonLotTransaction: IInventoryLotCost = {
|
||||||
|
...pick(transaction, [
|
||||||
|
'date', 'rate', 'itemId', 'quantity', 'invTransId', 'lotTransId',
|
||||||
|
'direction', 'transactionType', 'transactionId', 'lotNumber', 'remaining'
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
let invRemaining = transaction.quantity;
|
||||||
|
const idsShouldDel: number[] = [];
|
||||||
|
|
||||||
|
this.inventoryByItem?.[itemId]?.some((_invTransactionId: number) => {
|
||||||
|
const _invINTransaction = this.inventoryINTrans[_invTransactionId];
|
||||||
|
|
||||||
|
// Can't continue if the IN transaction remaining equals zero.
|
||||||
|
if (invRemaining <= 0) { return true; }
|
||||||
|
|
||||||
|
// Can't continue if the IN transaction date is after the current transaction date.
|
||||||
|
if (moment(_invINTransaction.date).isAfter(transaction.date)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Detarmines the 'OUT' lot tranasctions whether bigger than 'IN' remaining transaction.
|
||||||
|
const biggerThanRemaining = (_invINTransaction.remaining - transaction.quantity) > 0;
|
||||||
|
const decrement = (biggerThanRemaining) ? transaction.quantity : _invINTransaction.remaining;
|
||||||
|
const maxDecrement = Math.min(decrement, invRemaining);
|
||||||
|
|
||||||
|
_invINTransaction.decrement += maxDecrement;
|
||||||
|
_invINTransaction.remaining = Math.max(
|
||||||
|
_invINTransaction.remaining - maxDecrement,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
invRemaining = Math.max(invRemaining - maxDecrement, 0);
|
||||||
|
|
||||||
|
this.costLotsTransactions.push({
|
||||||
|
...commonLotTransaction,
|
||||||
|
quantity: maxDecrement,
|
||||||
|
lotNumber: _invINTransaction.lotNumber,
|
||||||
|
});
|
||||||
|
// Pop the 'IN' lots that has zero remaining.
|
||||||
|
if (_invINTransaction.remaining === 0) {
|
||||||
|
idsShouldDel.push(_invTransactionId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (invRemaining > 0) {
|
||||||
|
this.costLotsTransactions.push({
|
||||||
|
...commonLotTransaction,
|
||||||
|
quantity: invRemaining,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.removeInventoryItems(itemId, idsShouldDel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inventory transactions for specific item id.
|
||||||
|
* @private
|
||||||
|
* @param {number} itemId
|
||||||
|
* @param {number[]} idsShouldDel
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
private removeInventoryItems(itemId: number, idsShouldDel: number[]) {
|
||||||
|
// Remove the IN transactions that has zero remaining amount.
|
||||||
|
this.inventoryByItem[itemId] = this.inventoryByItem?.[itemId]
|
||||||
|
?.filter((transId: number) => idsShouldDel.indexOf(transId) === -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,12 +12,34 @@ import HasItemsEntries from '@/services/Sales/HasItemsEntries';
|
|||||||
import CustomerRepository from '@/repositories/CustomerRepository';
|
import CustomerRepository from '@/repositories/CustomerRepository';
|
||||||
import InventoryService from '@/services/Inventory/Inventory';
|
import InventoryService from '@/services/Inventory/Inventory';
|
||||||
import { formatDateFields } from '@/utils';
|
import { formatDateFields } from '@/utils';
|
||||||
|
import { Item } from '../../models';
|
||||||
|
import JournalCommands from '../Accounting/JournalCommands';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sales invoices service
|
* Sales invoices service
|
||||||
* @service
|
* @service
|
||||||
*/
|
*/
|
||||||
export default class SaleInvoicesService {
|
export default class SaleInvoicesService {
|
||||||
|
|
||||||
|
static filterNonInventoryEntries(entries: [], items: []) {
|
||||||
|
const nonInventoryItems = items.filter((item: any) => item.type !== 'inventory');
|
||||||
|
const nonInventoryItemsIds = nonInventoryItems.map((i: any) => i.id);
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.filter((entry: any) => (
|
||||||
|
(nonInventoryItemsIds.indexOf(entry.item_id)) !== -1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
static filterInventoryEntries(entries: [], items: []) {
|
||||||
|
const inventoryItems = items.filter((item: any) => item.type === 'inventory');
|
||||||
|
const inventoryItemsIds = inventoryItems.map((i: any) => i.id);
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.filter((entry: any) => (
|
||||||
|
(inventoryItemsIds.indexOf(entry.item_id)) !== -1
|
||||||
|
));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Creates a new sale invoices and store it to the storage
|
* Creates a new sale invoices and store it to the storage
|
||||||
* with associated to entries and journal transactions.
|
* with associated to entries and journal transactions.
|
||||||
@@ -60,19 +82,65 @@ export default class SaleInvoicesService {
|
|||||||
const recordInventoryTransOpers = this.recordInventoryTranscactions(
|
const recordInventoryTransOpers = this.recordInventoryTranscactions(
|
||||||
saleInvoice, storedInvoice.id
|
saleInvoice, storedInvoice.id
|
||||||
);
|
);
|
||||||
|
// Records the non-inventory transactions of the entries items.
|
||||||
|
const recordNonInventoryJEntries = this.recordNonInventoryEntries(
|
||||||
|
saleInvoice, storedInvoice.id,
|
||||||
|
);
|
||||||
// Await all async operations.
|
// Await all async operations.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...opers,
|
...opers,
|
||||||
incrementOper,
|
incrementOper,
|
||||||
|
recordNonInventoryJEntries,
|
||||||
recordInventoryTransOpers,
|
recordInventoryTransOpers,
|
||||||
]);
|
]);
|
||||||
// Schedule sale invoice re-compute based on the item cost
|
// Schedule sale invoice re-compute based on the item cost
|
||||||
// method and starting date.
|
// method and starting date.
|
||||||
await this.scheduleComputeItemsCost(saleInvoice);
|
// await this.scheduleComputeItemsCost(saleInvoice);
|
||||||
|
|
||||||
return storedInvoice;
|
return storedInvoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the journal entries for non-inventory entries.
|
||||||
|
* @param {SaleInvoice} saleInvoice
|
||||||
|
*/
|
||||||
|
static async recordNonInventoryEntries(saleInvoice: any, saleInvoiceId: number) {
|
||||||
|
const saleInvoiceItems = saleInvoice.entries.map((entry: any) => entry.item_id);
|
||||||
|
|
||||||
|
// Retrieves items data to detarmines whether the item type.
|
||||||
|
const itemsMeta = await Item.tenant().query().whereIn('id', saleInvoiceItems);
|
||||||
|
const storedItemsMap = new Map(itemsMeta.map((item) => [item.id, item]));
|
||||||
|
|
||||||
|
// Filters the non-inventory and inventory entries based on the item type.
|
||||||
|
const nonInventoryEntries: any[] = this.filterNonInventoryEntries(saleInvoice.entries, itemsMeta);
|
||||||
|
|
||||||
|
const transactions: any = [];
|
||||||
|
const common = {
|
||||||
|
referenceType: 'SaleInvoice',
|
||||||
|
referenceId: saleInvoiceId,
|
||||||
|
date: saleInvoice.invoice_date,
|
||||||
|
};
|
||||||
|
nonInventoryEntries.forEach((entry) => {
|
||||||
|
const item = storedItemsMap.get(entry.item_id);
|
||||||
|
|
||||||
|
transactions.push({
|
||||||
|
...common,
|
||||||
|
income: entry.amount,
|
||||||
|
incomeAccountId: item.incomeAccountId,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const accountsDepGraph = await Account.tenant().depGraph().query();
|
||||||
|
const journal = new JournalPoster(accountsDepGraph);
|
||||||
|
const journalCommands = new JournalCommands(journal);
|
||||||
|
|
||||||
|
journalCommands.nonInventoryEntries(transactions);
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
journal.deleteEntries(),
|
||||||
|
journal.saveEntries(),
|
||||||
|
journal.saveBalance(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit the given sale invoice.
|
* Edit the given sale invoice.
|
||||||
* @async
|
* @async
|
||||||
|
|||||||
Reference in New Issue
Block a user