diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel
index 9348a53a0..e4549e29b 100644
--- a/bodyshop_translations.babel
+++ b/bodyshop_translations.babel
@@ -8229,6 +8229,27 @@
+
+ qbo
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
rbac
false
diff --git a/client/src/assets/C2QB_composite_English.svg b/client/src/assets/C2QB_composite_English.svg
new file mode 100644
index 000000000..d5e302253
--- /dev/null
+++ b/client/src/assets/C2QB_composite_English.svg
@@ -0,0 +1,24 @@
+
diff --git a/client/src/assets/C2QB_transparent_English.svg b/client/src/assets/C2QB_transparent_English.svg
new file mode 100644
index 000000000..d5e302253
--- /dev/null
+++ b/client/src/assets/C2QB_transparent_English.svg
@@ -0,0 +1,24 @@
+
diff --git a/client/src/assets/qbo/C2QB_green_btn_med_default.svg b/client/src/assets/qbo/C2QB_green_btn_med_default.svg
new file mode 100644
index 000000000..5777594bf
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_green_btn_med_default.svg
@@ -0,0 +1,4 @@
+
diff --git a/client/src/assets/qbo/C2QB_green_btn_med_hover.svg b/client/src/assets/qbo/C2QB_green_btn_med_hover.svg
new file mode 100644
index 000000000..4495659ad
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_green_btn_med_hover.svg
@@ -0,0 +1,5 @@
+
diff --git a/client/src/assets/qbo/C2QB_green_btn_short_default.svg b/client/src/assets/qbo/C2QB_green_btn_short_default.svg
new file mode 100644
index 000000000..f5e02d040
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_green_btn_short_default.svg
@@ -0,0 +1,4 @@
+
diff --git a/client/src/assets/qbo/C2QB_green_btn_short_hover.svg b/client/src/assets/qbo/C2QB_green_btn_short_hover.svg
new file mode 100644
index 000000000..41c42a246
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_green_btn_short_hover.svg
@@ -0,0 +1,5 @@
+
diff --git a/client/src/assets/qbo/C2QB_green_btn_tall_default.svg b/client/src/assets/qbo/C2QB_green_btn_tall_default.svg
new file mode 100644
index 000000000..d93a0e482
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_green_btn_tall_default.svg
@@ -0,0 +1,4 @@
+
diff --git a/client/src/assets/qbo/C2QB_green_btn_tall_hover.svg b/client/src/assets/qbo/C2QB_green_btn_tall_hover.svg
new file mode 100644
index 000000000..78e4f7670
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_green_btn_tall_hover.svg
@@ -0,0 +1,5 @@
+
diff --git a/client/src/assets/qbo/C2QB_transparent_btn_med_default.svg b/client/src/assets/qbo/C2QB_transparent_btn_med_default.svg
new file mode 100644
index 000000000..575057b1c
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_transparent_btn_med_default.svg
@@ -0,0 +1,4 @@
+
diff --git a/client/src/assets/qbo/C2QB_transparent_btn_med_hover.svg b/client/src/assets/qbo/C2QB_transparent_btn_med_hover.svg
new file mode 100644
index 000000000..f4bab4f04
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_transparent_btn_med_hover.svg
@@ -0,0 +1,5 @@
+
diff --git a/client/src/assets/qbo/C2QB_transparent_btn_short_default.svg b/client/src/assets/qbo/C2QB_transparent_btn_short_default.svg
new file mode 100644
index 000000000..d1c15abeb
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_transparent_btn_short_default.svg
@@ -0,0 +1,4 @@
+
diff --git a/client/src/assets/qbo/C2QB_transparent_btn_short_hover.svg b/client/src/assets/qbo/C2QB_transparent_btn_short_hover.svg
new file mode 100644
index 000000000..ab88da614
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_transparent_btn_short_hover.svg
@@ -0,0 +1,5 @@
+
diff --git a/client/src/assets/qbo/C2QB_transparent_btn_tall_default.svg b/client/src/assets/qbo/C2QB_transparent_btn_tall_default.svg
new file mode 100644
index 000000000..66d56b3ff
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_transparent_btn_tall_default.svg
@@ -0,0 +1,4 @@
+
diff --git a/client/src/assets/qbo/C2QB_transparent_btn_tall_hover.svg b/client/src/assets/qbo/C2QB_transparent_btn_tall_hover.svg
new file mode 100644
index 000000000..d8319ae93
--- /dev/null
+++ b/client/src/assets/qbo/C2QB_transparent_btn_tall_hover.svg
@@ -0,0 +1,5 @@
+
diff --git a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx
index d7f57388e..ea9e5517b 100644
--- a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx
+++ b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx
@@ -11,6 +11,7 @@ import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-butto
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
+import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -206,6 +207,9 @@ export function AccountingReceivablesTableComponent({
completedCallback={setSelectedJobs}
/>
)}
+ {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
+
+ )}
{
+ //Check if it's a CDK setup.
if (bodyshop.cdk_dealerid) {
history.push(`/manage/dms?jobId=${jobId}`);
return;
@@ -41,48 +42,58 @@ export function JobsCloseExportButton({
logImEXEvent("jobs_close_export");
setLoading(true);
- let QbXmlResponse;
- try {
- QbXmlResponse = await axios.post(
- "/accounting/qbxml/receivables",
- { jobIds: [jobId] },
- {
- headers: {
- Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
- },
- }
- );
- console.log("handle -> XML", QbXmlResponse);
- } catch (error) {
- console.log("Error getting QBXML from Server.", error);
- notification["error"]({
- message: t("jobs.errors.exporting", {
- error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
- }),
- });
- setLoading(false);
- return;
- }
-
+ //Check if it's a QBO Setup.
let PartnerResponse;
- try {
- PartnerResponse = await axios.post(
- "http://localhost:1337/qb/",
- // "http://609feaeae986.ngrok.io/qb/",
- QbXmlResponse.data,
- {
- headers: {
- Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
- },
- }
- );
- } catch (error) {
- console.log("Error connecting to quickbooks or partner.", error);
- notification["error"]({
- message: t("jobs.errors.exporting-partner"),
+ if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
+ PartnerResponse = await axios.post(`/qbo/receivables`, {
+ withCredentials: true,
+ jobIds: [jobId],
});
- setLoading(false);
- return;
+ } else {
+ //Default is QBD
+
+ let QbXmlResponse;
+ try {
+ QbXmlResponse = await axios.post(
+ "/accounting/qbxml/receivables",
+ { jobIds: [jobId] },
+ {
+ headers: {
+ Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
+ },
+ }
+ );
+ console.log("handle -> XML", QbXmlResponse);
+ } catch (error) {
+ console.log("Error getting QBXML from Server.", error);
+ notification["error"]({
+ message: t("jobs.errors.exporting", {
+ error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
+ }),
+ });
+ setLoading(false);
+ return;
+ }
+
+ try {
+ PartnerResponse = await axios.post(
+ "http://localhost:1337/qb/",
+ // "http://609feaeae986.ngrok.io/qb/",
+ QbXmlResponse.data,
+ {
+ headers: {
+ Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
+ },
+ }
+ );
+ } catch (error) {
+ console.log("Error connecting to quickbooks or partner.", error);
+ notification["error"]({
+ message: t("jobs.errors.exporting-partner"),
+ });
+ setLoading(false);
+ return;
+ }
}
console.log("PartnerResponse", PartnerResponse);
diff --git a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
index e0be5621c..69b15dcfc 100644
--- a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
+++ b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
@@ -34,53 +34,61 @@ export function JobsExportAllButton({
const [loading, setLoading] = useState(false);
const handleQbxml = async () => {
logImEXEvent("jobs_export_all");
-
- setLoading(true);
- let QbXmlResponse;
- try {
- QbXmlResponse = await axios.post(
- "/accounting/qbxml/receivables",
- { jobIds: jobIds },
- {
- headers: {
- Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
- },
- }
- );
- } catch (error) {
- console.log("Error getting QBXML from Server.", error);
- notification["error"]({
- message: t("jobs.errors.exporting", {
- error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
- }),
- });
- setLoading(false);
- return;
- }
-
let PartnerResponse;
- try {
- PartnerResponse = await axios.post(
- "http://localhost:1337/qb/",
- // "http://609feaeae986.ngrok.io/qb/",
- QbXmlResponse.data,
- {
- headers: {
- Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
- },
- }
- );
- } catch (error) {
- console.log("Error connecting to quickbooks or partner.", error);
- notification["error"]({
- message: t("jobs.errors.exporting-partner"),
+ setLoading(true);
+ if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
+ PartnerResponse = await axios.post(`/qbo/receivables`, {
+ withCredentials: true,
+ jobIds: jobIds,
});
- setLoading(false);
- return;
- }
+ } else {
+ let QbXmlResponse;
+ try {
+ QbXmlResponse = await axios.post(
+ "/accounting/qbxml/receivables",
+ { jobIds: jobIds },
+ {
+ headers: {
+ Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
+ },
+ }
+ );
+ } catch (error) {
+ console.log("Error getting QBXML from Server.", error);
+ notification["error"]({
+ message: t("jobs.errors.exporting", {
+ error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
+ }),
+ });
+ setLoading(false);
+ return;
+ }
+ try {
+ PartnerResponse = await axios.post(
+ "http://localhost:1337/qb/",
+ // "http://609feaeae986.ngrok.io/qb/",
+ QbXmlResponse.data,
+ {
+ headers: {
+ Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
+ },
+ }
+ );
+ } catch (error) {
+ console.log("Error connecting to quickbooks or partner.", error);
+ notification["error"]({
+ message: t("jobs.errors.exporting-partner"),
+ });
+ setLoading(false);
+ return;
+ }
+ }
console.log("PartnerResponse", PartnerResponse);
- const groupedData = _.groupBy(PartnerResponse.data, "id");
+ const groupedData = _.groupBy(
+ PartnerResponse.data,
+ bodyshop.accountingconfig.qbo ? "jobid" : "id"
+ );
await Promise.all(
Object.keys(groupedData).map(async (key) => {
@@ -157,6 +165,7 @@ export function JobsExportAllButton({
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
+
setLoading(false);
};
diff --git a/client/src/components/qbo-authorize/qbo-authorize.component.jsx b/client/src/components/qbo-authorize/qbo-authorize.component.jsx
index 839466f70..682e5ebe0 100644
--- a/client/src/components/qbo-authorize/qbo-authorize.component.jsx
+++ b/client/src/components/qbo-authorize/qbo-authorize.component.jsx
@@ -1,19 +1,19 @@
-import { Button, Space } from "antd";
+import { Space, Tag } from "antd";
import Axios from "axios";
-import React, { useEffect } from "react";
-//import QboImg from "./qbo_signin.png";
import queryString from "query-string";
-import { useLocation } from "react-router-dom";
+import React, { useEffect } from "react";
import { useCookies } from "react-cookie";
+import { useHistory, useLocation } from "react-router-dom";
+import QboSignIn from "../../assets/qbo/C2QB_green_btn_med_default.svg";
+import "./qbo-authorize.scss";
export default function QboAuthorizeComponent() {
const location = useLocation();
-
- const [, setCookie] = useCookies(["access_token", "refresh_token"]);
+ const history = useHistory();
+ const [cookies, setCookie] = useCookies(["access_token", "refresh_token"]);
const handleQbSignIn = async () => {
const result = await Axios.post("/qbo/authorize");
- console.log("pushing to history", result.data);
window.location.href = result.data;
};
const qs = queryString.parse(location.search);
@@ -35,42 +35,24 @@ export default function QboAuthorizeComponent() {
path: "/",
expires,
});
+
+ history.push({ pathname: `/manage/accounting/receivables` });
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [qs, location, setCookie]);
return (
-
-
-
-
-
-
+
+
+ {!cookies.qbo_realmId && (
+ No QuickBooks company has been connected.
+ )}
{error && JSON.parse(decodeURIComponent(error)).error_description}
-
+
);
}
diff --git a/client/src/components/qbo-authorize/qbo-authorize.scss b/client/src/components/qbo-authorize/qbo-authorize.scss
new file mode 100644
index 000000000..b4016ae80
--- /dev/null
+++ b/client/src/components/qbo-authorize/qbo-authorize.scss
@@ -0,0 +1,8 @@
+.qbo-sign-in {
+ background-image: url("../../assets/qbo/C2QB_green_btn_med_default.svg");
+ width: 274px;
+ height: 48px;
+ &:hover {
+ background-image: url("../../assets/qbo/C2QB_green_btn_med_hover.svg");
+ }
+}
diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx
index 6aa952218..1ab1e13f5 100644
--- a/client/src/components/shop-info/shop-info.general.component.jsx
+++ b/client/src/components/shop-info/shop-info.general.component.jsx
@@ -121,6 +121,13 @@ export default function ShopInfoGeneral({ form }) {
+
+
+
{
Authorization: BearerToken,
},
});
- logger.log("qbo-payable-create", "DEBUG", req.user.email, jobIds);
+ logger.log("qbo-receivable-create", "DEBUG", req.user.email, jobIds);
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, {
- ids: ["966dc7f9-2acd-44dc-9df5-d07c5578070a"],
- //jobIds
+ ids: jobIds,
});
const { jobs, bodyshops } = result;
-
- const job = jobs[0];
const bodyshop = bodyshops[0];
- const isThreeTier = bodyshop.accountingconfig.tiers === 3;
- const twoTierPref = bodyshop.accountingconfig.twotierpref;
- //Replace this with a for-each loop to check every single Job that's included in the list.
+ const ret = [];
+ for (const job of jobs) {
+ //const job = jobs[0];
+ try {
+ const isThreeTier = bodyshop.accountingconfig.tiers === 3;
+ const twoTierPref = bodyshop.accountingconfig.twotierpref;
- let insCoCustomerTier, ownerCustomerTier, jobTier;
- if (isThreeTier || twoTierPref === "source") {
- //Insert the insurance company tier.
- //Query for top level customer, the insurance company name.
- insCoCustomerTier = await QueryInsuranceCo(oauthClient, req, job);
- if (!insCoCustomerTier) {
- //Creating the Insurance Customer.
- insCoCustomerTier = await InsertInsuranceCo(
- oauthClient,
- req,
- job,
- bodyshop
- );
+ //Replace this with a for-each loop to check every single Job that's included in the list.
+
+ let insCoCustomerTier, ownerCustomerTier, jobTier;
+ if (isThreeTier || twoTierPref === "source") {
+ //Insert the insurance company tier.
+ //Query for top level customer, the insurance company name.
+ insCoCustomerTier = await QueryInsuranceCo(oauthClient, req, job);
+ if (!insCoCustomerTier) {
+ //Creating the Insurance Customer.
+ insCoCustomerTier = await InsertInsuranceCo(
+ oauthClient,
+ req,
+ job,
+ bodyshop
+ );
+ }
+ }
+ console.log(insCoCustomerTier);
+ if (isThreeTier || twoTierPref === "name") {
+ //Insert the name/owner and account for whether the source should be the ins co in 3 tier..
+ ownerCustomerTier = await QueryOwner(oauthClient, req, job);
+ //Query for the owner itself.
+ if (!ownerCustomerTier) {
+ ownerCustomerTier = await InsertOwner(
+ oauthClient,
+ req,
+ job,
+ isThreeTier,
+ insCoCustomerTier
+ );
+ }
+ }
+ console.log(ownerCustomerTier);
+ //Query for the Job or Create it.
+ jobTier = await QueryJob(oauthClient, req, job);
+
+ // Need to validate that the job tier is associated to the right individual?
+
+ if (!jobTier) {
+ jobTier = await InsertJob(
+ oauthClient,
+ req,
+ job,
+ isThreeTier,
+ ownerCustomerTier
+ );
+ }
+ console.log(jobTier);
+ await InsertInvoice(oauthClient, req, job, bodyshop, jobTier);
+ ret.push({ jobid: job.id, success: true });
+ } catch (error) {
+ ret.push({
+ jobid: job.id,
+ success: false,
+ errorMessage: error.message,
+ });
}
}
- if (isThreeTier || twoTierPref === "name") {
- //Insert the name/owner and account for whether the source should be the ins co in 3 tier..
- ownerCustomerTier = await QueryOwner(oauthClient, req, job);
- //Query for the owner itself.
- if (!ownerCustomerTier) {
- ownerCustomerTier = await InsertOwner(
- oauthClient,
- req,
- job,
- isThreeTier,
- insCoCustomerTier
- );
- }
- }
- //Query for the Job or Create it.
- jobTier = await QueryJob(oauthClient, req, job);
-
- // Need to validate that the job tier is associated to the right individual?
-
- if (!jobTier) {
- jobTier = await InsertJob(
- oauthClient,
- req,
- job,
- isThreeTier,
- ownerCustomerTier
- );
- }
- await InsertInvoice(oauthClient, req, job, bodyshop, jobTier);
- res.sendStatus(200);
+ res.status(200).json(ret);
} catch (error) {
console.log(error);
- logger.log("qbo-payable-create-error", "ERROR", req.user.email, { error });
+ logger.log("qbo-receivable-create-error", "ERROR", req.user.email, {
+ error,
+ });
res.status(400).json(error);
}
};
@@ -168,7 +182,7 @@ async function InsertInsuranceCo(oauthClient, req, job, bodyshop) {
body: JSON.stringify(Customer),
});
setNewRefreshToken(req.user.email, result);
- return result && result.Customer;
+ return result && result.json.Customer;
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
@@ -230,7 +244,7 @@ async function InsertOwner(oauthClient, req, job, isThreeTier, parentTierRef) {
body: JSON.stringify(Customer),
});
setNewRefreshToken(req.user.email, result);
- return result && result.Customer;
+ return result && result.json.Customer;
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
@@ -290,7 +304,7 @@ async function InsertJob(oauthClient, req, job, isThreeTier, parentTierRef) {
body: JSON.stringify(Customer),
});
setNewRefreshToken(req.user.email, result);
- return result && result.Customer;
+ return result && result.json.Customer;
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
@@ -319,14 +333,6 @@ async function QueryMetaData(oauthClient, req) {
const taxCodeMapping = {};
- const accounts = await oauthClient.makeApiCall({
- url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Account`),
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- });
-
taxCodes.json &&
taxCodes.json.QueryResponse &&
taxCodes.json.QueryResponse.TaxCode.forEach((t) => {