diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx
index ad81b683e..d1fb03369 100644
--- a/client/src/components/bill-form/bill-form.lines.component.jsx
+++ b/client/src/components/bill-form/bill-form.lines.component.jsx
@@ -33,13 +33,12 @@ export function BillEnterModalLinesComponent({
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
- // 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 +78,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);
@@ -115,7 +113,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 +142,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 }]
@@ -203,7 +200,7 @@ 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 }]
@@ -216,17 +213,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
})
);
}
@@ -243,7 +242,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 }]
@@ -295,7 +294,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 }]
@@ -386,7 +385,7 @@ 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",
@@ -409,7 +408,7 @@ 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: () => (
@@ -430,7 +429,7 @@ export function BillEnterModalLinesComponent({
width: "40px",
formItemProps: (field) => ({
valuePropName: "checked",
- key: `${field.index}deductedfromlbr`,
+ key: `${field.name}deductedfromlbr`,
name: [field.name, "deductedfromlbr"]
}),
formInput: () => ,
@@ -516,7 +515,7 @@ 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"]
}),
@@ -531,7 +530,7 @@ 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"]
}),
@@ -547,7 +546,7 @@ 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"]
}),
@@ -561,23 +560,28 @@ export function BillEnterModalLinesComponent({
dataIndex: "actions",
render: (text, record) => (
- {() => (
-
- }
- disabled={disabled || getFieldValue("billlines")[record.fieldKey]?.inventories?.length > 0}
- onClick={() => remove(record.name)}
- />
+ {() => {
+ const currentLine = getFieldValue(["billlines", record.name]);
+ const invLen = currentLine?.inventories?.length ?? 0;
- {Simple_Inventory.treatment === "on" && (
-
+ }
+ disabled={disabled || invLen > 0}
+ onClick={() => remove(record.name)}
/>
- )}
-
- )}
+
+ {Simple_Inventory.treatment === "on" && (
+
+ )}
+
+ );
+ }}
)
}
@@ -625,8 +629,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"
/>
@@ -681,11 +686,7 @@ const EditableCell = ({
const control = skipFormItem ? (
(formInput && formInput(record, record.name, propsFinal)) || children
) : (
-
+
{(formInput && formInput(record, record.name, propsFinal)) || children}
);
@@ -702,10 +703,7 @@ const EditableCell = ({
const { style: tdStyle, ...tdRest } = restProps;
const td = (
-
+ |
{cellInner}
|
);
diff --git a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx
index 315d94534..cf7de778f 100644
--- a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx
+++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx
@@ -17,108 +17,126 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
-const mapDispatchToProps = () => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
-});
+const mapDispatchToProps = () => ({});
export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory);
export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled, jobid }) {
const [loading, setLoading] = useState(false);
- const { billid } = queryString.parse(useLocation().search);
+ const qs = queryString.parse(useLocation().search);
+ const billid = qs?.billid != null ? String(qs.billid) : null;
+
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const notification = useNotification();
+ const inventoryCount = billline?.inventories?.length ?? 0;
+ const quantity = billline?.quantity ?? 0;
+
const addToInventory = async () => {
- setLoading(true);
+ if (loading) return;
- //Check to make sure there are no existing items already in the inventory.
-
- const cm = {
- vendorid: bodyshop.inhousevendorid,
- invoice_number: "ih",
- jobid: jobid,
- isinhouse: true,
- is_credit_memo: true,
- date: dayjs().format("YYYY-MM-DD"),
- federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
- state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
- local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
- total: 0,
- billlines: [
- {
- actual_price: billline.actual_price,
- actual_cost: billline.actual_cost,
- quantity: billline.quantity,
- line_desc: billline.line_desc,
- cost_center: billline.cost_center,
- deductedfromlbr: billline.deductedfromlbr,
- applicable_taxes: {
- local: billline.applicable_taxes.local,
- state: billline.applicable_taxes.state,
- federal: billline.applicable_taxes.federal
- }
- }
- ]
- };
-
- cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
-
- const insertResult = await insertInventoryLine({
- variables: {
- joblineId: billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
- //Unfortunately, we can't send null as the GQL syntax validation fails.
- joblineStatus: bodyshop.md_order_statuses.default_returned,
- inv: {
- shopid: bodyshop.id,
- billlineid: billline.id,
- actual_price: billline.actual_price,
- actual_cost: billline.actual_cost,
- quantity: billline.quantity,
- line_desc: billline.line_desc
- },
- cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
- pol: {
- returnfrombill: billid,
- vendorid: bodyshop.inhousevendorid,
- deliver_by: dayjs().format("YYYY-MM-DD"),
- parts_order_lines: {
- data: [
- {
- line_desc: billline.line_desc,
-
- act_price: billline.actual_price,
- cost: billline.actual_cost,
- quantity: billline.quantity,
- job_line_id: billline.joblineid === "noline" ? null : billline.joblineid,
- part_type: billline.jobline && billline.jobline.part_type,
- cm_received: true
- }
- ]
- },
- order_date: "2022-06-01",
- orderedby: currentUser.email,
- jobid: jobid,
- user_email: currentUser.email,
- return: true,
- status: "Ordered"
- }
- },
- refetchQueries: ["QUERY_BILL_BY_PK"]
- });
-
- if (!insertResult.errors) {
- notification.success({
- title: t("inventory.successes.inserted")
- });
- } else {
+ // Defensive: row identity can transiently desync during remove/add reindexing.
+ if (!billline) {
notification.error({
- title: t("inventory.errors.inserting", {
- error: JSON.stringify(insertResult.errors)
- })
+ title: t("inventory.errors.inserting", { error: "Bill line is missing (please try again)." })
});
+ return;
}
- setLoading(false);
+ setLoading(true);
+
+ try {
+ const taxes = billline?.applicable_taxes ?? {};
+ const cm = {
+ vendorid: bodyshop.inhousevendorid,
+ invoice_number: "ih",
+ jobid: jobid,
+ isinhouse: true,
+ is_credit_memo: true,
+ date: dayjs().format("YYYY-MM-DD"),
+ federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
+ state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
+ local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
+ total: 0,
+ billlines: [
+ {
+ actual_price: billline.actual_price,
+ actual_cost: billline.actual_cost,
+ quantity: billline.quantity,
+ line_desc: billline.line_desc,
+ cost_center: billline.cost_center,
+ deductedfromlbr: billline.deductedfromlbr,
+ applicable_taxes: {
+ local: taxes.local,
+ state: taxes.state,
+ federal: taxes.federal
+ }
+ }
+ ]
+ };
+
+ cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
+
+ const insertResult = await insertInventoryLine({
+ variables: {
+ joblineId: billline.joblineid === "noline" ? billline.id : billline.joblineid,
+ joblineStatus: bodyshop.md_order_statuses.default_returned,
+ inv: {
+ shopid: bodyshop.id,
+ billlineid: billline.id,
+ actual_price: billline.actual_price,
+ actual_cost: billline.actual_cost,
+ quantity: billline.quantity,
+ line_desc: billline.line_desc
+ },
+ cm: { ...cm, billlines: { data: cm.billlines } },
+ pol: {
+ returnfrombill: billid,
+ vendorid: bodyshop.inhousevendorid,
+ deliver_by: dayjs().format("YYYY-MM-DD"),
+ parts_order_lines: {
+ data: [
+ {
+ line_desc: billline.line_desc,
+ act_price: billline.actual_price,
+ cost: billline.actual_cost,
+ quantity: billline.quantity,
+ job_line_id: billline.joblineid === "noline" ? null : billline.joblineid,
+ part_type: billline.jobline && billline.jobline.part_type,
+ cm_received: true
+ }
+ ]
+ },
+ order_date: "2022-06-01",
+ orderedby: currentUser.email,
+ jobid: jobid,
+ user_email: currentUser.email,
+ return: true,
+ status: "Ordered"
+ }
+ },
+ refetchQueries: ["QUERY_BILL_BY_PK"]
+ });
+
+ if (!insertResult?.errors?.length) {
+ notification.success({
+ title: t("inventory.successes.inserted")
+ });
+ } else {
+ notification.error({
+ title: t("inventory.errors.inserting", {
+ error: JSON.stringify(insertResult.errors)
+ })
+ });
+ }
+ } catch (err) {
+ notification.error({
+ title: t("inventory.errors.inserting", {
+ error: err?.message || String(err)
+ })
+ });
+ } finally {
+ setLoading(false);
+ }
};
return (
@@ -126,10 +144,10 @@ export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled
}
loading={loading}
- disabled={disabled || billline?.inventories?.length >= billline.quantity}
+ disabled={disabled || inventoryCount >= quantity}
onClick={addToInventory}
>
- {billline?.inventories?.length > 0 && ({billline?.inventories?.length} in inv)
}
+ {inventoryCount > 0 && ({inventoryCount} in inv)
}
);