BOD-63 Added CSS styling to prevent overflow. Added totals calculation to invoice screen. Updated packages.
This commit is contained in:
@@ -46,10 +46,22 @@ export function InvoiceDetailEditContainer({ bodyshop }) {
|
||||
data
|
||||
? {
|
||||
...data.invoices_by_pk,
|
||||
|
||||
invoicelines: data.invoices_by_pk.invoicelines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||
applicable_taxes: {
|
||||
federal:
|
||||
(i.applicable_taxes && i.applicable_taxes.federal) ||
|
||||
false,
|
||||
state:
|
||||
(i.applicable_taxes && i.applicable_taxes.state) ||
|
||||
false,
|
||||
local:
|
||||
(i.applicable_taxes && i.applicable_taxes.local) ||
|
||||
false,
|
||||
},
|
||||
};
|
||||
}),
|
||||
date: data.invoices_by_pk
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { DatePicker, Form, Input, Switch } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { DatePicker, Form, Input, Statistic, Switch } from "antd";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DocumentsUploadContainer from "../documents-upload/documents-upload.container";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import InvoiceFormLines from "./invoice-form.lines.component";
|
||||
import Dinero from "dinero.js";
|
||||
import { CalculateInvoiceTotal } from "./invoice-form.totals.utility";
|
||||
import "./invoice-form.styles.scss";
|
||||
|
||||
export default function InvoiceFormComponent({
|
||||
form,
|
||||
@@ -25,9 +26,22 @@ export default function InvoiceFormComponent({
|
||||
setDiscount(opt.discount);
|
||||
};
|
||||
|
||||
//TODO: Test this further. Required to set discount when viewing an invoice.
|
||||
useEffect(() => {
|
||||
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
|
||||
const vendorId = form.getFieldValue("vendorid");
|
||||
const matchingVendors = vendorAutoCompleteOptions.filter(
|
||||
(v) => v.id === vendorId
|
||||
);
|
||||
if (matchingVendors.length === 1) {
|
||||
setDiscount(matchingVendors[0].discount);
|
||||
}
|
||||
}
|
||||
}, [form, setDiscount, vendorAutoCompleteOptions]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div className="invoice-form-wrapper">
|
||||
<div className="invoice-form-invoice-details">
|
||||
<Form.Item
|
||||
name="jobid"
|
||||
label={t("invoices.fields.ro_number")}
|
||||
@@ -64,7 +78,7 @@ export default function InvoiceFormComponent({
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div className="invoice-form-invoice-details">
|
||||
<Form.Item
|
||||
label={t("invoices.fields.invoice_number")}
|
||||
name="invoice_number"
|
||||
@@ -158,21 +172,62 @@ export default function InvoiceFormComponent({
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<div>
|
||||
{JSON.stringify(form.getFieldsValue(["invoicelines", "total"]))}
|
||||
</div>
|
||||
const totals = CalculateInvoiceTotal(
|
||||
form.getFieldsValue([
|
||||
"invoicelines",
|
||||
"total",
|
||||
"federal_tax_rate",
|
||||
"state_tax_rate",
|
||||
"local_tax_rate",
|
||||
])
|
||||
);
|
||||
if (!!totals)
|
||||
return (
|
||||
<div className="invoice-form-totals">
|
||||
<Statistic
|
||||
title={t("invoices.labels.subtotal")}
|
||||
value={totals.subtotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.federal_tax")}
|
||||
value={totals.federalTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.state_tax")}
|
||||
value={totals.stateTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.local_tax")}
|
||||
value={totals.localTax.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.entered_total")}
|
||||
value={totals.enteredTotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.invoice_total")}
|
||||
value={totals.invoiceTotal.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("invoices.labels.discrepancy")}
|
||||
valueStyle={{
|
||||
color:
|
||||
totals.discrepancy.getAmount() === 0 ? "green" : "red",
|
||||
}}
|
||||
value={totals.discrepancy.toFormat()}
|
||||
precision={2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(form.getFieldsValue());
|
||||
}}
|
||||
>
|
||||
Get Field Values
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import InvoiceLineSearchSelect from "../invoice-line-search-select/invoice-line-search-select.component";
|
||||
import { WarningOutlined } from "@ant-design/icons";
|
||||
|
||||
export default function InvoiceEnterModalLinesComponent({
|
||||
lineData,
|
||||
@@ -18,10 +19,10 @@ export default function InvoiceEnterModalLinesComponent({
|
||||
<Form.List name="invoicelines">
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="invoice-form-lines-wrapper">
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item required={false} key={field.key}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div className="invoice-form-line">
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.jobline")}
|
||||
key={`${index}joblinename`}
|
||||
@@ -116,6 +117,23 @@ export default function InvoiceEnterModalLinesComponent({
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const line = getFieldsValue(["invoicelines"])
|
||||
.invoicelines[index];
|
||||
if (!!!line) return null;
|
||||
const lineDiscount = +(
|
||||
1 -
|
||||
Math.round(
|
||||
(line.actual_cost / line.actual_price) * 100
|
||||
) /
|
||||
100
|
||||
).toFixed(2);
|
||||
|
||||
if (lineDiscount === discount) return null;
|
||||
return <WarningOutlined style={{ color: "red" }} />;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.cost_center")}
|
||||
key={`${index}cost_center`}
|
||||
@@ -136,7 +154,7 @@ export default function InvoiceEnterModalLinesComponent({
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.federal_tax_applicable")}
|
||||
key={`${index}fedtax`}
|
||||
initialValue={"true"}
|
||||
initialValue={true}
|
||||
valuePropName="checked"
|
||||
name={[field.name, "applicable_taxes", "federal"]}
|
||||
>
|
||||
@@ -149,7 +167,7 @@ export default function InvoiceEnterModalLinesComponent({
|
||||
name={[field.name, "applicable_taxes", "state"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>{" "}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("invoicelines.fields.local_tax_applicable")}
|
||||
key={`${index}localtax`}
|
||||
|
||||
40
client/src/components/invoice-form/invoice-form.styles.scss
Normal file
40
client/src/components/invoice-form/invoice-form.styles.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
.invoice-form-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.invoice-form-totals {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
& > * {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-form-invoice-details {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
& > * {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-form-lines-wrapper {
|
||||
border: 3px ridge rgba(28, 110, 164, 0.24);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.invoice-form-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-around;
|
||||
border-bottom: 2px dashed rgba(7, 7, 7, 0.4);
|
||||
F & > * {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
export const CalculateInvoiceTotal = (invoice) => {
|
||||
const {
|
||||
total,
|
||||
invoicelines,
|
||||
federal_tax_rate,
|
||||
local_tax_rate,
|
||||
state_tax_rate,
|
||||
} = invoice;
|
||||
|
||||
let subtotal = Dinero({ amount: 0 });
|
||||
let federalTax = Dinero({ amount: 0 });
|
||||
let stateTax = Dinero({ amount: 0 });
|
||||
let localTax = Dinero({ amount: 0 });
|
||||
if (!!!invoicelines) return null;
|
||||
invoicelines.map((i) => {
|
||||
if (!!i) {
|
||||
const itemTotal = Dinero({ amount: i.actual_cost * 100 || 0 }).multiply(
|
||||
i.quantity || 1
|
||||
);
|
||||
subtotal = subtotal.add(itemTotal);
|
||||
if (i.applicable_taxes.federal) {
|
||||
console.log("Adding fed tax.");
|
||||
federalTax = federalTax.add(
|
||||
itemTotal.percentage(federal_tax_rate || 0)
|
||||
);
|
||||
}
|
||||
if (i.applicable_taxes.state)
|
||||
stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
|
||||
if (i.applicable_taxes.local)
|
||||
localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
|
||||
}
|
||||
});
|
||||
|
||||
const invoiceTotal = Dinero({ amount: total * 100 || 0 });
|
||||
const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
|
||||
const discrepancy = enteredTotal.subtract(invoiceTotal);
|
||||
|
||||
return {
|
||||
subtotal,
|
||||
federalTax,
|
||||
stateTax,
|
||||
localTax,
|
||||
enteredTotal,
|
||||
invoiceTotal,
|
||||
discrepancy,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user