Merged in development (pull request #28)

Reorder totals table IO-734
Resolve null parts status reference. IO-737
Allow time ticket entry for RO jobs IO-741
Technician clock issues IO-731
Updates for audatex claims & mapa/mash calculations IO-718
Partial fixes to jobline upsert & totals calculation. IO-730
Clear Errors & Update CI
This commit is contained in:
Patrick Fic
2021-03-05 00:01:28 +00:00
24 changed files with 192 additions and 116 deletions

View File

@@ -22603,6 +22603,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>shiftclockin</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
@@ -25619,7 +25640,7 @@
</translations>
</concept_node>
<concept_node>
<name>filing_coverhseet_portrait</name>
<name>filing_coversheet_portrait</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>

View File

@@ -47,13 +47,14 @@ export function JobLinesComponent({
form,
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
const {
loading: billLinesLoading,
error: billLinesError,
data: billLinesData,
} = useQuery(QUERY_BILLS_BY_JOB_REF, {
variables: { jobId: job.id },
skip: loading,
variables: { jobId: job && job.id },
skip: loading || !job,
});
const billLinesDataObj = useMemo(() => {
@@ -299,40 +300,43 @@ export function JobLinesComponent({
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space>
<Button
disabled={jobRO}
onClick={() => {
setJobLineEditContext({
actions: { refetch: refetch, submit: form && form.submit },
context: record,
});
}}
>
{t("general.actions.edit")}
</Button>
<div>
{record.manual_line && (
<Button
onClick={() =>
deleteJobLine({
variables: { joblineId: record.id },
update(cache) {
cache.modify({
id: cache.identify(job),
fields: {
joblines(existingJobLines, { readField }) {
return existingJobLines.filter(
(jlRef) => record.id !== readField("id", jlRef)
);
<Space >
<Button
disabled={jobRO}
onClick={() => {
setJobLineEditContext({
actions: { refetch: refetch, submit: form && form.submit },
context: record,
});
}}
>
{t("general.actions.edit")}
</Button>
<Button
disabled={jobRO}
onClick={() =>
deleteJobLine({
variables: { joblineId: record.id },
update(cache) {
cache.modify({
id: cache.identify(job),
fields: {
joblines(existingJobLines, { readField }) {
return existingJobLines.filter(
(jlRef) => record.id !== readField("id", jlRef)
);
},
},
},
});
},
})
}
>
<DeleteFilled />
</Button>
});
},
})
}
>
<DeleteFilled />
</Button>
</Space>
)}
{
// <AllocationsAssignmentContainer
@@ -342,7 +346,7 @@ export function JobLinesComponent({
// hours={record.mod_lb_hrs}
// />
}
</Space>
</div>
),
},
];

View File

@@ -61,7 +61,7 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
onSelect={handleChange}
onBlur={handleSave}
>
{bodyshop.md_order_statuses.statuses.map((s, idx) => (
{Object.values(bodyshop.md_order_statuses).map((s, idx) => (
<Select.Option key={idx} value={s}>
{s}
</Select.Option>

View File

@@ -10,6 +10,7 @@ import {
} from "../../graphql/jobs-lines.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
import UndefinedToNull from "../../utils/undefinedtonull";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
const mapStateToProps = createStructuredSelector({
@@ -39,7 +40,7 @@ function JobLinesUpsertModalContainer({
manual_line: !(
jobLineEditModal.context && jobLineEditModal.context.id
),
...values,
...UndefinedToNull(values),
},
],
},

View File

@@ -267,6 +267,14 @@ export function JobsTotalsTableComponent({ bodyshop, jobRO, job }) {
</Typography.Title>
<table>
<tbody>
<tr>
<td>{t("jobs.labels.subtotal")}</td>
<td className="currency">
<strong>
{Dinero(job.job_totals.totals.subtotal).toFormat()}
</strong>
</td>
</tr>
<tr>
<td>{t("jobs.labels.local_tax_amt")}</td>
<td className="currency">
@@ -317,26 +325,18 @@ export function JobsTotalsTableComponent({ bodyshop, jobRO, job }) {
).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.total_cust_payable")}</td>
<td className="currency">
{Dinero(job.job_totals.totals.custPayable.total).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.subtotal")}</td>
<td className="currency">
<strong>
{Dinero(job.job_totals.totals.subtotal).toFormat()}
</strong>
</td>
</tr>
<tr>
<td>{t("jobs.labels.total_repairs")}</td>
<td className="currency">
{Dinero(job.job_totals.totals.total_repairs).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.total_cust_payable")}</td>
<td className="currency">
{Dinero(job.job_totals.totals.custPayable.total).toFormat()}
</td>
</tr>
<tr>
<td>{t("jobs.labels.net_repairs")}</td>
<td className="currency">

View File

@@ -97,7 +97,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
label={t("jobs.fields.referralsource")}
name="referral_source"
>
<Select disabled={jobRO}>
<Select disabled={jobRO} allowClear>
{bodyshop.md_referral_sources.map((s) => (
<Select.Option key={s} value={s}>
{s}
@@ -106,7 +106,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport">
<Select disabled={jobRO}>
<Select disabled={jobRO} allowClear>
{bodyshop.appt_alt_transport.map((s) => (
<Select.Option key={s} value={s}>
{s}

View File

@@ -28,6 +28,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "payment" })),
setJobCostingContext: (context) =>
dispatch(setModalContext({ context: context, modal: "jobCosting" })),
setTimeTicketContext: (context) =>
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
});
export function JobsDetailHeaderActions({
@@ -39,6 +41,7 @@ export function JobsDetailHeaderActions({
setPaymentContext,
setJobCostingContext,
jobRO,
setTimeTicketContext,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -110,6 +113,19 @@ export function JobsDetailHeaderActions({
{t("jobs.actions.viewchecklist")}
</Link>
</Menu.Item>
<Menu.Item
key="entertimetickets"
onClick={() => {
logImEXEvent("job_header_enter_time_ticekts");
setTimeTicketContext({
actions: {},
context: { jobId: job.id },
});
}}
>
{t("timetickets.actions.enter")}
</Menu.Item>
<Menu.Item
key="enterpayments"
disabled={jobRO}

View File

@@ -27,11 +27,7 @@ export function JobsDetailLaborContainer({
return (
<div>
{techConsole ? null : (
<TimeTicketEnterButton
disabled={jobRO}
actions={{ refetch }}
context={{ jobId: jobId }}
>
<TimeTicketEnterButton actions={{ refetch }} context={{ jobId: jobId }}>
{t("timetickets.actions.enter")}
</TimeTicketEnterButton>
)}

View File

@@ -86,17 +86,13 @@ export default function JobsFindModalComponent({
key: "vehicle",
width: "15%",
ellipsis: true,
render: (text, record) => {
return record.vehicle ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
t("jobs.errors.novehicle")
);
},
render: (text, record) => (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
),
},
{
title: t("vehicles.fields.plate_no"),

View File

@@ -1,19 +1,21 @@
import { Form } from "antd";
import { Form, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import JobsDetailLaborContainer from "../jobs-detail-labor/jobs-detail-labor.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
});
export function TechClockInComponent({ form, bodyshop }) {
export function TechClockInComponent({ form, bodyshop, technician }) {
const { t } = useTranslation();
const emps = bodyshop.employees.filter((e) => e.id === technician.id)[0];
return (
<div>
<Form.Item
@@ -29,6 +31,26 @@ export function TechClockInComponent({ form, bodyshop }) {
<JobSearchSelect />
</Form.Item>
<Form.Item
name="cost_center"
label={t("timetickets.fields.cost_center")}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Select>
{emps &&
emps.rates.map((item) => (
<Select.Option key={item.cost_center}>
{item.cost_center}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.jobid !== curValues.jobid

View File

@@ -35,7 +35,7 @@ export function TechClockInContainer({ technician, bodyshop }) {
date: theTime,
clockon: theTime,
jobid: values.jobid,
cost_center: technician.cost_center,
cost_center: values.cost_center,
ciecacode: Object.keys(
bodyshop.md_responsibility_centers.defaults.costs
).find((key) => {

View File

@@ -30,6 +30,7 @@ export function TechClockOffButton({
const [form] = Form.useForm();
const { t } = useTranslation();
const emps = bodyshop.employees.filter((e) => e.id === technician.id)[0];
const handleFinish = async (values) => {
logImEXEvent("tech_clock_out_job");
@@ -120,9 +121,10 @@ export function TechClockOffButton({
{t("timetickets.labels.shift")}
</Select.Option>
) : (
bodyshop.md_responsibility_centers.costs.map((i, idx) => (
<Select.Option key={idx} value={i.name}>
{i.name}
emps &&
emps.rates.map((item) => (
<Select.Option key={item.cost_center}>
{item.cost_center}
</Select.Option>
))
)}

View File

@@ -81,6 +81,9 @@ export function TechClockedInList({ technician }) {
<DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel>
<DataLabel label={t("timetickets.fields.cost_center")}>
{ticket.cost_center}{" "}
</DataLabel>
</Card>
</List.Item>
)}

View File

@@ -1,6 +1,6 @@
import { PrinterFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Col, Drawer, Grid, PageHeader, Row, Tag, Tabs } from "antd";
import { Button, Drawer, Grid, PageHeader, Tabs, Tag } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -9,26 +9,26 @@ import { Link, useHistory, useLocation } from "react-router-dom";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import OwnerTagPopoverComponent from "../owner-tag-popover/owner-tag-popover.component";
import VehicleTagPopoverComponent from "../vehicle-tag-popover/vehicle-tag-popover.component";
import JobLinesContainer from "../job-detail-lines/job-lines.container";
import JobsDocumentsGalleryContainer from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobNotesContainer from "../jobs-notes/jobs-notes.container";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import OwnerTagPopoverComponent from "../owner-tag-popover/owner-tag-popover.component";
import VehicleTagPopoverComponent from "../vehicle-tag-popover/vehicle-tag-popover.component";
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })),
});
const colBreakPoints = {
xs: {
span: 24,
},
sm: {
span: 8,
},
};
// const colBreakPoints = {
// xs: {
// span: 24,
// },
// sm: {
// span: 8,
// },
// };
export function JobDetailCards({ setPrintCenterContext }) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
@@ -116,16 +116,12 @@ export function JobDetailCards({ setPrintCenterContext }) {
</Button>
}
>
<Row gutter={[16, 16]}>
<Col {...colBreakPoints}> What would be good to have here?</Col>
<Col {...colBreakPoints}> What would be good to have here?</Col>
<Col {...colBreakPoints}> What would be good to have here?</Col>
</Row>
<Tabs size="large">
<Tabs.TabPane key="lines" tab={t("jobs.labels.lines")}>
<JobLinesContainer
jobId={selected}
joblines={data.jobs_by_pk.joblines}
job={data.jobs_by_pk}
refetch={refetch}
/>
</Tabs.TabPane>

View File

@@ -13,10 +13,8 @@ import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
export default function TimeTicketModalComponent({
form,
roAutoCompleteOptions,
employeeAutoCompleteOptions,
loadLineTicketData,
lineTicketData,
}) {
const { t } = useTranslation();
@@ -33,7 +31,7 @@ export default function TimeTicketModalComponent({
},
]}
>
<JobSearchSelect options={roAutoCompleteOptions} />
<JobSearchSelect convertedOnly notExported={false} />
</Form.Item>
<Form.Item
label={t("timetickets.fields.date")}

View File

@@ -346,6 +346,7 @@ export const GET_JOB_BY_PK = gql`
inproduction
vehicleid
plate_no
plate_st
v_vin
v_model_yr
v_model_desc
@@ -357,6 +358,7 @@ export const GET_JOB_BY_PK = gql`
vehicle {
id
plate_no
plate_st
v_vin
v_model_yr
v_model_desc
@@ -828,7 +830,6 @@ export const SEARCH_JOBS_FOR_AUTOCOMPLETE = gql`
search_jobs(
args: { search: $search }
limit: 50
order_by: { ro_number: desc_nulls_last }
where: {
_and: {
converted: { _eq: $isConverted }

View File

@@ -115,6 +115,7 @@ export const QUERY_ACTIVE_TIME_TICKETS = gql`
id
clockon
memo
cost_center
job {
id

View File

@@ -1344,7 +1344,8 @@
"login": "Login",
"logout": "Logout",
"productionboard": "Production Board - Visual",
"productionlist": "Production List"
"productionlist": "Production List",
"shiftclockin": "Shift Clock"
}
},
"messaging": {
@@ -1552,7 +1553,7 @@
"diagnostic_authorization": "Diagnostic Authorization",
"estimate": "Estimate Only",
"estimate_detail": "Estimate Details",
"filing_coverhseet_portrait": "Filing Coversheet (Portrait)",
"filing_coversheet_portrait": "Filing Coversheet (Portrait)",
"final_invoice": "Final Invoice",
"fippa_authorization": "FIPPA Authorization",
"glass_express_checklist": "Glass Express Checklist",

View File

@@ -1344,7 +1344,8 @@
"login": "",
"logout": "",
"productionboard": "",
"productionlist": ""
"productionlist": "",
"shiftclockin": ""
}
},
"messaging": {
@@ -1552,7 +1553,7 @@
"diagnostic_authorization": "",
"estimate": "",
"estimate_detail": "",
"filing_coverhseet_portrait": "",
"filing_coversheet_portrait": "",
"final_invoice": "",
"fippa_authorization": "",
"glass_express_checklist": "",

View File

@@ -1344,7 +1344,8 @@
"login": "",
"logout": "",
"productionboard": "",
"productionlist": ""
"productionlist": "",
"shiftclockin": ""
}
},
"messaging": {
@@ -1552,7 +1553,7 @@
"diagnostic_authorization": "",
"estimate": "",
"estimate_detail": "",
"filing_coverhseet_portrait": "",
"filing_coversheet_portrait": "",
"final_invoice": "",
"fippa_authorization": "",
"glass_express_checklist": "",

View File

@@ -42,7 +42,11 @@ export default async function RenderTemplate(
try {
const render = await jsreport.renderAsync(reportRequest);
if (!renderAsHtml) {
render.download(Templates[templateObject.name].title || "");
render.download(
(Templates[templateObject.name] &&
Templates[templateObject.name].title) ||
""
);
} else {
return new Promise((resolve, reject) => {
resolve(render.toString());

View File

@@ -167,11 +167,11 @@ export const TemplateList = (type, context) => {
key: "coversheet_portrait",
disabled: false,
},
filing_coverhseet_portrait: {
title: i18n.t("printcenter.jobs.filing_coverhseet_portrait"),
filing_coversheet_portrait: {
title: i18n.t("printcenter.jobs.filing_coversheet_portrait"),
description: "All Jobs Notes",
subject: i18n.t("printcenter.jobs.filing_coverhseet_portrait"),
key: "filing_coverhseet_portrait",
subject: i18n.t("printcenter.jobs.filing_coversheet_portrait"),
key: "filing_coversheet_portrait",
disabled: false,
},
}

View File

@@ -0,0 +1,6 @@
export default function UndefinedToNull(obj) {
Object.keys(obj).forEach((key) => {
if (obj[key] === undefined) obj[key] = null;
});
return obj;
}

View File

@@ -41,10 +41,10 @@ exports.totalsSsu = async function (req, res) {
},
});
res.status(200);
res.status(200).send();
} catch (error) {
console.log(error);
res.status(503);
res.status(503).send();
}
};
@@ -269,7 +269,13 @@ function IsAdditionalCost(jobLine) {
//936008 is Paint/Materials
//936007 is Shop/Materials
return !jobLine.db_ref || jobLine.db_ref.startsWith("9360");
//Remove paint and shop mat lines. They're calculated under rates.
const isPaintOrShopMat =
jobLine.db_ref === "936008" || jobLine.db_ref === "936007";
return (
!jobLine.db_ref || (jobLine.db_ref.startsWith("9360") && !isPaintOrShopMat)
);
}
function CalculateAdditional(job) {
@@ -305,7 +311,7 @@ function CalculateAdditional(job) {
function CalculateTaxesTotals(job, otherTotals) {
const subtotal = otherTotals.parts.parts.subtotal
.add(otherTotals.parts.sublets.subtotal)
.add(otherTotals.rates.rates_subtotal)
.add(otherTotals.rates.subtotal) //No longer using just rates subtotal to include mapa/mash.
.add(otherTotals.additional);
// .add(Dinero({ amount: (job.towing_payable || 0) * 100 }))
// .add(Dinero({ amount: (job.storage_payable || 0) * 100 }));
@@ -319,7 +325,7 @@ function CalculateTaxesTotals(job, otherTotals) {
job.joblines
.filter((jl) => !jl.removed)
.forEach((val) => {
if (!val.tax_part || !val.part_type || IsAdditionalCost(val)) {
if (!val.tax_part || (!val.part_type && IsAdditionalCost(val))) {
additionalItemsTax = additionalItemsTax.add(
Dinero({ amount: Math.round((val.act_price || 0) * 100) })
.multiply(val.part_qty || 1)
@@ -350,7 +356,7 @@ function CalculateTaxesTotals(job, otherTotals) {
statePartsTax,
state_tax: statePartsTax
.add(
otherTotals.rates.rates_subtotal.percentage((job.tax_lbr_rt || 0) * 100)
otherTotals.rates.subtotal.percentage((job.tax_lbr_rt || 0) * 100) // THis is currently using the lbr tax rate from PFH not PFL.
)
.add(
Dinero({