Merged in release/2022-09-23 (pull request #582)

Release/2022 09 23
This commit is contained in:
Patrick Fic
2022-09-23 20:36:42 +00:00
35 changed files with 43935 additions and 134 deletions

View File

@@ -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>

View File

@@ -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",

View File

@@ -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.")
? [
{

View File

@@ -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);
};

View File

@@ -125,7 +125,10 @@ function JobsDocumentsComponent({
deletionCallback={billsCallback || refetch}
/>
{!billId && (
<JobsDocumentsGalleryReassign galleryImages={galleryImages} />
<JobsDocumentsGalleryReassign
galleryImages={galleryImages}
callback={refetch}
/>
)}
</Space>
</Col>

View File

@@ -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 {

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View 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

View File

@@ -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

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "qb_multiple_payers" jsonb
null;

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
alter table "public"."email_audit_trail" add column "sesmessageid" text
null;

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."email_audit_trail_sesmessageid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "email_audit_trail_sesmessageid" on
"public"."email_audit_trail" using btree ("sesmessageid");

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
alter table "public"."email_audit_trail" add column "status" text
null;

View File

@@ -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();

View File

@@ -0,0 +1,2 @@
alter table "public"."email_audit_trail" add column "status_context" jsonb
null default jsonb_build_array();

File diff suppressed because one or more lines are too long

View File

@@ -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(

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -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),
});
}

View File

@@ -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);
};

View File

@@ -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
}
}
}`;

View File

@@ -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.

View File

@@ -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,