IO-256 Export receivables for QBO

This commit is contained in:
Patrick Fic
2021-09-24 16:43:57 -07:00
parent cf91ec14c0
commit 8f2b1f0f78
26 changed files with 340 additions and 190 deletions

View File

@@ -8229,6 +8229,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>qbo</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>rbac</name>
<definition_loaded>false</definition_loaded>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -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 && (
<QboAuthorizeComponent />
)}
<Input.Search
value={state.search}
onChange={handleSearch}

View File

@@ -34,6 +34,7 @@ export function JobsCloseExportButton({
const [loading, setLoading] = useState(false);
const handleQbxml = async () => {
//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);

View File

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

View File

@@ -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 (
<div>
<Space wrap>
<Button onClick={handleQbSignIn}>
{/* <img
src={QboImg}
alt="Sign in with Intuit"
onClick={handleQbSignIn}
/> */}
auth
</Button>
<Button
onClick={async () => {
const response = await Axios.get(`/qbo/refresh`, {
withCredentials: true,
});
console.log(response);
}}
>
Refresh Token
</Button>
<Button
onClick={async () => {
const response = await Axios.post(`/qbo/receivables`, {
withCredentials: true,
});
console.log(response);
}}
>
REC
</Button>
</Space>
<Space>
<img
onClick={handleQbSignIn}
alt="Sign In to QuickBooks Online"
src={QboSignIn}
style={{ cursor: "pointer" }}
/>
{!cookies.qbo_realmId && (
<Tag color="red">No QuickBooks company has been connected.</Tag>
)}
{error && JSON.parse(decodeURIComponent(error)).error_description}
</div>
</Space>
);
}

View File

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

View File

@@ -121,6 +121,13 @@ export default function ShopInfoGeneral({ form }) {
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")}>
<Form.Item
label={t("bodyshop.labels.qbo")}
valuePropName="checked"
name={["accountingconfig", "qbo"]}
>
<Switch />
</Form.Item>
<Form.Item
label={t("bodyshop.labels.accountingtiers")}
rules={[

View File

@@ -509,6 +509,7 @@
"orderstatuses": "Order Statuses",
"partslocations": "Parts Locations",
"printlater": "Print Later",
"qbo": "Use QuickBooks Online?",
"rbac": "Role Based Access Control",
"responsibilitycenters": {
"costs": "Cost Centers",

View File

@@ -509,6 +509,7 @@
"orderstatuses": "",
"partslocations": "",
"printlater": "",
"qbo": "",
"rbac": "",
"responsibilitycenters": {
"costs": "",

View File

@@ -509,6 +509,7 @@
"orderstatuses": "",
"partslocations": "",
"printlater": "",
"qbo": "",
"rbac": "",
"responsibilitycenters": {
"costs": "",

View File

@@ -7,9 +7,6 @@ require("dotenv").config({
});
const logger = require("../../utils/logger");
const OAuthClient = require("intuit-oauth");
var QuickBooks = require("node-quickbooks");
const Promise = require("bluebird");
const QuickBooksPromise = Promise.promisifyAll(QuickBooks.prototype);
const client = require("../../graphql-client/graphql-client").client;
const queries = require("../../graphql-client/queries");
const queryString = require("query-string");

View File

@@ -7,8 +7,6 @@ require("dotenv").config({
),
});
const logger = require("../../utils/logger");
const Dinero = require("dinero.js");
const DineroQbFormat = require("../accounting-constants").DineroQbFormat;
const apiGqlClient = require("../../graphql-client/graphql-client").client;
const queries = require("../../graphql-client/queries");
const {
@@ -48,71 +46,87 @@ exports.default = async (req, res) => {
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) => {