503 lines
17 KiB
JavaScript
503 lines
17 KiB
JavaScript
import Icon, { UploadOutlined } from '@ant-design/icons';
|
|
import { useApolloClient } from '@apollo/client';
|
|
import { useSplitTreatments } from '@splitsoftware/splitio-react';
|
|
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from 'antd';
|
|
import React, { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { MdOpenInNew } from 'react-icons/md';
|
|
import { connect } from 'react-redux';
|
|
import { Link } from 'react-router-dom';
|
|
import { createStructuredSelector } from 'reselect';
|
|
import { CHECK_BILL_INVOICE_NUMBER } from '../../graphql/bills.queries';
|
|
import { selectBodyshop } from '../../redux/user/user.selectors';
|
|
import dayjs from '../../utils/day';
|
|
import InstanceRenderManager from '../../utils/instanceRenderMgr';
|
|
import AlertComponent from '../alert/alert.component';
|
|
import BillFormLinesExtended from '../bill-form-lines-extended/bill-form-lines-extended.component';
|
|
import FormDatePicker from '../form-date-picker/form-date-picker.component';
|
|
import FormFieldsChanged from '../form-fields-changed-alert/form-fields-changed-alert.component';
|
|
import CurrencyInput from '../form-items-formatted/currency-form-item.component';
|
|
import JobSearchSelect from '../job-search-select/job-search-select.component';
|
|
import LayoutFormRow from '../layout-form-row/layout-form-row.component';
|
|
import VendorSearchSelect from '../vendor-search-select/vendor-search-select.component';
|
|
import BillFormLines from './bill-form.lines.component';
|
|
import { CalculateBillTotal } from './bill-form.totals.utility';
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
bodyshop: selectBodyshop,
|
|
});
|
|
const mapDispatchToProps = (dispatch) => ({});
|
|
|
|
export function BillFormComponent({
|
|
bodyshop,
|
|
disabled,
|
|
form,
|
|
vendorAutoCompleteOptions,
|
|
lineData,
|
|
responsibilityCenters,
|
|
loadLines,
|
|
billEdit,
|
|
disableInvNumber,
|
|
job,
|
|
loadOutstandingReturns,
|
|
loadInventory,
|
|
preferredMake,
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const client = useApolloClient();
|
|
const [discount, setDiscount] = useState(0);
|
|
|
|
const {
|
|
treatments: { Extended_Bill_Posting, ClosingPeriod },
|
|
} = useSplitTreatments({
|
|
attributes: {},
|
|
names: ['Extended_Bill_Posting', 'ClosingPeriod'],
|
|
splitKey: bodyshop.imexshopid,
|
|
});
|
|
|
|
const handleVendorSelect = (props, opt) => {
|
|
setDiscount(opt.discount);
|
|
|
|
opt &&
|
|
!billEdit &&
|
|
loadOutstandingReturns({
|
|
variables: {
|
|
jobId: form.getFieldValue('jobid'),
|
|
vendorId: opt.value,
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleFederalTaxExemptSwitchToggle = (checked) => {
|
|
// Early gate
|
|
if (!checked) return;
|
|
const values = form.getFieldsValue('billlines');
|
|
// Gate bill lines
|
|
if (!values?.billlines?.length) return;
|
|
|
|
const billlines = values.billlines.map((b) => {
|
|
b.applicable_taxes.federal = false;
|
|
return b;
|
|
});
|
|
form.setFieldsValue({ billlines });
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (job) form.validateFields(['is_credit_memo']);
|
|
}, [job, form]);
|
|
|
|
useEffect(() => {
|
|
const vendorId = form.getFieldValue('vendorid');
|
|
if (vendorId && vendorAutoCompleteOptions) {
|
|
const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
|
|
if (matchingVendors.length === 1) {
|
|
setDiscount(matchingVendors[0].discount);
|
|
}
|
|
}
|
|
const jobId = form.getFieldValue('jobid');
|
|
if (jobId) {
|
|
loadLines({ variables: { id: jobId } });
|
|
if (form.getFieldValue('is_credit_memo') && vendorId && !billEdit) {
|
|
loadOutstandingReturns({
|
|
variables: {
|
|
jobId: jobId,
|
|
vendorId: vendorId,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
|
|
loadInventory();
|
|
}
|
|
}, [
|
|
form,
|
|
billEdit,
|
|
loadOutstandingReturns,
|
|
loadInventory,
|
|
setDiscount,
|
|
vendorAutoCompleteOptions,
|
|
loadLines,
|
|
bodyshop.inhousevendorid,
|
|
]);
|
|
|
|
return (
|
|
<div>
|
|
<FormFieldsChanged form={form} />
|
|
<Form.Item style={{ display: 'none' }} name="isinhouse" valuePropName="checked">
|
|
<Switch />
|
|
</Form.Item>
|
|
<LayoutFormRow grow>
|
|
<Form.Item
|
|
name="jobid"
|
|
label={t('bills.fields.ro_number')}
|
|
rules={[
|
|
{
|
|
required: true,
|
|
//message: t("general.validation.required"),
|
|
},
|
|
]}
|
|
>
|
|
<JobSearchSelect
|
|
disabled={billEdit || disabled}
|
|
convertedOnly
|
|
notExported={false}
|
|
onBlur={() => {
|
|
if (
|
|
form.getFieldValue('jobid') !== null &&
|
|
form.getFieldValue('jobid') !== undefined
|
|
) {
|
|
loadLines({ variables: { id: form.getFieldValue('jobid') } });
|
|
if (
|
|
form.getFieldValue('vendorid') !== null &&
|
|
form.getFieldValue('vendorid') !== undefined
|
|
) {
|
|
loadOutstandingReturns({
|
|
variables: {
|
|
jobId: form.getFieldValue('jobid'),
|
|
vendorId: form.getFieldValue('vendorid'),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('bills.fields.vendor')}
|
|
name="vendorid"
|
|
// style={{ display: billEdit ? "none" : null }}
|
|
rules={[
|
|
{
|
|
required: true,
|
|
//message: t("general.validation.required"),
|
|
},
|
|
({ getFieldValue }) => ({
|
|
validator(rule, value) {
|
|
if (value && !getFieldValue(['isinhouse']) && value === bodyshop.inhousevendorid) {
|
|
return Promise.reject(t('bills.validation.manualinhouse'));
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
}),
|
|
]}
|
|
>
|
|
<VendorSearchSelect
|
|
disabled={disabled}
|
|
options={vendorAutoCompleteOptions}
|
|
preferredMake={preferredMake}
|
|
onSelect={handleVendorSelect}
|
|
/>
|
|
</Form.Item>
|
|
</LayoutFormRow>
|
|
{job &&
|
|
job.ious &&
|
|
job.ious.length > 0 &&
|
|
job.ious.map((iou) => (
|
|
<Alert
|
|
key={iou.id}
|
|
type="warning"
|
|
message={
|
|
<Space>
|
|
{t('bills.labels.iouexists')}
|
|
<Link
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
to={`/manage/jobs/${iou.id}?tab=repairdata`}
|
|
>
|
|
<Space>
|
|
{iou.ro_number}
|
|
<Icon component={MdOpenInNew} />
|
|
</Space>
|
|
</Link>
|
|
</Space>
|
|
}
|
|
/>
|
|
))}
|
|
<LayoutFormRow>
|
|
<Form.Item
|
|
label={t('bills.fields.invoice_number')}
|
|
name="invoice_number"
|
|
validateTrigger="onBlur"
|
|
hasFeedback
|
|
rules={[
|
|
{
|
|
required: true,
|
|
//message: t("general.validation.required"),
|
|
},
|
|
({ getFieldValue }) => ({
|
|
async validator(rule, value) {
|
|
const vendorid = getFieldValue('vendorid');
|
|
if (vendorid && value) {
|
|
const response = await client.query({
|
|
query: CHECK_BILL_INVOICE_NUMBER,
|
|
variables: {
|
|
invoice_number: value,
|
|
vendorid: vendorid,
|
|
},
|
|
});
|
|
|
|
if (response.data.bills_aggregate.aggregate.count === 0) {
|
|
return Promise.resolve();
|
|
} else if (
|
|
response.data.bills_aggregate.nodes.length === 1 &&
|
|
response.data.bills_aggregate.nodes[0].id === form.getFieldValue('id')
|
|
) {
|
|
return Promise.resolve();
|
|
}
|
|
return Promise.reject(t('bills.validation.unique_invoice_number'));
|
|
} else {
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
}),
|
|
]}
|
|
>
|
|
<Input disabled={disabled || disableInvNumber} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('bills.fields.date')}
|
|
name="date"
|
|
rules={[
|
|
{
|
|
required: true,
|
|
//message: t("general.validation.required"),
|
|
},
|
|
({ getFieldValue }) => ({
|
|
validator(rule, value) {
|
|
if (ClosingPeriod.treatment === 'on' && bodyshop.accountingconfig.ClosingPeriod) {
|
|
if (
|
|
dayjs(value)
|
|
.startOf('day')
|
|
.isSameOrAfter(
|
|
dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf('day')
|
|
) &&
|
|
dayjs(value)
|
|
.startOf('day')
|
|
.isSameOrBefore(
|
|
dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf('day')
|
|
)
|
|
) {
|
|
return Promise.resolve();
|
|
} else {
|
|
return Promise.reject(t('bills.validation.closingperiod'));
|
|
}
|
|
} else {
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
}),
|
|
]}
|
|
>
|
|
<FormDatePicker disabled={disabled} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('bills.fields.is_credit_memo')}
|
|
name="is_credit_memo"
|
|
valuePropName="checked"
|
|
rules={[
|
|
({ getFieldValue }) => ({
|
|
validator(rule, value) {
|
|
if (value === true && getFieldValue('jobid') && getFieldValue('vendorid')) {
|
|
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
|
|
// loadOutstandingReturns({
|
|
// variables: {
|
|
// jobId: form.getFieldValue("jobid"),
|
|
// vendorId: form.getFieldValue("vendorid"),
|
|
// },
|
|
// });
|
|
}
|
|
|
|
if (
|
|
!bodyshop.bill_allow_post_to_closed &&
|
|
job &&
|
|
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
|
|
job.status === bodyshop.md_ro_statuses.default_exported ||
|
|
job.status === bodyshop.md_ro_statuses.default_void) &&
|
|
(value === false || !value)
|
|
) {
|
|
return Promise.reject(t('bills.labels.onlycmforinvoiced'));
|
|
}
|
|
|
|
return Promise.resolve();
|
|
},
|
|
}),
|
|
]}
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('bills.fields.total')}
|
|
name="total"
|
|
rules={[
|
|
{
|
|
required: true,
|
|
//message: t("general.validation.required"),
|
|
},
|
|
]}
|
|
>
|
|
<CurrencyInput min={0} disabled={disabled} />
|
|
</Form.Item>
|
|
{!billEdit && (
|
|
<Form.Item label={t('bills.fields.allpartslocation')} name="location">
|
|
<Select style={{ width: '10rem' }} disabled={disabled} allowClear>
|
|
{bodyshop.md_parts_locations.map((loc, idx) => (
|
|
<Select.Option key={idx} value={loc}>
|
|
{loc}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
)}
|
|
</LayoutFormRow>
|
|
<LayoutFormRow>
|
|
{InstanceRenderManager({
|
|
imex: (
|
|
<Form.Item span={3} label={t('bills.fields.federal_tax_rate')} name="federal_tax_rate">
|
|
<CurrencyInput min={0} disabled={disabled} />
|
|
</Form.Item>
|
|
),
|
|
})}
|
|
<Form.Item span={3} label={t('bills.fields.state_tax_rate')} name="state_tax_rate">
|
|
<CurrencyInput min={0} disabled={disabled} />
|
|
</Form.Item>
|
|
{InstanceRenderManager({
|
|
imex: (
|
|
<>
|
|
<Form.Item span={3} label={t('bills.fields.local_tax_rate')} name="local_tax_rate">
|
|
<CurrencyInput min={0} />
|
|
</Form.Item>
|
|
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
|
<Form.Item
|
|
span={2}
|
|
label={t('bills.labels.federal_tax_exempt')}
|
|
name="federal_tax_exempt"
|
|
>
|
|
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
|
|
</Form.Item>
|
|
) : null}
|
|
</>
|
|
),
|
|
})}
|
|
<Form.Item shouldUpdate span={13}>
|
|
{() => {
|
|
const values = form.getFieldsValue([
|
|
'billlines',
|
|
'total',
|
|
'federal_tax_rate',
|
|
'state_tax_rate',
|
|
'local_tax_rate',
|
|
]);
|
|
let totals;
|
|
if (!!values.total && !!values.billlines && values.billlines.length > 0)
|
|
totals = CalculateBillTotal(values);
|
|
if (!!totals)
|
|
return (
|
|
<div align="right">
|
|
<Space wrap>
|
|
<Statistic
|
|
title={t('bills.labels.subtotal')}
|
|
value={totals.subtotal.toFormat()}
|
|
precision={2}
|
|
/>
|
|
{InstanceRenderManager({
|
|
imex: (
|
|
<Statistic
|
|
title={t('bills.labels.federal_tax')}
|
|
value={totals.federalTax.toFormat()}
|
|
precision={2}
|
|
/>
|
|
),
|
|
})}
|
|
<Statistic
|
|
title={t('bills.labels.state_tax')}
|
|
value={totals.stateTax.toFormat()}
|
|
precision={2}
|
|
/>
|
|
{InstanceRenderManager({
|
|
imex: (
|
|
<Statistic
|
|
title={t('bills.labels.local_tax')}
|
|
value={totals.localTax.toFormat()}
|
|
precision={2}
|
|
/>
|
|
),
|
|
})}
|
|
<Statistic
|
|
title={t('bills.labels.entered_total')}
|
|
value={totals.enteredTotal.toFormat()}
|
|
precision={2}
|
|
/>
|
|
<Statistic
|
|
title={t('bills.labels.bill_total')}
|
|
value={totals.invoiceTotal.toFormat()}
|
|
precision={2}
|
|
/>
|
|
<Statistic
|
|
title={t('bills.labels.discrepancy')}
|
|
valueStyle={{
|
|
color: totals.discrepancy.getAmount() === 0 ? 'green' : 'red',
|
|
}}
|
|
value={totals.discrepancy.toFormat()}
|
|
precision={2}
|
|
/>
|
|
</Space>
|
|
{form.getFieldValue('is_credit_memo') ? (
|
|
<AlertComponent type="warning" message={t('bills.labels.enteringcreditmemo')} />
|
|
) : null}
|
|
</div>
|
|
);
|
|
return null;
|
|
}}
|
|
</Form.Item>
|
|
</LayoutFormRow>
|
|
<Divider orientation="left">{t('bills.labels.bill_lines')}</Divider>
|
|
|
|
{Extended_Bill_Posting.treatment === 'on' ? (
|
|
<BillFormLinesExtended
|
|
lineData={lineData}
|
|
discount={discount}
|
|
form={form}
|
|
responsibilityCenters={responsibilityCenters}
|
|
disabled={disabled}
|
|
/>
|
|
) : (
|
|
<BillFormLines
|
|
lineData={lineData}
|
|
discount={discount}
|
|
form={form}
|
|
responsibilityCenters={responsibilityCenters}
|
|
disabled={disabled}
|
|
billEdit={billEdit}
|
|
/>
|
|
)}
|
|
<Divider orientation="left" style={{ display: billEdit ? 'none' : null }}>
|
|
{t('documents.labels.upload')}
|
|
</Divider>
|
|
<Form.Item
|
|
name="upload"
|
|
label="Upload"
|
|
style={{ display: billEdit ? 'none' : null }}
|
|
valuePropName="fileList"
|
|
getValueFromEvent={(e) => {
|
|
if (Array.isArray(e)) {
|
|
return e;
|
|
}
|
|
return e && e.fileList;
|
|
}}
|
|
>
|
|
<Upload.Dragger multiple={true} name="logo" beforeUpload={() => false} listType="picture">
|
|
<>
|
|
<p className="ant-upload-drag-icon">
|
|
<UploadOutlined />
|
|
</p>
|
|
<p className="ant-upload-text">Click or drag files to this area to upload.</p>
|
|
</>
|
|
</Upload.Dragger>
|
|
</Form.Item>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);
|