@@ -1,4 +1,4 @@
|
||||
<babeledit_project be_version="2.7.1" version="1.2">
|
||||
<babeledit_project version="1.2" be_version="2.7.1">
|
||||
<!--
|
||||
|
||||
BabelEdit project file
|
||||
@@ -1233,6 +1233,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>status</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>
|
||||
<concept_node>
|
||||
<name>subject</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -13447,6 +13468,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>deleting</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>
|
||||
<concept_node>
|
||||
<name>deleting_cloudinary</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -24536,6 +24578,53 @@
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
<name>qb_multiple_payers</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>amount</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>
|
||||
<concept_node>
|
||||
<name>name</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>
|
||||
<concept_node>
|
||||
<name>queued_for_parts</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import { Table } from "antd";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function EmailAuditTrailListComponent({ loading, data }) {
|
||||
const [state, setState] = useState({
|
||||
@@ -11,7 +10,7 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
|
||||
filteredInfo: {},
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
const columns = [
|
||||
{
|
||||
title: t("audit.fields.created"),
|
||||
dataIndex: " created",
|
||||
|
||||
@@ -85,6 +85,11 @@ export function JobAuditTrail({ currentUser, jobId }) {
|
||||
dataIndex: "subject",
|
||||
key: "subject",
|
||||
},
|
||||
{
|
||||
title: t("audit.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
},
|
||||
...(currentUser?.email.includes("@imex.")
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { Button, Form, notification, Popover, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
GET_DOC_SIZE_BY_JOB,
|
||||
UPDATE_DOCUMENT,
|
||||
} from "../../graphql/documents.queries";
|
||||
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||
|
||||
@@ -23,7 +20,11 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(JobsDocumentsGalleryReassign);
|
||||
|
||||
export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
|
||||
export function JobsDocumentsGalleryReassign({
|
||||
bodyshop,
|
||||
galleryImages,
|
||||
callback,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
@@ -36,34 +37,33 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
|
||||
const client = useApolloClient();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updateDocument] = useMutation(UPDATE_DOCUMENT);
|
||||
|
||||
const updateImage = async (i, jobid) => {
|
||||
//Move the cloudinary image
|
||||
// const updateImage = async (i, jobid) => {
|
||||
// //Move the cloudinary image
|
||||
|
||||
//Update it in the database.
|
||||
const result = await updateDocument({
|
||||
variables: {
|
||||
id: i.id,
|
||||
document: {
|
||||
key: i.public_id,
|
||||
jobid: jobid,
|
||||
},
|
||||
},
|
||||
});
|
||||
// //Update it in the database.
|
||||
// const result = await updateDocument({
|
||||
// variables: {
|
||||
// id: i.id,
|
||||
// document: {
|
||||
// key: i.public_id,
|
||||
// jobid: jobid,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("documents.errors.updating", {
|
||||
message: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
notification["success"]({
|
||||
message: t("documents.successes.updated"),
|
||||
});
|
||||
}
|
||||
};
|
||||
// if (!!result.errors) {
|
||||
// notification["error"]({
|
||||
// message: t("documents.errors.updating", {
|
||||
// message: JSON.stringify(result.errors),
|
||||
// }),
|
||||
// });
|
||||
// } else {
|
||||
// notification["success"]({
|
||||
// message: t("documents.successes.updated"),
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleFinish = async ({ jobid }) => {
|
||||
setLoading(true);
|
||||
@@ -96,6 +96,7 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
|
||||
}
|
||||
|
||||
const res = await axios.post("/media/rename", {
|
||||
tojobid: jobid,
|
||||
documents: selectedImages.map((i) => {
|
||||
//Need to check if the current key folder is null, or another job.
|
||||
const currentKeys = i.key.split("/");
|
||||
@@ -110,24 +111,21 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
|
||||
};
|
||||
}),
|
||||
});
|
||||
//Add in confirmation & errors.
|
||||
if (callback) callback();
|
||||
|
||||
res.data
|
||||
.filter((d) => d.error)
|
||||
.forEach((d) => {
|
||||
notification["error"]({ message: t("documents.errors.updating") });
|
||||
console.error("Error updating job document", d);
|
||||
if (res.errors) {
|
||||
notification["error"]({
|
||||
message: t("documents.errors.updating", {
|
||||
message: JSON.stringify(res.errors),
|
||||
}),
|
||||
});
|
||||
|
||||
const proms = [];
|
||||
|
||||
res.data
|
||||
.filter((d) => !d.error)
|
||||
.forEach((d) => {
|
||||
proms.push(updateImage(d, jobid));
|
||||
}
|
||||
if (!res.mutationResult?.errors) {
|
||||
notification["success"]({
|
||||
message: t("documents.successes.updated"),
|
||||
});
|
||||
|
||||
await Promise.all(proms);
|
||||
|
||||
}
|
||||
setVisible(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -125,7 +125,10 @@ function JobsDocumentsComponent({
|
||||
deletionCallback={billsCallback || refetch}
|
||||
/>
|
||||
{!billId && (
|
||||
<JobsDocumentsGalleryReassign galleryImages={galleryImages} />
|
||||
<JobsDocumentsGalleryReassign
|
||||
galleryImages={galleryImages}
|
||||
callback={refetch}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { DELETE_DOCUMENTS } from "../../graphql/documents.queries";
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
export default function JobsDocumentsDeleteButton({
|
||||
@@ -13,7 +11,7 @@ export default function JobsDocumentsDeleteButton({
|
||||
deletionCallback,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [deleteDocument] = useMutation(DELETE_DOCUMENTS);
|
||||
|
||||
const imagesToDelete = [
|
||||
...galleryImages.images.filter((image) => image.isSelected),
|
||||
...galleryImages.other.filter((image) => image.isSelected),
|
||||
@@ -27,31 +25,10 @@ export default function JobsDocumentsDeleteButton({
|
||||
ids: imagesToDelete,
|
||||
});
|
||||
|
||||
const successfulDeletes = [];
|
||||
res.data.forEach((resType) => {
|
||||
Object.keys(resType.deleted).forEach((key) => {
|
||||
if (resType.deleted[key] !== "deleted") {
|
||||
notification["error"]({
|
||||
message: t("documents.errors.deleting_cloudinary", {
|
||||
message: JSON.stringify(resType.deleted[key]),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
successfulDeletes.push(key.replace(/\.[^/.]+$/, ""));
|
||||
}
|
||||
});
|
||||
});
|
||||
const delres = await deleteDocument({
|
||||
variables: {
|
||||
ids: imagesToDelete
|
||||
.filter((i) => successfulDeletes.includes(i.key))
|
||||
.map((i) => i.id),
|
||||
},
|
||||
});
|
||||
if (delres.errors) {
|
||||
if (res.data.error) {
|
||||
notification["error"]({
|
||||
message: t("documents.errors.deleting", {
|
||||
message: JSON.stringify(delres.errors),
|
||||
error: JSON.stringify(res.data.error.response.errors),
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -34,6 +34,7 @@ export function BillMarkSelectedExported({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
const [updateBill] = useMutation(gql`
|
||||
mutation UPDATE_BILL($billIds: [uuid!]!) {
|
||||
@@ -84,11 +85,24 @@ export function BillMarkSelectedExported({
|
||||
completedCallback && completedCallback([]);
|
||||
setLoading(false);
|
||||
refetch && refetch();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button loading={loading} disabled={disabled} onClick={handleUpdate}>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
visible={visible}
|
||||
title={t("general.labels.areyousure")}
|
||||
onCancel={() => setVisible(false)}
|
||||
onConfirm={handleUpdate}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { gql, useMutation } from "@apollo/client";
|
||||
import { Button, notification } from "antd";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -34,6 +34,8 @@ export function PaymentMarkSelectedExported({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
const [updatePayments] = useMutation(gql`
|
||||
mutation UPDATE_PAYMENTS($paymentIds: [uuid!]!, $exportedat: timestamptz!) {
|
||||
@@ -86,11 +88,24 @@ export function PaymentMarkSelectedExported({
|
||||
completedCallback && completedCallback([]);
|
||||
setLoading(false);
|
||||
refetch && refetch();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button loading={loading} disabled={disabled} onClick={handleUpdate}>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
visible={visible}
|
||||
title={t("general.labels.areyousure")}
|
||||
onCancel={() => setVisible(false)}
|
||||
onConfirm={handleUpdate}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4535,6 +4535,24 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={<div>Multiple Payers Item</div>}>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={[
|
||||
"md_responsibility_centers",
|
||||
"qb_multiple_payers",
|
||||
"accountitem",
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Typography.Title level={4}>
|
||||
{t("bodyshop.labels.responsibilitycenters.sales_tax_codes")}
|
||||
</Typography.Title>
|
||||
|
||||
@@ -1890,6 +1890,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
actual_in
|
||||
kmin
|
||||
kmout
|
||||
qb_multiple_payers
|
||||
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
|
||||
id
|
||||
removed
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
notification,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Alert,
|
||||
Button,
|
||||
Divider,
|
||||
PageHeader,
|
||||
InputNumber,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
notification,
|
||||
PageHeader,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
//import { useHistory } from "react-router-dom";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import moment from "moment";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
|
||||
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component";
|
||||
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
|
||||
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
|
||||
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
|
||||
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
|
||||
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
|
||||
import moment from "moment";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -42,6 +46,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
||||
// const history = useHistory();
|
||||
const [closeJob] = useMutation(UPDATE_JOB);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { Qb_Multi_Ar } = useTreatments(
|
||||
["Qb_Multi_Ar"],
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const handleFinish = async ({ removefromproduction, ...values }) => {
|
||||
setLoading(true);
|
||||
@@ -65,6 +74,9 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
||||
kmout: values.kmout,
|
||||
dms_allocation: values.dms_allocation,
|
||||
...(removefromproduction ? { inproduction: false } : {}),
|
||||
...(values.qb_multiple_payers
|
||||
? { qb_multiple_payers: values.qb_multiple_payers }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"],
|
||||
@@ -127,6 +139,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
||||
kmin: job.kmin,
|
||||
kmout: job.kmout,
|
||||
dms_allocation: job.dms_allocation,
|
||||
qb_multiple_payers: job.qb_multiple_payers,
|
||||
}}
|
||||
scrollToFirstError
|
||||
>
|
||||
@@ -312,6 +325,76 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
{Qb_Multi_Ar.treatment === "on" && (
|
||||
<>
|
||||
<Form.List name={["qb_multiple_payers"]}>
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<Space>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.qb_multiple_payers.name")}
|
||||
key={`${index}name`}
|
||||
name={[field.name, "name"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
style={{ minWidth: "12rem" }}
|
||||
disabled={jobRO}
|
||||
>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("jobs.fields.qb_multiple_payers.amount")}
|
||||
key={`${index}amount`}
|
||||
name={[field.name, "amount"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
<DeleteFilled
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
if (fields.length < 3) add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("jobs.actions.dms.addpayer")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
<JobsCloseLines job={job} />
|
||||
</Form>
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"contents": "Contents",
|
||||
"created": "Time",
|
||||
"operation": "Operation",
|
||||
"status": "Status",
|
||||
"subject": "Subject",
|
||||
"to": "To",
|
||||
"useremail": "User",
|
||||
@@ -835,6 +836,7 @@
|
||||
},
|
||||
"errors": {
|
||||
"deletes3": "Error deleting document from storage. ",
|
||||
"deleting": "Error deleting documents {{error}}",
|
||||
"deleting_cloudinary": "Error deleting document from storage. {{message}}",
|
||||
"getpresignurl": "Error obtaining presigned URL for document. {{message}}",
|
||||
"insert": "Unable to upload file. {{message}}",
|
||||
@@ -1467,6 +1469,10 @@
|
||||
"production_vars": {
|
||||
"note": "Production Note"
|
||||
},
|
||||
"qb_multiple_payers": {
|
||||
"amount": "Amount",
|
||||
"name": "Name"
|
||||
},
|
||||
"queued_for_parts": "Queued for Parts",
|
||||
"rate_ats": "ATS Rate",
|
||||
"rate_la1": "LA1",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"contents": "",
|
||||
"created": "",
|
||||
"operation": "",
|
||||
"status": "",
|
||||
"subject": "",
|
||||
"to": "",
|
||||
"useremail": "",
|
||||
@@ -835,6 +836,7 @@
|
||||
},
|
||||
"errors": {
|
||||
"deletes3": "Error al eliminar el documento del almacenamiento.",
|
||||
"deleting": "",
|
||||
"deleting_cloudinary": "",
|
||||
"getpresignurl": "Error al obtener la URL prescrita para el documento. {{message}}",
|
||||
"insert": "Incapaz de cargar el archivo. {{message}}",
|
||||
@@ -1467,6 +1469,10 @@
|
||||
"production_vars": {
|
||||
"note": ""
|
||||
},
|
||||
"qb_multiple_payers": {
|
||||
"amount": "",
|
||||
"name": ""
|
||||
},
|
||||
"queued_for_parts": "",
|
||||
"rate_ats": "",
|
||||
"rate_la1": "Tarifa LA1",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"contents": "",
|
||||
"created": "",
|
||||
"operation": "",
|
||||
"status": "",
|
||||
"subject": "",
|
||||
"to": "",
|
||||
"useremail": "",
|
||||
@@ -835,6 +836,7 @@
|
||||
},
|
||||
"errors": {
|
||||
"deletes3": "Erreur lors de la suppression du document du stockage.",
|
||||
"deleting": "",
|
||||
"deleting_cloudinary": "",
|
||||
"getpresignurl": "Erreur lors de l'obtention de l'URL présignée pour le document. {{message}}",
|
||||
"insert": "Incapable de télécharger le fichier. {{message}}",
|
||||
@@ -1467,6 +1469,10 @@
|
||||
"production_vars": {
|
||||
"note": ""
|
||||
},
|
||||
"qb_multiple_payers": {
|
||||
"amount": "",
|
||||
"name": ""
|
||||
},
|
||||
"queued_for_parts": "",
|
||||
"rate_ats": "",
|
||||
"rate_la1": "Taux LA1",
|
||||
|
||||
24
hasura/MigrationResolution.md
Normal file
24
hasura/MigrationResolution.md
Normal file
@@ -0,0 +1,24 @@
|
||||
Issue when migrating events resolved by running SQL below. User needed updating when running in prod.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hdb_catalog.hdb_source_catalog_version
|
||||
(
|
||||
version text COLLATE pg_catalog."default" NOT NULL,
|
||||
upgraded_on timestamp with time zone NOT NULL
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE hdb_catalog.hdb_source_catalog_version
|
||||
OWNER to postgres;
|
||||
-- Index: hdb_source_catalog_version_one_row
|
||||
|
||||
-- DROP INDEX hdb_catalog.hdb_source_catalog_version_one_row;
|
||||
|
||||
CREATE UNIQUE INDEX hdb_source_catalog_version_one_row
|
||||
ON hdb_catalog.hdb_source_catalog_version USING btree
|
||||
((version IS NOT NULL) ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
INSERT INTO hdb_catalog.hdb_source_catalog_version (version, upgraded_on) VALUES ('2', NOW());
|
||||
|
||||
https://devscope.io/code/hasura/graphql-engine/issues/8694
|
||||
@@ -1882,17 +1882,19 @@
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- cc
|
||||
- to
|
||||
- contents
|
||||
- subject
|
||||
- useremail
|
||||
- created_at
|
||||
- updated_at
|
||||
- bodyshopid
|
||||
- cc
|
||||
- contents
|
||||
- created_at
|
||||
- id
|
||||
- jobid
|
||||
- noteid
|
||||
- status
|
||||
- status_context
|
||||
- subject
|
||||
- to
|
||||
- updated_at
|
||||
- useremail
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
@@ -3139,6 +3141,7 @@
|
||||
- po_number
|
||||
- policy_no
|
||||
- production_vars
|
||||
- qb_multiple_payers
|
||||
- queued_for_parts
|
||||
- rate_ats
|
||||
- rate_la1
|
||||
@@ -3401,6 +3404,7 @@
|
||||
- po_number
|
||||
- policy_no
|
||||
- production_vars
|
||||
- qb_multiple_payers
|
||||
- queued_for_parts
|
||||
- rate_ats
|
||||
- rate_la1
|
||||
@@ -3673,6 +3677,7 @@
|
||||
- po_number
|
||||
- policy_no
|
||||
- production_vars
|
||||
- qb_multiple_payers
|
||||
- queued_for_parts
|
||||
- rate_ats
|
||||
- rate_la1
|
||||
@@ -3767,8 +3772,6 @@
|
||||
- name: jobs_arms
|
||||
definition:
|
||||
enable_manual: false
|
||||
insert:
|
||||
columns: '*'
|
||||
update:
|
||||
columns:
|
||||
- actual_delivery
|
||||
@@ -3776,9 +3779,9 @@
|
||||
- scheduled_completion
|
||||
- actual_completion
|
||||
- date_scheduled
|
||||
- inproduction
|
||||
- clm_total
|
||||
- suspended
|
||||
- date_open
|
||||
- job_totals
|
||||
- converted
|
||||
- employee_body
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."jobs" add column "qb_multiple_payers" jsonb
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."jobs" add column "qb_multiple_payers" jsonb
|
||||
null;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."email_audit_trail" add column "sesmessageid" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."email_audit_trail" add column "sesmessageid" text
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."email_audit_trail_sesmessageid";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "email_audit_trail_sesmessageid" on
|
||||
"public"."email_audit_trail" using btree ("sesmessageid");
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."email_audit_trail" add column "status" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."email_audit_trail" add column "status" text
|
||||
null;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."email_audit_trail" add column "status_context" jsonb
|
||||
-- null default jsonb_build_array();
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."email_audit_trail" add column "status_context" jsonb
|
||||
null default jsonb_build_array();
|
||||
43065
logs/oAuthClient-log.log
43065
logs/oAuthClient-log.log
File diff suppressed because one or more lines are too long
@@ -64,6 +64,11 @@ app.use(
|
||||
//Email Based Paths.
|
||||
var sendEmail = require("./server/email/sendemail.js");
|
||||
app.post("/sendemail", fb.validateFirebaseIdToken, sendEmail.sendEmail);
|
||||
app.post(
|
||||
"/emailbounce",
|
||||
bodyParser.text(),
|
||||
sendEmail.emailBounce
|
||||
);
|
||||
|
||||
//Test route to ensure Express is responding.
|
||||
app.get("/test", async function (req, res) {
|
||||
@@ -238,7 +243,6 @@ app.get("/", async function (req, res) {
|
||||
res.status(200).send("Access Forbidden.");
|
||||
});
|
||||
|
||||
|
||||
server.listen(port, (error) => {
|
||||
if (error) throw error;
|
||||
logger.log(
|
||||
|
||||
@@ -648,6 +648,56 @@ exports.default = function ({
|
||||
});
|
||||
}
|
||||
|
||||
//Check if there are multiple payers. If there are, add a deduction line and make sure we create new invoices.
|
||||
|
||||
if (
|
||||
jobs_by_pk.qb_multiple_payers &&
|
||||
jobs_by_pk.qb_multiple_payers.length > 0
|
||||
) {
|
||||
jobs_by_pk.qb_multiple_payers.forEach((payer) => {
|
||||
if (qbo) {
|
||||
InvoiceLineAdd.push({
|
||||
DetailType: "SalesItemLineDetail",
|
||||
Amount: Dinero({ amount: (payer.amount || 0) * 100 * -1 }).toFormat(
|
||||
DineroQbFormat
|
||||
),
|
||||
SalesItemLineDetail: {
|
||||
...(jobs_by_pk.class
|
||||
? { ClassRef: { value: classes[jobs_by_pk.class] } }
|
||||
: {}),
|
||||
ItemRef: {
|
||||
value:
|
||||
items[responsibilityCenters.qb_multiple_payers?.accountitem],
|
||||
},
|
||||
Qty: 1,
|
||||
TaxCodeRef: {
|
||||
value:
|
||||
taxCodes[
|
||||
findTaxCode(
|
||||
{
|
||||
local: false,
|
||||
federal: false,
|
||||
state: false,
|
||||
},
|
||||
bodyshop.md_responsibility_centers.sales_tax_codes
|
||||
)
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
InvoiceLineAdd.push({
|
||||
ItemRef: {
|
||||
FullName: responsibilityCenters.qb_multiple_payers?.accountitem,
|
||||
},
|
||||
Desc: `${payer.name} Liability`,
|
||||
Amount: Dinero({ amount: (payer.amount || 0) * 100 * -1 }).toFormat(
|
||||
DineroQbFormat
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return InvoiceLineAdd;
|
||||
};
|
||||
|
||||
@@ -667,3 +717,65 @@ const findTaxCode = ({ local, state, federal }, taxcode) => {
|
||||
}
|
||||
};
|
||||
exports.findTaxCode = findTaxCode;
|
||||
|
||||
exports.createMultiQbPayerLines = function ({
|
||||
bodyshop,
|
||||
jobs_by_pk,
|
||||
qbo = false,
|
||||
items,
|
||||
taxCodes,
|
||||
classes,
|
||||
payer,
|
||||
}) {
|
||||
const InvoiceLineAdd = [];
|
||||
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||
|
||||
const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name.
|
||||
|
||||
if (qbo) {
|
||||
//Going to always assume that we need to apply GST and PST for labor.
|
||||
const taxAccountCode = findTaxCode(
|
||||
{
|
||||
local: false,
|
||||
federal: false,
|
||||
state: false,
|
||||
},
|
||||
bodyshop.md_responsibility_centers.sales_tax_codes
|
||||
);
|
||||
const QboTaxId = taxCodes[taxAccountCode];
|
||||
InvoiceLineAdd.push({
|
||||
DetailType: "SalesItemLineDetail",
|
||||
Amount: Dinero({
|
||||
amount: Math.round((payer.amount || 0) * 100),
|
||||
}).toFormat(DineroQbFormat),
|
||||
SalesItemLineDetail: {
|
||||
...(jobs_by_pk.class
|
||||
? { ClassRef: { value: classes[jobs_by_pk.class] } }
|
||||
: {}),
|
||||
ItemRef: {
|
||||
value: items[responsibilityCenters.qb_multiple_payers?.accountitem],
|
||||
},
|
||||
TaxCodeRef: {
|
||||
value: QboTaxId,
|
||||
},
|
||||
Qty: 1,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
InvoiceLineAdd.push({
|
||||
ItemRef: {
|
||||
FullName: responsibilityCenters.qb_multiple_payers?.accountitem,
|
||||
},
|
||||
Desc: `${payer.name} Liability`,
|
||||
Quantity: 1,
|
||||
Amount: Dinero({
|
||||
amount: Math.round((payer.amount || 0) * 100),
|
||||
}).toFormat(DineroQbFormat),
|
||||
SalesTaxCodeRef: {
|
||||
FullName: "E",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return InvoiceLineAdd;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const moment = require("moment-timezone");
|
||||
|
||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
|
||||
const { createMultiQbPayerLines } = require("../qb-receivables-lines");
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
const oauthClient = new OAuthClient({
|
||||
@@ -115,7 +116,13 @@ exports.default = async (req, res) => {
|
||||
}
|
||||
|
||||
//Query for the Job or Create it.
|
||||
jobTier = await QueryJob(oauthClient, qbo_realmId, req, job);
|
||||
jobTier = await QueryJob(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
isThreeTier ? ownerCustomerTier : null // ownerCustomerTier || insCoCustomerTier
|
||||
);
|
||||
|
||||
// Need to validate that the job tier is associated to the right individual?
|
||||
|
||||
@@ -140,6 +147,65 @@ exports.default = async (req, res) => {
|
||||
jobTier
|
||||
);
|
||||
|
||||
if (job.qb_multiple_payers && job.qb_multiple_payers.length > 0) {
|
||||
for (const [index, payer] of job.qb_multiple_payers.entries()) {
|
||||
//do the thing.
|
||||
|
||||
//Create the source level.
|
||||
let insCoCustomerTier, ownerCustomerTier, jobTier;
|
||||
|
||||
//Insert the insurance company tier.
|
||||
//Query for top level customer, the insurance company name.
|
||||
insCoCustomerTier = await QueryInsuranceCo(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
{ ...job, ins_co_nm: payer.name }
|
||||
);
|
||||
if (!insCoCustomerTier) {
|
||||
//Creating the Insurance Customer.
|
||||
insCoCustomerTier = await InsertInsuranceCo(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
{ ...job, ins_co_nm: payer.name },
|
||||
bodyshop
|
||||
);
|
||||
}
|
||||
//Query for the Job or Create it.
|
||||
jobTier = await QueryJob(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
insCoCustomerTier
|
||||
);
|
||||
// Need to validate that the job tier is associated to the right individual?
|
||||
if (!jobTier) {
|
||||
jobTier = await InsertJob(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
insCoCustomerTier
|
||||
);
|
||||
}
|
||||
|
||||
//Create the RO level
|
||||
|
||||
await InsertInvoiceMultiPayerInvoice(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
bodyshop,
|
||||
jobTier,
|
||||
payer,
|
||||
`-${index + 1}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// //No error. Mark the job exported & insert export log.
|
||||
if (elgen) {
|
||||
const result = await client
|
||||
@@ -212,7 +278,7 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
|
||||
"query",
|
||||
`select * From Customer where DisplayName = '${StandardizeName(
|
||||
job.ins_co_nm.trim()
|
||||
)}'`
|
||||
)}' and Active = true`
|
||||
),
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -284,7 +350,7 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job) {
|
||||
"query",
|
||||
`select * From Customer where DisplayName = '${StandardizeName(
|
||||
ownerName
|
||||
)}'`
|
||||
)}' and Active = true`
|
||||
),
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -348,12 +414,12 @@ async function InsertOwner(
|
||||
}
|
||||
}
|
||||
exports.InsertOwner = InsertOwner;
|
||||
async function QueryJob(oauthClient, qbo_realmId, req, job) {
|
||||
async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||
const result = await oauthClient.makeApiCall({
|
||||
url: urlBuilder(
|
||||
qbo_realmId,
|
||||
"query",
|
||||
`select * From Customer where DisplayName = '${job.ro_number}'`
|
||||
`select * From Customer where DisplayName = '${job.ro_number}' and Active = true`
|
||||
),
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -365,9 +431,14 @@ async function QueryJob(oauthClient, qbo_realmId, req, job) {
|
||||
result.json &&
|
||||
result.json.QueryResponse &&
|
||||
result.json.QueryResponse.Customer &&
|
||||
result.json.QueryResponse.Customer[0]
|
||||
(parentTierRef
|
||||
? result.json.QueryResponse.Customer.find(
|
||||
(x) => x.ParentRef.value === parentTierRef.Id
|
||||
)
|
||||
: result.json.QueryResponse.Customer[0])
|
||||
);
|
||||
}
|
||||
|
||||
exports.QueryJob = QueryJob;
|
||||
async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||
const Customer = {
|
||||
@@ -602,3 +673,137 @@ async function InsertInvoice(
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async function InsertInvoiceMultiPayerInvoice(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
bodyshop,
|
||||
parentTierRef,
|
||||
payer,
|
||||
suffix
|
||||
) {
|
||||
const { items, taxCodes, classes } = await QueryMetaData(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req
|
||||
);
|
||||
const InvoiceLineAdd = createMultiQbPayerLines({
|
||||
bodyshop,
|
||||
jobs_by_pk: job,
|
||||
qbo: true,
|
||||
items,
|
||||
taxCodes,
|
||||
classes,
|
||||
payer,
|
||||
suffix,
|
||||
});
|
||||
|
||||
const invoiceObj = {
|
||||
Line: InvoiceLineAdd,
|
||||
TxnDate: moment(job.date_invoiced)
|
||||
.tz(bodyshop.timezone)
|
||||
.format("YYYY-MM-DD"),
|
||||
DocNumber: job.ro_number + suffix,
|
||||
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
|
||||
CustomerMemo: {
|
||||
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${
|
||||
job.po_number ? `PO No: ${job.po_number}` : ``
|
||||
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
||||
job.v_model_desc || ""
|
||||
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim(),
|
||||
},
|
||||
CustomerRef: {
|
||||
value: parentTierRef.Id,
|
||||
},
|
||||
...(bodyshop.accountingconfig.qbo_departmentid &&
|
||||
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid },
|
||||
}),
|
||||
CustomField: [
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField1
|
||||
? [
|
||||
{
|
||||
DefinitionId: "1",
|
||||
StringValue:
|
||||
job[bodyshop.accountingconfig.ReceivableCustomField1],
|
||||
Type: "StringType",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField2
|
||||
? [
|
||||
{
|
||||
DefinitionId: "2",
|
||||
StringValue:
|
||||
job[bodyshop.accountingconfig.ReceivableCustomField2],
|
||||
Type: "StringType",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField3
|
||||
? [
|
||||
{
|
||||
DefinitionId: "3",
|
||||
StringValue:
|
||||
job[bodyshop.accountingconfig.ReceivableCustomField3],
|
||||
Type: "StringType",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
...(bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
bodyshop.accountingconfig.qbo_usa &&
|
||||
bodyshop.region_config.includes("CA_") && {
|
||||
TxnTaxDetail: {
|
||||
TxnTaxCodeRef: {
|
||||
value:
|
||||
taxCodes[
|
||||
bodyshop.md_responsibility_centers.taxes.state.accountitem
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
...(bodyshop.accountingconfig.printlater
|
||||
? { PrintStatus: "NeedToPrint" }
|
||||
: {}),
|
||||
...(bodyshop.accountingconfig.emaillater && job.ownr_ea
|
||||
? { EmailStatus: "NeedToSend" }
|
||||
: {}),
|
||||
BillAddr: {
|
||||
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${
|
||||
job.ownr_zip || ""
|
||||
}`.trim(),
|
||||
Line2: job.ownr_addr1 || "",
|
||||
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
|
||||
job.ownr_co_nm || ""
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {
|
||||
invoiceObj,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await oauthClient.makeApiCall({
|
||||
url: urlBuilder(qbo_realmId, "invoice"),
|
||||
method: "POST",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(invoiceObj),
|
||||
});
|
||||
setNewRefreshToken(req.user.email, result);
|
||||
return result && result.json && result.json.Invoice;
|
||||
} catch (error) {
|
||||
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
|
||||
error,
|
||||
method: "InsertOwner",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ function pollFunc(fn, timeout, interval) {
|
||||
pollFunc(getEntegralShopData, 0, 5 * 60 * 1000); //Set the metadata to refresh every 5 minutes.
|
||||
|
||||
async function getEntegralShopData() {
|
||||
const { bodyshops } = await client.request(queries.GET_ENTEGRAL_SHOPS);
|
||||
await storage.init({ logging: true });
|
||||
const { bodyshops } = await client.request(queries.GET_ENTEGRAL_SHOPS);
|
||||
logger.log("set-entegral-shops-local-storage", "DEBUG", "API", null, null);
|
||||
await storage.setItem("entegralShops", bodyshops);
|
||||
return true; //Continue execution.
|
||||
@@ -866,12 +866,16 @@ exports.default = async (req, res) => {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = entegralResponse;
|
||||
} catch (error) {
|
||||
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||
logger.log("arms-failed-job-upload", "ERROR", "api", job.shopid, {
|
||||
job: JSON.stringify({ id: job.id, ro_number: job.ro_number }),
|
||||
error: error.message || JSON.stringify(error),
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("arms-failed-job", "ERROR", "api", job.shopid, {
|
||||
job: JSON.stringify({ id: job.id, ro_number: job.ro_number }),
|
||||
error: error.message || JSON.stringify(error),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ exports.sendEmail = async (req, res) => {
|
||||
to: req.body.to,
|
||||
cc: req.body.cc,
|
||||
subject: req.body.subject,
|
||||
messageId: info.messageId,
|
||||
});
|
||||
res.json({
|
||||
success: true, //response: info
|
||||
@@ -190,6 +191,8 @@ async function logEmail(req, email) {
|
||||
useremail: req.user.email,
|
||||
contents: req.body.html,
|
||||
jobid: req.body.jobid,
|
||||
sesmessageid: email.messageId,
|
||||
status: "Sent",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -202,3 +205,59 @@ async function logEmail(req, email) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.emailBounce = async function (req, res, next) {
|
||||
try {
|
||||
const body = JSON.parse(req.body);
|
||||
if (body.type === "SubscriptionConfirmation") {
|
||||
logger.log("SNS-confirmation", "DEBUG", "api", null, {
|
||||
message: body.message,
|
||||
url: body.SubscribeUrl,
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
if (body.Type === "Notification") {
|
||||
const message = JSON.parse(body.Message);
|
||||
let replyTo, subject, messageId;
|
||||
message.mail.headers.forEach((header) => {
|
||||
if (header.name === "Reply-To") {
|
||||
replyTo = header.value;
|
||||
} else if (header.name === "Subject") {
|
||||
subject = header.value;
|
||||
} else if (header.name === "Message-ID") {
|
||||
messageId = header.value;
|
||||
}
|
||||
});
|
||||
//If it's bounced, log it as bounced in audit log. Send an email to the user.
|
||||
const result = await client.request(queries.UPDATE_EMAIL_AUDIT, {
|
||||
sesid: messageId,
|
||||
status: "Bounced",
|
||||
context: message.bounce?.bouncedRecipients,
|
||||
});
|
||||
transporter.sendMail(
|
||||
{
|
||||
from: `ImEX Online <noreply@imex.online>`,
|
||||
to: "patrick@snapt.ca", // replyTo,
|
||||
bcc: "patrick@snapt.ca",
|
||||
subject: `ImEX Online Bounced Email - RE: ${subject}`,
|
||||
text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
|
||||
|
||||
${message.bounce?.bouncedRecipients.map(
|
||||
(r) =>
|
||||
`Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode}
|
||||
`
|
||||
)}
|
||||
`,
|
||||
},
|
||||
(err, info) => {
|
||||
console.log("***", err || info);
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("sns-error", "ERROR", "api", null, {
|
||||
error: JSON.stringify(error),
|
||||
});
|
||||
}
|
||||
res.sendStatus(200);
|
||||
};
|
||||
|
||||
@@ -191,6 +191,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
|
||||
storage_payable
|
||||
adjustment_bottom_line
|
||||
state_tax_rate
|
||||
qb_multiple_payers
|
||||
owner {
|
||||
accountingid
|
||||
}
|
||||
@@ -1611,3 +1612,22 @@ exports.INSERT_EMAIL_AUDIT = `mutation INSERT_EMAIL_AUDIT($email: email_audit_tr
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports.DELETE_MEDIA_DOCUMENTS = `
|
||||
mutation DELETE_DOCUMENTS($ids: [uuid!]!) {
|
||||
delete_documents(where: { id: { _in: $ids } }) {
|
||||
returning {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports.UPDATE_EMAIL_AUDIT = `
|
||||
mutation ($sesid: String!, $status: String, $context: jsonb) {
|
||||
update_email_audit_trail(where: {sesmessageid: {_eq: $sesid}}, _set: {status: $status, status_context: $context}) {
|
||||
returning {
|
||||
contents
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const path = require("path");
|
||||
const _ = require("lodash");
|
||||
const logger = require("../utils/logger");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const queries = require("../graphql-client/queries");
|
||||
|
||||
require("dotenv").config({
|
||||
path: path.resolve(
|
||||
@@ -69,11 +71,38 @@ exports.deleteFiles = async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
res.send(returns);
|
||||
// Delete it on apollo.
|
||||
const successfulDeletes = [];
|
||||
returns.forEach((resType) => {
|
||||
Object.keys(resType.deleted).forEach((key) => {
|
||||
if (
|
||||
resType.deleted[key] === "deleted" ||
|
||||
resType.deleted[key] === "not_found"
|
||||
) {
|
||||
successfulDeletes.push(key.replace(/\.[^/.]+$/, ""));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await client.request(queries.DELETE_MEDIA_DOCUMENTS, {
|
||||
ids: ids
|
||||
.filter((i) => successfulDeletes.includes(i.key))
|
||||
.map((i) => i.id),
|
||||
});
|
||||
|
||||
res.send({ returns, result });
|
||||
} catch (error) {
|
||||
logger.log("media-delete-error", "ERROR", req.user.email, null, [
|
||||
{ ids, error: error.message || JSON.stringify(error) },
|
||||
]);
|
||||
|
||||
res.json({ error });
|
||||
}
|
||||
};
|
||||
|
||||
exports.renameKeys = async (req, res) => {
|
||||
const { documents } = req.body;
|
||||
const { documents, tojobid } = req.body;
|
||||
logger.log("media-bulk-rename", "DEBUG", req.user.email, null, documents);
|
||||
|
||||
const proms = [];
|
||||
@@ -98,8 +127,37 @@ exports.renameKeys = async (req, res) => {
|
||||
let result;
|
||||
|
||||
result = await Promise.all(proms);
|
||||
const errors = [];
|
||||
result
|
||||
.filter((d) => d.error)
|
||||
.forEach((d) => {
|
||||
errors.push(d);
|
||||
});
|
||||
|
||||
res.send(result);
|
||||
let mutations = "";
|
||||
|
||||
result
|
||||
.filter((d) => !d.error)
|
||||
.forEach((d, idx) => {
|
||||
//Create mutation text
|
||||
|
||||
mutations =
|
||||
mutations +
|
||||
`
|
||||
update_doc${idx}:update_documents_by_pk(pk_columns: { id: "${d.id}" }, _set: {key: "${d.public_id}", jobid: "${tojobid}"}){
|
||||
id
|
||||
}
|
||||
`;
|
||||
});
|
||||
|
||||
if (mutations !== "") {
|
||||
const mutationResult = await client.request(`mutation {
|
||||
${mutations}
|
||||
}`);
|
||||
res.json({ errors, mutationResult });
|
||||
} else {
|
||||
res.json({ errors: "No images were succesfully moved on remote server. " });
|
||||
}
|
||||
};
|
||||
|
||||
//Also needs to be updated in upload utility and mobile app.
|
||||
|
||||
@@ -24,9 +24,8 @@ exports.mixdataUpload = async (req, res) => {
|
||||
});
|
||||
|
||||
try {
|
||||
req.files.forEach(async (element) => {
|
||||
for (const element of req.files) {
|
||||
const b = Buffer.from(element.buffer);
|
||||
console.log(b.toString());
|
||||
|
||||
const inboundRequest = await xml2js.parseStringPromise(b.toString(), {
|
||||
explicitArray: false,
|
||||
@@ -59,23 +58,24 @@ exports.mixdataUpload = async (req, res) => {
|
||||
ScaleType,
|
||||
jobHash
|
||||
);
|
||||
|
||||
const foundJobs = MixDataArray.filter((m) => m.jobid);
|
||||
const MixDataQuery = `
|
||||
mutation UPSERT_MIXDATA{
|
||||
${MixDataArray.map((md, idx) =>
|
||||
GenerateGqlForMixData(md, idx)
|
||||
).join(" ")}
|
||||
${foundJobs
|
||||
.map((md, idx) => GenerateGqlForMixData(md, idx))
|
||||
.join(" ")}
|
||||
}
|
||||
`;
|
||||
|
||||
const resp = await client.request(MixDataQuery);
|
||||
if (foundJobs.length > 1) {
|
||||
const resp = await client.request(MixDataQuery);
|
||||
}
|
||||
|
||||
//Process the list of ROs and return an object to generate the queries.
|
||||
}
|
||||
});
|
||||
}
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
res.status(500).JSON(error);
|
||||
res.status(500).json(error);
|
||||
logger.log("job-mixdata-upload-error", "ERROR", null, null, {
|
||||
error: error.message,
|
||||
...error,
|
||||
@@ -98,7 +98,7 @@ function DetermineScaleType(inboundRequest) {
|
||||
|
||||
function GetListOfRos(inboundRequest, ScaleType) {
|
||||
if (ScaleType.company === "PPG" && ScaleType.version === "1.3.0") {
|
||||
return inboundRequest.PPG.DataExportInterface.ROData.RepairOrders.RO.map(
|
||||
return inboundRequest.PPG.MixDataInterface.ROData.RepairOrders.RO.map(
|
||||
(r) => r.RONumber
|
||||
);
|
||||
}
|
||||
@@ -106,11 +106,11 @@ function GetListOfRos(inboundRequest, ScaleType) {
|
||||
|
||||
function GenerateMixDataArray(inboundRequest, ScaleType, jobHash) {
|
||||
if (ScaleType.company === "PPG" && ScaleType.version === "1.3.0") {
|
||||
return inboundRequest.PPG.DataExportInterface.ROData.RepairOrders.RO.map(
|
||||
return inboundRequest.PPG.MixDataInterface.ROData.RepairOrders.RO.map(
|
||||
(r) => {
|
||||
return {
|
||||
jobid: jobHash[r.RONumber].jobid,
|
||||
id: jobHash[r.RONumber].mixdataid,
|
||||
jobid: jobHash[r.RONumber]?.jobid,
|
||||
id: jobHash[r.RONumber]?.mixdataid,
|
||||
mixdata: r,
|
||||
totalliquidcost: r.TotalLiquidCost,
|
||||
totalsundrycost: r.TotalSundryCost,
|
||||
|
||||
Reference in New Issue
Block a user