Merge branch 'master-AIO' into feature/IO-3515-ocr-bill-posting

This commit is contained in:
Patrick Fic
2026-02-18 10:08:25 -08:00
208 changed files with 8558 additions and 2872 deletions

View File

@@ -373,9 +373,11 @@ export function BillFormComponent({
"local_tax_rate"
]);
let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0)
if (!!values.total && !!values.billlines && values.billlines.length > 0) {
totals = CalculateBillTotal(values);
if (totals)
}
if (totals) {
return (
// TODO: Align is not correct
// eslint-disable-next-line react/no-unknown-property
@@ -414,7 +416,7 @@ export function BillFormComponent({
<Statistic
title={t("bills.labels.discrepancy")}
styles={{
value: {
content: {
color: totals.discrepancy.getAmount() === 0 ? "green" : "red"
}
}}
@@ -427,6 +429,7 @@ export function BillFormComponent({
) : null}
</div>
);
}
return null;
}}
</Form.Item>

View File

@@ -1,6 +1,7 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -32,14 +33,14 @@ export function BillEnterModalLinesComponent({
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const firstFieldRefs = useRef({});
// Keep input row heights consistent with the rest of the table controls.
const CONTROL_HEIGHT = 32;
const normalizeDiscount = (d) => {
const n = Number(d);
if (!Number.isFinite(n) || n <= 0) return 0;
return n > 1 ? n / 100 : n; // supports 15 or 0.15
return n > 1 ? n / 100 : n;
};
const round2 = (v) => Math.round((v + Number.EPSILON) * 100) / 100;
@@ -79,7 +80,6 @@ export function BillEnterModalLinesComponent({
return NaN;
};
// safe per-field setter (supports AntD 6+ setFieldValue, falls back to setFieldsValue)
const setLineField = (index, field, value) => {
if (typeof form.setFieldValue === "function") {
form.setFieldValue(["billlines", index, field], value);
@@ -92,6 +92,7 @@ export function BillEnterModalLinesComponent({
});
};
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
const autofillActualCost = (index) => {
Promise.resolve().then(() => {
const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]);
@@ -115,7 +116,6 @@ export function BillEnterModalLinesComponent({
};
const getIndicatorShellStyles = (statusColor) => {
// bring back the “colored shell” feel around the $ indicator while keeping row height stable
if (isDarkMode) {
if (statusColor === "green")
return { borderColor: "rgba(82, 196, 26, 0.75)", background: "rgba(82, 196, 26, 0.10)" };
@@ -145,7 +145,7 @@ export function BillEnterModalLinesComponent({
editable: true,
minWidth: "10rem",
formItemProps: (field) => ({
key: `${field.index}joblinename`,
key: `${field.name}joblinename`,
name: [field.name, "joblineid"],
label: t("billlines.fields.jobline"),
rules: [{ required: true }]
@@ -157,6 +157,9 @@ export function BillEnterModalLinesComponent({
),
formInput: (record, index) => (
<BillLineSearchSelect
ref={(el) => {
firstFieldRefs.current[index] = el;
}}
disabled={disabled}
options={lineData}
style={{
@@ -167,10 +170,9 @@ export function BillEnterModalLinesComponent({
}}
allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => {
const d = normalizeDiscount(discount);
const retail = Number(opt.cost);
const computedActual = Number.isFinite(retail) ? round2(retail * (1 - d)) : null;
// IMPORTANT:
// Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
// from Retail (actual_price) -> Actual Cost (actual_cost).
setFieldsValue({
billlines: (getFieldValue("billlines") || []).map((item, idx) => {
if (idx !== index) return item;
@@ -181,7 +183,7 @@ export function BillEnterModalLinesComponent({
quantity: opt.part_qty || 1,
actual_price: opt.cost,
original_actual_price: opt.cost,
actual_cost: isBlank(item.actual_cost) ? computedActual : item.actual_cost,
// actual_cost intentionally untouched here
cost_center: opt.part_type
? bodyshopHasDmsKey(bodyshop)
? opt.part_type !== "PAE"
@@ -203,12 +205,12 @@ export function BillEnterModalLinesComponent({
editable: true,
minWidth: "10rem",
formItemProps: (field) => ({
key: `${field.index}line_desc`,
key: `${field.name}line_desc`,
name: [field.name, "line_desc"],
label: t("billlines.fields.line_desc"),
rules: [{ required: true }]
}),
formInput: () => <Input.TextArea disabled={disabled} autoSize />
formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
},
{
title: t("billlines.fields.confidence"),
@@ -228,17 +230,19 @@ export function BillEnterModalLinesComponent({
editable: true,
width: "4rem",
formItemProps: (field) => ({
key: `${field.index}quantity`,
key: `${field.name}quantity`,
name: [field.name, "quantity"],
label: t("billlines.fields.quantity"),
rules: [
{ required: true },
({ getFieldValue: gf }) => ({
validator(rule, value) {
if (value && gf("billlines")[field.fieldKey]?.inventories?.length > value) {
validator(_, value) {
const invLen = gf(["billlines", field.name, "inventories"])?.length ?? 0;
if (value && invLen > value) {
return Promise.reject(
t("bills.validation.inventoryquantity", {
number: gf("billlines")[field.fieldKey]?.inventories?.length
number: invLen
})
);
}
@@ -247,7 +251,7 @@ export function BillEnterModalLinesComponent({
})
]
}),
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} />
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} tabIndex={0} />
},
{
title: t("billlines.fields.actual_price"),
@@ -255,7 +259,7 @@ export function BillEnterModalLinesComponent({
width: "8rem",
editable: true,
formItemProps: (field) => ({
key: `${field.index}actual_price`,
key: `${field.name}actual_price`,
name: [field.name, "actual_price"],
label: t("billlines.fields.actual_price"),
rules: [{ required: true }]
@@ -264,9 +268,10 @@ export function BillEnterModalLinesComponent({
<CurrencyInput
min={0}
disabled={disabled}
onBlur={() => autofillActualCost(index)}
tabIndex={0}
// NOTE: Autofill should only happen on forward Tab out of Retail
onKeyDown={(e) => {
if (e.key === "Tab") autofillActualCost(index);
if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
}}
/>
),
@@ -307,7 +312,7 @@ export function BillEnterModalLinesComponent({
width: "10rem",
skipFormItem: true,
formItemProps: (field) => ({
key: `${field.index}actual_cost`,
key: `${field.name}actual_cost`,
name: [field.name, "actual_cost"],
label: t("billlines.fields.actual_cost"),
rules: [{ required: true }]
@@ -341,6 +346,7 @@ export function BillEnterModalLinesComponent({
min={0}
disabled={disabled}
controls={false}
tabIndex={0}
style={{ width: "100%", height: CONTROL_HEIGHT }}
onFocus={() => autofillActualCost(index)}
/>
@@ -398,14 +404,14 @@ export function BillEnterModalLinesComponent({
dataIndex: "cost_center",
editable: true,
formItemProps: (field) => ({
key: `${field.index}cost_center`,
key: `${field.name}cost_center`,
name: [field.name, "cost_center"],
label: t("billlines.fields.cost_center"),
valuePropName: "value",
rules: [{ required: true }]
}),
formInput: () => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled} tabIndex={0}>
{bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
@@ -421,11 +427,11 @@ export function BillEnterModalLinesComponent({
editable: true,
label: t("billlines.fields.location"),
formItemProps: (field) => ({
key: `${field.index}location`,
key: `${field.name}location`,
name: [field.name, "location"]
}),
formInput: () => (
<Select disabled={disabled}>
<Select disabled={disabled} tabIndex={0}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
@@ -442,10 +448,10 @@ export function BillEnterModalLinesComponent({
width: "40px",
formItemProps: (field) => ({
valuePropName: "checked",
key: `${field.index}deductedfromlbr`,
key: `${field.name}deductedfromlbr`,
name: [field.name, "deductedfromlbr"]
}),
formInput: () => <Switch disabled={disabled} />,
formInput: () => <Switch disabled={disabled} tabIndex={0} />,
additional: (record, index) => (
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
{() => {
@@ -528,11 +534,15 @@ export function BillEnterModalLinesComponent({
editable: true,
width: "40px",
formItemProps: (field) => ({
key: `${field.index}fedtax`,
key: `${field.name}fedtax`,
valuePropName: "checked",
name: [field.name, "applicable_taxes", "federal"]
name: [field.name, "applicable_taxes", "federal"],
initialValue: InstanceRenderManager({
imex: true,
rome: false
})
}),
formInput: () => <Switch disabled={disabled} />
formInput: () => <Switch disabled={disabled} tabIndex={0} />
}
]
}),
@@ -543,11 +553,11 @@ export function BillEnterModalLinesComponent({
editable: true,
width: "40px",
formItemProps: (field) => ({
key: `${field.index}statetax`,
key: `${field.name}statetax`,
valuePropName: "checked",
name: [field.name, "applicable_taxes", "state"]
}),
formInput: () => <Switch disabled={disabled} />
formInput: () => <Switch disabled={disabled} tabIndex={0} />
},
...InstanceRenderManager({
@@ -559,11 +569,11 @@ export function BillEnterModalLinesComponent({
editable: true,
width: "40px",
formItemProps: (field) => ({
key: `${field.index}localtax`,
key: `${field.name}localtax`,
valuePropName: "checked",
name: [field.name, "applicable_taxes", "local"]
}),
formInput: () => <Switch disabled={disabled} />
formInput: () => <Switch disabled={disabled} tabIndex={0} />
}
]
}),
@@ -573,24 +583,29 @@ export function BillEnterModalLinesComponent({
dataIndex: "actions",
render: (text, record) => (
<Form.Item shouldUpdate noStyle>
{() => (
<Space wrap>
<Button
disabled={disabled || getFieldValue("billlines")[record.fieldKey]?.inventories?.length > 0}
onClick={() => remove(record.name)}
>
<DeleteFilled />
</Button>
{() => {
const currentLine = getFieldValue(["billlines", record.name]);
const invLen = currentLine?.inventories?.length ?? 0;
{Simple_Inventory.treatment === "on" && (
<BilllineAddInventory
disabled={!billEdit || form.isFieldsTouched() || form.getFieldValue("is_credit_memo")}
billline={getFieldValue("billlines")[record.fieldKey]}
jobid={getFieldValue("jobid")}
return (
<Space wrap>
<Button
icon={<DeleteFilled />}
disabled={disabled || invLen > 0}
onClick={() => remove(record.name)}
tabIndex={0}
/>
)}
</Space>
)}
{Simple_Inventory.treatment === "on" && (
<BilllineAddInventory
disabled={!billEdit || form.isFieldsTouched() || form.getFieldValue("is_credit_memo")}
billline={currentLine}
jobid={getFieldValue("jobid")}
/>
)}
</Space>
);
}}
</Form.Item>
)
}
@@ -638,8 +653,9 @@ export function BillEnterModalLinesComponent({
size="small"
bordered
dataSource={fields}
rowKey="key"
columns={mergedColumns(remove)}
scroll={hasRows ? { x: "max-content" } : undefined} // <-- no scrollbar when empty
scroll={hasRows ? { x: "max-content" } : undefined}
pagination={false}
rowClassName="editable-row"
/>
@@ -649,12 +665,19 @@ export function BillEnterModalLinesComponent({
<Button
disabled={disabled}
onClick={() => {
const newIndex = fields.length;
add(
InstanceRenderManager({
imex: { applicable_taxes: { federal: true } },
rome: { applicable_taxes: { federal: false } }
})
);
setTimeout(() => {
const firstField = firstFieldRefs.current[newIndex];
if (firstField?.focus) {
firstField.focus();
}
}, 100);
}}
style={{ width: "100%" }}
>
@@ -694,11 +717,7 @@ const EditableCell = ({
const control = skipFormItem ? (
(formInput && formInput(record, record.name, propsFinal)) || children
) : (
<Form.Item
labelCol={{ span: 0 }}
{...propsFinal}
style={{ marginBottom: 0 }} // <-- important: remove default Form.Item margin
>
<Form.Item labelCol={{ span: 0 }} {...propsFinal} style={{ marginBottom: 0 }}>
{(formInput && formInput(record, record.name, propsFinal)) || children}
</Form.Item>
);
@@ -715,10 +734,7 @@ const EditableCell = ({
const { style: tdStyle, ...tdRest } = restProps;
const td = (
<td
{...tdRest}
style={{ ...tdStyle, verticalAlign: "middle" }} // optional but helps consistency
>
<td {...tdRest} style={{ ...tdStyle, verticalAlign: "middle" }}>
{cellInner}
</td>
);