Merge master into feature/local-images

This commit is contained in:
Patrick Fic
2022-05-09 08:26:43 -07:00
41 changed files with 2092 additions and 350 deletions

View File

@@ -8927,6 +8927,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>md_to_emails</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>md_to_emails_emails</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> <concept_node>
<name>messagingpresets</name> <name>messagingpresets</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -14017,6 +14059,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>external_id</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> <concept_node>
<name>first_name</name> <name>first_name</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -36695,6 +36758,32 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node>
<name>special</name>
<children>
<concept_node>
<name>attendance_detail_csv</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>
<folder_node> <folder_node>
<name>subjects</name> <name>subjects</name>
<children> <children>

View File

@@ -28,7 +28,7 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(AccountingPayablesTableComponent); )(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) { export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]); const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false); const [transInProgress, setTransInProgress] = useState(false);
@@ -149,6 +149,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
disabled={transInProgress || !!record.exported} disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills} setSelectedBills={setSelectedBills}
refetch={refetch}
/> />
</div> </div>
), ),
@@ -181,6 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
disabled={transInProgress || selectedBills.length === 0} disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedBills} completedCallback={setSelectedBills}
refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent /> <QboAuthorizeComponent />

View File

@@ -32,6 +32,7 @@ export function AccountingPayablesTableComponent({
bodyshop, bodyshop,
loading, loading,
payments, payments,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]); const [selectedPayments, setSelectedPayments] = useState([]);
@@ -147,6 +148,7 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || !!record.exportedat} disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments} setSelectedPayments={setSelectedPayments}
refetch={refetch}
/> />
), ),
}, },
@@ -187,6 +189,7 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || selectedPayments.length === 0} disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments} completedCallback={setSelectedPayments}
refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent /> <QboAuthorizeComponent />

View File

@@ -31,6 +31,7 @@ export function AccountingReceivablesTableComponent({
bodyshop, bodyshop,
loading, loading,
jobs, jobs,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]); const [selectedJobs, setSelectedJobs] = useState([]);
@@ -155,6 +156,7 @@ export function AccountingReceivablesTableComponent({
jobId={record.id} jobId={record.id}
disabled={!!record.date_exported} disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs} setSelectedJobs={setSelectedJobs}
refetch={refetch}
/> />
<Link to={`/manage/jobs/${record.id}/close`}> <Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button> <Button>{t("jobs.labels.viewallocations")}</Button>
@@ -205,6 +207,7 @@ export function AccountingReceivablesTableComponent({
disabled={transInProgress || selectedJobs.length === 0} disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress} loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs} completedCallback={setSelectedJobs}
refetch={refetch}
/> />
)} )}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (

View File

@@ -42,7 +42,12 @@ export function EmailOverlayComponent({
const { t } = useTranslation(); const { t } = useTranslation();
const handleClick = ({ item, key, keyPath }) => { const handleClick = ({ item, key, keyPath }) => {
const email = item.props.value; const email = item.props.value;
form.setFieldsValue({ to: _.uniq([...form.getFieldValue("to"), email]) }); form.setFieldsValue({
to: _.uniq([
...form.getFieldValue("to"),
...(typeof email === "string" ? [email] : email),
]),
});
}; };
const menu = ( const menu = (
@@ -55,6 +60,11 @@ export function EmailOverlayComponent({
{`${e.first_name} ${e.last_name}`} {`${e.first_name} ${e.last_name}`}
</Menu.Item> </Menu.Item>
))} ))}
{bodyshop.md_to_emails.map((e, idx) => (
<Menu.Item value={e.emails} key={idx + "group"}>
{e.label}
</Menu.Item>
))}
</Menu> </Menu>
</div> </div>
); );

View File

@@ -15,7 +15,6 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -27,6 +26,7 @@ export function JobsCloseExportButton({
jobId, jobId,
disabled, disabled,
setSelectedJobs, setSelectedJobs,
refetch,
}) { }) {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -46,13 +46,10 @@ export function JobsCloseExportButton({
//Check if it's a QBO Setup. //Check if it's a QBO Setup.
let PartnerResponse; let PartnerResponse;
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post( PartnerResponse = await axios.post(`/qbo/receivables`, {
`/qbo/receivables`, jobIds: [jobId],
{ elgen: true,
jobIds: [jobId], });
},
);
} else { } else {
//Default is QBD //Default is QBD
@@ -117,58 +114,64 @@ export function JobsCloseExportButton({
}); });
}); });
await insertExportLog({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
logs: [ await insertExportLog({
{ variables: {
bodyshopid: bodyshop.id, logs: [
jobid: jobId, {
successful: false, bodyshopid: bodyshop.id,
message: JSON.stringify( jobid: jobId,
failedTransactions.map((ft) => ft.errorMessage) successful: false,
), message: JSON.stringify(
useremail: currentUser.email, failedTransactions.map((ft) => ft.errorMessage)
}, ),
], useremail: currentUser.email,
}, },
}); ],
},
});
}
} else { } else {
//Insert success export log. //Insert success export log.
await insertExportLog({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
logs: [ await insertExportLog({
{ variables: {
bodyshopid: bodyshop.id, logs: [
jobid: jobId, {
successful: true, bodyshopid: bodyshop.id,
useremail: currentUser.email, jobid: jobId,
}, successful: true,
], useremail: currentUser.email,
}, },
}); ],
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
job: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
}, },
}, });
});
if (!jobUpdateResponse.errors) { const jobUpdateResponse = await updateJob({
notification.open({ variables: {
type: "success", jobId: jobId,
key: "jobsuccessexport", job: {
message: t("jobs.successes.exported"), status: bodyshop.md_ro_statuses.default_exported || "Exported*",
}); date_exported: new Date(),
} else { },
notification["error"]({ },
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
}); });
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
} }
if (setSelectedJobs) { if (setSelectedJobs) {
setSelectedJobs((selectedJobs) => { setSelectedJobs((selectedJobs) => {
@@ -176,7 +179,7 @@ export function JobsCloseExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -26,6 +26,7 @@ export function JobsExportAllButton({
disabled, disabled,
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS); const [updateJob] = useMutation(UPDATE_JOBS);
@@ -39,6 +40,7 @@ export function JobsExportAllButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/receivables`, { PartnerResponse = await axios.post(`/qbo/receivables`, {
jobIds: jobIds, jobIds: jobIds,
elgen: true,
}); });
} else { } else {
let QbXmlResponse; let QbXmlResponse;
@@ -83,6 +85,7 @@ export function JobsExportAllButton({
return; return;
} }
} }
console.log("PartnerResponse", PartnerResponse); console.log("PartnerResponse", PartnerResponse);
const groupedData = _.groupBy( const groupedData = _.groupBy(
PartnerResponse.data, PartnerResponse.data,
@@ -106,61 +109,70 @@ export function JobsExportAllButton({
}); });
//Call is not awaited as it is not critical to finish before proceeding. //Call is not awaited as it is not critical to finish before proceeding.
}); });
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const jobUpdateResponse = await updateJob({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
jobIds: [key], await insertExportLog({
fields: { variables: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*", logs: [
date_exported: new Date(), {
bodyshopid: bodyshop.id,
jobid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
}, },
}, });
}); }
} else {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
if (!jobUpdateResponse.errors) { const jobUpdateResponse = await updateJob({
notification.open({ variables: {
type: "success", jobIds: [key],
key: "jobsuccessexport", fields: {
message: t("jobs.successes.exported"), status:
}); bodyshop.md_ro_statuses.default_exported || "Exported*",
} else { date_exported: new Date(),
notification["error"]({ },
message: t("jobs.errors.exporting", { },
error: JSON.stringify(jobUpdateResponse.error),
}),
}); });
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
} }
} }
}) })
); );
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);

View File

@@ -0,0 +1,47 @@
import React from "react";
import { Button, Popconfirm } from "antd";
import { DeleteFilled } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { DELETE_PARTS_ORDER_LINE } from "../../graphql/parts-orders.queries";
import { useMutation } from "@apollo/client";
export default function PartsOrderDeleteLine({
disabled,
partsLineId,
partsOrderId,
}) {
const { t } = useTranslation();
const [deletePartsOrderLine] = useMutation(DELETE_PARTS_ORDER_LINE);
return (
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={disabled}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrderLine({
variables: { partsOrderLineId: partsLineId },
update(cache) {
cache.modify({
id: cache.identify({
__typename: "parts_orders",
id: partsOrderId,
}),
fields: {
parts_order_lines(cached, { readField }) {
return cached.filter((c) => {
return readField("id", c) !== partsLineId;
});
},
},
});
},
});
}}
>
<Button disabled={disabled}>
<DeleteFilled />
</Button>
</Popconfirm>
);
}

View File

@@ -30,6 +30,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component"; import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component"; import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component"; import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container"; import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component"; import PrintWrapper from "../print-wrapper/print-wrapper.component";
@@ -391,12 +392,21 @@ export function PartsOrderListTableComponent({
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<PartsOrderLineBackorderButton <Space wrap>
disabled={jobRO} <PartsOrderDeleteLine
partsOrderStatus={record.status} disabled={jobRO}
partsLineId={record.id} partsOrderStatus={record.status}
jobLineId={record.job_line_id} partsLineId={record.id}
/> partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
), ),
}, },
]; ];

View File

@@ -27,6 +27,7 @@ export function PayableExportAll({
disabled, disabled,
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateBill] = useMutation(UPDATE_BILLS); const [updateBill] = useMutation(UPDATE_BILLS);
@@ -42,6 +43,7 @@ export function PayableExportAll({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payables`, { PartnerResponse = await axios.post(`/qbo/payables`, {
bills: billids, bills: billids,
elgen: true,
}); });
} else { } else {
let QbXmlResponse; let QbXmlResponse;
@@ -104,57 +106,62 @@ export function PayableExportAll({
}), }),
}) })
); );
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
await insertExportLog({ //QBO Logs are handled server side.
variables: { await insertExportLog({
logs: [ variables: {
{ logs: [
bodyshopid: bodyshop.id, {
billid: key, bodyshopid: bodyshop.id,
successful: false, billid: key,
message: JSON.stringify( successful: false,
failedTransactions.map((ft) => ft.errorMessage) message: JSON.stringify(
), failedTransactions.map((ft) => ft.errorMessage)
useremail: currentUser.email, ),
}, useremail: currentUser.email,
], },
}, ],
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: [key],
bill: {
exported: true,
exported_at: new Date(),
}, },
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
}); });
} else { }
notification["error"]({ } else {
message: t("bills.errors.exporting", { if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
error: JSON.stringify(billUpdateResponse.error), //QBO Logs are handled server side.
}), await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: key,
successful: true,
useremail: currentUser.email,
},
],
},
}); });
const billUpdateResponse = await updateBill({
variables: {
billIdList: [key],
bill: {
exported: true,
exported_at: new Date(),
},
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
});
}
} }
} }
})() })()
@@ -164,6 +171,8 @@ export function PayableExportAll({
await Promise.all(proms); await Promise.all(proms);
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -26,6 +26,7 @@ export function PayableExportButton({
disabled, disabled,
loadingCallback, loadingCallback,
setSelectedBills, setSelectedBills,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateBill] = useMutation(UPDATE_BILLS); const [updateBill] = useMutation(UPDATE_BILLS);
@@ -43,6 +44,7 @@ export function PayableExportButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payables`, { PartnerResponse = await axios.post(`/qbo/payables`, {
bills: [billId], bills: [billId],
elgen: true,
}); });
} else { } else {
//Default is QBD //Default is QBD
@@ -100,64 +102,72 @@ export function PayableExportButton({
}), }),
}) })
); );
await insertExportLog({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
logs: [ await insertExportLog({
{ variables: {
bodyshopid: bodyshop.id, logs: [
billid: billId, {
successful: false, bodyshopid: bodyshop.id,
message: JSON.stringify( billid: billId,
failedTransactions.map((ft) => ft.errorMessage) successful: false,
), message: JSON.stringify(
useremail: currentUser.email, failedTransactions.map((ft) => ft.errorMessage)
}, ),
], useremail: currentUser.email,
}, },
}); ],
}
if (successfulTransactions.length > 0) {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: billId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
),
bill: {
exported: true,
exported_at: new Date(),
}, },
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
}); });
} }
}
if (successfulTransactions.length > 0) {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: billId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
),
bill: {
exported: true,
exported_at: new Date(),
},
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (setSelectedBills) { if (setSelectedBills) {
setSelectedBills((selectedBills) => { setSelectedBills((selectedBills) => {
return selectedBills.filter((i) => i !== billId); return selectedBills.filter((i) => i !== billId);

View File

@@ -26,6 +26,7 @@ export function PaymentExportButton({
disabled, disabled,
loadingCallback, loadingCallback,
setSelectedPayments, setSelectedPayments,
refetch,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updatePayment] = useMutation(UPDATE_PAYMENTS); const [updatePayment] = useMutation(UPDATE_PAYMENTS);
@@ -40,6 +41,7 @@ export function PaymentExportButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payments`, { PartnerResponse = await axios.post(`/qbo/payments`, {
payments: [paymentId], payments: [paymentId],
elgen: true,
}); });
} else { } else {
//Default is QBD //Default is QBD
@@ -100,63 +102,68 @@ export function PaymentExportButton({
}), }),
}) })
); );
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
await insertExportLog({ //QBO Logs are handled server side.
variables: { await insertExportLog({
logs: [ variables: {
{ logs: [
bodyshopid: bodyshop.id, {
paymentid: paymentId, bodyshopid: bodyshop.id,
successful: false, paymentid: paymentId,
message: JSON.stringify( successful: false,
failedTransactions.map((ft) => ft.errorMessage) message: JSON.stringify(
), failedTransactions.map((ft) => ft.errorMessage)
useremail: currentUser.email, ),
}, useremail: currentUser.email,
], },
}, ],
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: paymentId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayment({
variables: {
paymentIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
),
payment: {
exportedat: new Date(),
}, },
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
}); });
} else { }
notification["error"]({ } else {
message: t("payments.errors.exporting", { if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
error: JSON.stringify(paymentUpdateResponse.error), //QBO Logs are handled server side.
}), await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: paymentId,
successful: true,
useremail: currentUser.email,
},
],
},
}); });
const paymentUpdateResponse = await updatePayment({
variables: {
paymentIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
),
payment: {
exportedat: new Date(),
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
});
}
} }
if (setSelectedPayments) { if (setSelectedPayments) {
@@ -165,7 +172,7 @@ export function PaymentExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
setLoading(false); setLoading(false);
}; };

View File

@@ -25,6 +25,7 @@ export function PaymentsExportAllButton({
disabled, disabled,
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updatePayments] = useMutation(UPDATE_PAYMENTS); const [updatePayments] = useMutation(UPDATE_PAYMENTS);
@@ -38,6 +39,7 @@ export function PaymentsExportAllButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payments`, { PartnerResponse = await axios.post(`/qbo/payments`, {
payments: paymentIds, payments: paymentIds,
elgen: true,
}); });
} else { } else {
let QbXmlResponse; let QbXmlResponse;
@@ -92,54 +94,61 @@ export function PaymentsExportAllButton({
}), }),
}) })
); );
await insertExportLog({ if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
variables: { //QBO Logs are handled server side.
logs: [ await insertExportLog({
{ variables: {
bodyshopid: bodyshop.id, logs: [
paymentid: key, {
successful: false, bodyshopid: bodyshop.id,
message: JSON.stringify( paymentid: key,
failedTransactions.map((ft) => ft.errorMessage) successful: false,
), message: JSON.stringify(
useremail: currentUser.email, failedTransactions.map((ft) => ft.errorMessage)
}, ),
], useremail: currentUser.email,
}, },
}); ],
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayments({
variables: {
paymentIdList: [key],
payment: {
exportedat: new Date(),
}, },
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
}); });
} else { }
notification["error"]({ } else {
message: t("payments.errors.exporting", { if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
error: JSON.stringify(paymentUpdateResponse.error), //QBO Logs are handled server side.
}),
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: key,
successful: true,
useremail: currentUser.email,
},
],
},
}); });
const paymentUpdateResponse = await updatePayments({
variables: {
paymentIdList: [key],
payment: {
exportedat: new Date(),
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
});
}
} }
} }
})() })()
@@ -148,6 +157,7 @@ export function PaymentsExportAllButton({
await Promise.all(proms); await Promise.all(proms);
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -328,6 +328,12 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item
label={t("employees.fields.external_id")}
name="external_id"
>
<Input />
</Form.Item>
</LayoutFormRow> </LayoutFormRow>
<Form.List name={["rates"]}> <Form.List name={["rates"]}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {

View File

@@ -1393,6 +1393,60 @@ export default function ShopInfoGeneral({ form }) {
}} }}
</Form.List> </Form.List>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.md_to_emails")}>
<Form.List name={["md_to_emails"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item
label={t("general.labels.label")}
key={`${index}label`}
name={[field.name, "label"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.labels.md_to_emails_emails")}
key={`${index}emails`}
name={[field.name, "emails"]}
>
<Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item>
<Space>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
</div> </div>
); );
} }

View File

@@ -237,7 +237,11 @@ export function TimeTicketModalComponent({
return Promise.reject( return Promise.reject(
t("timetickets.validation.clockoffwithoutclockon") t("timetickets.validation.clockoffwithoutclockon")
); );
if (value && !value.isSameOrAfter(clockon)) if (
value &&
value.isSameOrAfter &&
!value.isSameOrAfter(clockon)
)
return Promise.reject( return Promise.reject(
t("timetickets.validation.clockoffmustbeafterclockon") t("timetickets.validation.clockoffmustbeafterclockon")
); );

View File

@@ -0,0 +1,42 @@
import { Button } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useLocation } from "react-router-dom";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { useTranslation } from "react-i18next";
import moment from "moment";
const AttendanceCsv = TemplateList("special").attendance_detail_csv;
export default function TimeTicketsAttendanceTable() {
const searchParams = queryString.parse(useLocation().search);
const { start, end } = searchParams;
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await GenerateDocument(
{
name: AttendanceCsv.key,
variables: {
start: start
? start
: moment().startOf("week").subtract(7, "days").format("YYYY-MM-DD"),
end: end ? end : moment().endOf("week").format("YYYY-MM-DD"),
},
},
{},
"text"
);
setLoading(false);
};
return (
<Button loading={loading} onClick={handleClick}>
{t("printcenter.special.attendance_detail_csv")}
</Button>
);
}

View File

@@ -106,6 +106,7 @@ export const QUERY_BODYSHOP = gql`
last_name_first last_name_first
md_parts_order_comment md_parts_order_comment
bill_allow_post_to_closed bill_allow_post_to_closed
md_to_emails
employees { employees {
user_email user_email
id id
@@ -114,6 +115,7 @@ export const QUERY_BODYSHOP = gql`
last_name last_name
employee_number employee_number
rates rates
external_id
} }
} }
} }
@@ -209,6 +211,7 @@ export const UPDATE_SHOP = gql`
last_name_first last_name_first
md_parts_order_comment md_parts_order_comment
bill_allow_post_to_closed bill_allow_post_to_closed
md_to_emails
employees { employees {
id id
first_name first_name
@@ -217,6 +220,7 @@ export const UPDATE_SHOP = gql`
employee_number employee_number
rates rates
user_email user_email
external_id
} }
} }
} }

View File

@@ -25,6 +25,7 @@ export const QUERY_EMPLOYEE_BY_ID = gql`
rates rates
pin pin
user_email user_email
external_id
employee_vacations(order_by: { start: desc }) { employee_vacations(order_by: { start: desc }) {
id id
start start

View File

@@ -292,6 +292,14 @@ export const DELETE_PARTS_ORDER = gql`
} }
`; `;
export const DELETE_PARTS_ORDER_LINE = gql`
mutation DELETE_PARTS_ORDER_LINE($partsOrderLineId: uuid!) {
delete_parts_order_lines_by_pk(id: $partsOrderLineId) {
id
}
}
`;
export const MUTATION_UPDATE_PO_CM_REECEIVED = gql` export const MUTATION_UPDATE_PO_CM_REECEIVED = gql`
mutation MUTATION_UPDATE_PO_CM_REECEIVED( mutation MUTATION_UPDATE_PO_CM_REECEIVED(
$partsLineId: uuid! $partsLineId: uuid!

View File

@@ -45,7 +45,7 @@ export function AccountingPayablesContainer({
checkPartnerStatus(bodyshop, true); checkPartnerStatus(bodyshop, true);
}, [t, setBreadcrumbs, setSelectedHeader, bodyshop]); }, [t, setBreadcrumbs, setSelectedHeader, bodyshop]);
const { loading, error, data } = useQuery(QUERY_BILLS_FOR_EXPORT, { const { loading, error, data, refetch } = useQuery(QUERY_BILLS_FOR_EXPORT, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
}); });
@@ -73,6 +73,7 @@ export function AccountingPayablesContainer({
<AccountingPayablesTable <AccountingPayablesTable
loadaing={loading} loadaing={loading}
bills={data ? data.bills : []} bills={data ? data.bills : []}
refetch={refetch}
/> />
</RbacWrapper> </RbacWrapper>
</div> </div>

View File

@@ -44,10 +44,13 @@ export function AccountingPaymentsContainer({
checkPartnerStatus(bodyshop, true); checkPartnerStatus(bodyshop, true);
}, [t, setBreadcrumbs, setSelectedHeader, bodyshop]); }, [t, setBreadcrumbs, setSelectedHeader, bodyshop]);
const { loading, error, data } = useQuery(QUERY_PAYMENTS_FOR_EXPORT, { const { loading, error, data, refetch } = useQuery(
fetchPolicy: "network-only", QUERY_PAYMENTS_FOR_EXPORT,
nextFetchPolicy: "network-only", {
}); fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
}
);
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
const noPath = const noPath =
@@ -70,6 +73,7 @@ export function AccountingPaymentsContainer({
<AccountingPaymentsTable <AccountingPaymentsTable
loadaing={loading} loadaing={loading}
payments={data ? data.payments : []} payments={data ? data.payments : []}
refetch={refetch}
/> />
</RbacWrapper> </RbacWrapper>
</div> </div>

View File

@@ -44,7 +44,7 @@ export function AccountingReceivablesContainer({
checkPartnerStatus(bodyshop, true); checkPartnerStatus(bodyshop, true);
}, [t, setBreadcrumbs, setSelectedHeader, bodyshop]); }, [t, setBreadcrumbs, setSelectedHeader, bodyshop]);
const { loading, error, data } = useQuery(QUERY_JOBS_FOR_EXPORT, { const { loading, error, data, refetch } = useQuery(QUERY_JOBS_FOR_EXPORT, {
variables: { variables: {
invoicedStatus: bodyshop.md_ro_statuses.default_invoiced || "Invoiced*", invoicedStatus: bodyshop.md_ro_statuses.default_invoiced || "Invoiced*",
}, },
@@ -75,6 +75,7 @@ export function AccountingReceivablesContainer({
<AccountingReceivablesTable <AccountingReceivablesTable
loadaing={loading} loadaing={loading}
jobs={data ? data.jobs : []} jobs={data ? data.jobs : []}
refetch={refetch}
/> />
</RbacWrapper> </RbacWrapper>
</div> </div>

View File

@@ -14,6 +14,7 @@ import TimeTicketList from "../../components/time-ticket-list/time-ticket-list.c
import TimeTicketsPayrollTable from "../../components/time-tickets-payroll-table/time-tickets-payroll-table.component"; import TimeTicketsPayrollTable from "../../components/time-tickets-payroll-table/time-tickets-payroll-table.component";
import TimeTicketsSummaryEmployees from "../../components/time-tickets-summary-employees/time-tickets-summary-employees.component"; import TimeTicketsSummaryEmployees from "../../components/time-tickets-summary-employees/time-tickets-summary-employees.component";
import { QUERY_TIME_TICKETS_IN_RANGE } from "../../graphql/timetickets.queries"; import { QUERY_TIME_TICKETS_IN_RANGE } from "../../graphql/timetickets.queries";
import TimeTicketsAttendanceTable from "../../components/time-tickets-attendance-table/time-tickets-attendance-table.component";
import { import {
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
@@ -71,6 +72,7 @@ export function TimeTicketsContainer({
timetickets={data ? data.timetickets : []} timetickets={data ? data.timetickets : []}
extra={ extra={
<Space wrap> <Space wrap>
<TimeTicketsAttendanceTable />
<TimeTicketsPayrollTable /> <TimeTicketsPayrollTable />
<TimeTicketsDatesSelector /> <TimeTicketsDatesSelector />
</Space> </Space>

View File

@@ -544,6 +544,8 @@
"jobstatuses": "Job Statuses", "jobstatuses": "Job Statuses",
"laborrates": "Labor Rates", "laborrates": "Labor Rates",
"licensing": "Licensing", "licensing": "Licensing",
"md_to_emails": "Preset To Emails",
"md_to_emails_emails": "Emails",
"messagingpresets": "Messaging Presets", "messagingpresets": "Messaging Presets",
"notemplatesavailable": "No templates available to add.", "notemplatesavailable": "No templates available to add.",
"notespresets": "Notes Presets", "notespresets": "Notes Presets",
@@ -878,6 +880,7 @@
"base_rate": "Base Rate", "base_rate": "Base Rate",
"cost_center": "Cost Center", "cost_center": "Cost Center",
"employee_number": "Employee Number", "employee_number": "Employee Number",
"external_id": "External Employee ID",
"first_name": "First Name", "first_name": "First Name",
"flat_rate": "Flat Rate (Disabled is Straight Time)", "flat_rate": "Flat Rate (Disabled is Straight Time)",
"hire_date": "Hire Date", "hire_date": "Hire Date",
@@ -2172,6 +2175,9 @@
"ca_bc_etf_table": "ICBC ETF Table", "ca_bc_etf_table": "ICBC ETF Table",
"exported_payroll": "Payroll Table" "exported_payroll": "Payroll Table"
}, },
"special": {
"attendance_detail_csv": "Attendance Table"
},
"subjects": { "subjects": {
"jobs": { "jobs": {
"parts_order": "Parts Order PO: {{ro_number}} - {{name}}" "parts_order": "Parts Order PO: {{ro_number}} - {{name}}"

View File

@@ -544,6 +544,8 @@
"jobstatuses": "", "jobstatuses": "",
"laborrates": "", "laborrates": "",
"licensing": "", "licensing": "",
"md_to_emails": "",
"md_to_emails_emails": "",
"messagingpresets": "", "messagingpresets": "",
"notemplatesavailable": "", "notemplatesavailable": "",
"notespresets": "", "notespresets": "",
@@ -878,6 +880,7 @@
"base_rate": "Tasa básica", "base_rate": "Tasa básica",
"cost_center": "Centro de costos", "cost_center": "Centro de costos",
"employee_number": "Numero de empleado", "employee_number": "Numero de empleado",
"external_id": "",
"first_name": "Nombre de pila", "first_name": "Nombre de pila",
"flat_rate": "Tarifa plana (deshabilitado es tiempo recto)", "flat_rate": "Tarifa plana (deshabilitado es tiempo recto)",
"hire_date": "Fecha de contratación", "hire_date": "Fecha de contratación",
@@ -2172,6 +2175,9 @@
"ca_bc_etf_table": "", "ca_bc_etf_table": "",
"exported_payroll": "" "exported_payroll": ""
}, },
"special": {
"attendance_detail_csv": ""
},
"subjects": { "subjects": {
"jobs": { "jobs": {
"parts_order": "" "parts_order": ""

View File

@@ -544,6 +544,8 @@
"jobstatuses": "", "jobstatuses": "",
"laborrates": "", "laborrates": "",
"licensing": "", "licensing": "",
"md_to_emails": "",
"md_to_emails_emails": "",
"messagingpresets": "", "messagingpresets": "",
"notemplatesavailable": "", "notemplatesavailable": "",
"notespresets": "", "notespresets": "",
@@ -878,6 +880,7 @@
"base_rate": "Taux de base", "base_rate": "Taux de base",
"cost_center": "Centre de coûts", "cost_center": "Centre de coûts",
"employee_number": "Numéro d'employé", "employee_number": "Numéro d'employé",
"external_id": "",
"first_name": "Prénom", "first_name": "Prénom",
"flat_rate": "Taux fixe (désactivé est le temps normal)", "flat_rate": "Taux fixe (désactivé est le temps normal)",
"hire_date": "Date d'embauche", "hire_date": "Date d'embauche",
@@ -2172,6 +2175,9 @@
"ca_bc_etf_table": "", "ca_bc_etf_table": "",
"exported_payroll": "" "exported_payroll": ""
}, },
"special": {
"attendance_detail_csv": ""
},
"subjects": { "subjects": {
"jobs": { "jobs": {
"parts_order": "" "parts_order": ""

View File

@@ -18,7 +18,8 @@ export default async function RenderTemplate(
templateObject, templateObject,
bodyshop, bodyshop,
renderAsHtml = false, renderAsHtml = false,
renderAsExcel = false renderAsExcel = false,
renderAsText = false
) { ) {
//Query assets that match the template name. Must be in format <<templateName>>.query //Query assets that match the template name. Must be in format <<templateName>>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData( let { contextData, useShopSpecificTemplate } = await fetchContextData(
@@ -54,6 +55,7 @@ export default async function RenderTemplate(
}), }),
}), }),
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}), ...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
...(renderAsText ? { recipe: "text" } : {}),
}, },
data: { data: {
...contextData, ...contextData,
@@ -254,6 +256,8 @@ export const GenerateDocument = async (
} else if (sendType === "x") { } else if (sendType === "x") {
console.log("excel"); console.log("excel");
await RenderTemplate(template, bodyshop, false, true); await RenderTemplate(template, bodyshop, false, true);
} else if (sendType === "text") {
await RenderTemplate(template, bodyshop, false, false, true);
} else { } else {
await RenderTemplate(template, bodyshop); await RenderTemplate(template, bodyshop);
} }

View File

@@ -1668,6 +1668,13 @@ export const TemplateList = (type, context) => {
key: "exported_payroll", key: "exported_payroll",
disabled: false, disabled: false,
}, },
attendance_detail_csv: {
title: i18n.t("printcenter.special.attendance_detail_csv"),
description: "Est Detail",
subject: i18n.t("printcenter.special.attendance_detail_csv"),
key: "attendance_detail_csv",
disabled: false,
},
production_by_technician_one: { production_by_technician_one: {
title: i18n.t( title: i18n.t(
"reportcenter.templates.production_by_technician_one" "reportcenter.templates.production_by_technician_one"

View File

@@ -858,6 +858,7 @@
- md_referral_sources - md_referral_sources
- md_responsibility_centers - md_responsibility_centers
- md_ro_statuses - md_ro_statuses
- md_to_emails
- messagingservicesid - messagingservicesid
- pbs_configuration - pbs_configuration
- pbs_serialnumber - pbs_serialnumber
@@ -944,6 +945,7 @@
- md_referral_sources - md_referral_sources
- md_responsibility_centers - md_responsibility_centers
- md_ro_statuses - md_ro_statuses
- md_to_emails
- pbs_configuration - pbs_configuration
- phone - phone
- prodtargethrs - prodtargethrs
@@ -1953,6 +1955,7 @@
- active - active
- created_at - created_at
- employee_number - employee_number
- external_id
- first_name - first_name
- flat_rate - flat_rate
- hire_date - hire_date
@@ -1971,6 +1974,7 @@
- active - active
- created_at - created_at
- employee_number - employee_number
- external_id
- first_name - first_name
- flat_rate - flat_rate
- hire_date - hire_date
@@ -1999,6 +2003,7 @@
- active - active
- created_at - created_at
- employee_number - employee_number
- external_id
- first_name - first_name
- flat_rate - flat_rate
- hire_date - hire_date

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"."bodyshops" add column "md_to_emails" jsonb
-- null default jsonb_build_array();

View File

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

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"."employees" add column "external_id" text
-- null;

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,7 @@ exports.default = async (req, res) => {
exports.refresh = async (oauthClient, req) => { exports.refresh = async (oauthClient, req) => {
try { try {
logger.log("qbo-token-refresh", "DEBUG", req.user.email, null, null); // logger.log("qbo-token-refresh", "DEBUG", req.user.email, null, null);
const authResponse = await oauthClient.refresh(); const authResponse = await oauthClient.refresh();
await client.request(queries.SET_QBO_AUTH, { await client.request(queries.SET_QBO_AUTH, {
email: req.user.email, email: req.user.email,
@@ -85,7 +85,7 @@ exports.refresh = async (oauthClient, req) => {
}; };
exports.setNewRefreshToken = async (email, apiResponse) => { exports.setNewRefreshToken = async (email, apiResponse) => {
logger.log("qbo-token-updated", "DEBUG", email, null, null); //logger.log("qbo-token-updated", "DEBUG", email, null, null);
await client.request(queries.SET_QBO_AUTH, { await client.request(queries.SET_QBO_AUTH, {
email, email,

View File

@@ -45,7 +45,7 @@ exports.default = async (req, res) => {
await refreshOauthToken(oauthClient, req); await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization; const BearerToken = req.headers.authorization;
const { bills: billsToQuery } = req.body; const { bills: billsToQuery, elgen } = req.body;
//Query Job Info //Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: { headers: {
@@ -59,8 +59,9 @@ exports.default = async (req, res) => {
bills: billsToQuery, bills: billsToQuery,
}); });
const { bills } = result; const { bills, bodyshops } = result;
const ret = []; const ret = [];
const bodyshop = bodyshops[0];
for (const bill of bills) { for (const bill of bills) {
try { try {
@@ -86,9 +87,31 @@ exports.default = async (req, res) => {
qbo_realmId, qbo_realmId,
req, req,
bill, bill,
vendorRecord vendorRecord,
bodyshop
); );
// //No error. Mark the job exported & insert export log.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QBO_MARK_BILL_EXPORTED, {
billId: bill.id,
bill: {
exported: true,
exported_at: moment().tz(bodyshop.timezone),
},
logs: [
{
bodyshopid: bodyshop.id,
billid: bill.id,
successful: true,
useremail: req.user.email,
},
],
});
}
ret.push({ billid: bill.id, success: true }); ret.push({ billid: bill.id, success: true });
} catch (error) { } catch (error) {
ret.push({ ret.push({
@@ -98,6 +121,26 @@ exports.default = async (req, res) => {
(error && error.authResponse && error.authResponse.body) || (error && error.authResponse && error.authResponse.body) ||
(error && error.message), (error && error.message),
}); });
//Add the export log error.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_EXPORT_LOG, {
logs: [
{
bodyshopid: bodyshop.id,
billid: bill.id,
successful: false,
message: JSON.stringify([
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
]),
useremail: req.user.email,
},
],
});
}
} }
} }
@@ -167,7 +210,14 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
} }
} }
async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) { async function InsertBill(
oauthClient,
qbo_realmId,
req,
bill,
vendor,
bodyshop
) {
const { accounts, taxCodes, classes } = await QueryMetaData( const { accounts, taxCodes, classes } = await QueryMetaData(
oauthClient, oauthClient,
qbo_realmId, qbo_realmId,
@@ -179,20 +229,20 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) {
il, il,
accounts, accounts,
bill.job.class, bill.job.class,
bill.job.bodyshop.md_responsibility_centers.sales_tax_codes, bodyshop.md_responsibility_centers.sales_tax_codes,
classes, classes,
taxCodes, taxCodes,
bill.job.bodyshop.md_responsibility_centers.costs bodyshop.md_responsibility_centers.costs
) )
); );
//QB USA with GST //QB USA with GST
//This was required for the No. 1 Collision Group. //This was required for the No. 1 Collision Group.
if ( if (
bill.job.bodyshop.accountingconfig && bodyshop.accountingconfig &&
bill.job.bodyshop.accountingconfig.qbo && bodyshop.accountingconfig.qbo &&
bill.job.bodyshop.accountingconfig.qbo_usa && bodyshop.accountingconfig.qbo_usa &&
bill.job.bodyshop.region_config.includes("CA_") bodyshop.region_config.includes("CA_")
) { ) {
lines.push({ lines.push({
DetailType: "AccountBasedExpenseLineDetail", DetailType: "AccountBasedExpenseLineDetail",
@@ -204,8 +254,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) {
AccountRef: { AccountRef: {
value: value:
accounts[ accounts[
bill.job.bodyshop.md_responsibility_centers.taxes.federal bodyshop.md_responsibility_centers.taxes.federal.accountdesc
.accountdesc
], ],
}, },
}, },
@@ -239,7 +288,18 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) {
}), }),
DocNumber: bill.invoice_number, DocNumber: bill.invoice_number,
//...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}), //...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}),
...(!(
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa &&
bodyshop.region_config.includes("CA_")
)
? { GlobalTaxCalculation: "TaxExcluded" }
: {}),
...(bodyshop.accountingconfig.qbo_departmentid &&
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid },
}),
PrivateNote: `RO ${bill.job.ro_number || ""}`, PrivateNote: `RO ${bill.job.ro_number || ""}`,
Line: lines, Line: lines,
}; };

View File

@@ -52,7 +52,7 @@ exports.default = async (req, res) => {
await refreshOauthToken(oauthClient, req); await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization; const BearerToken = req.headers.authorization;
const { payments: paymentsToQuery } = req.body; const { payments: paymentsToQuery, elgen } = req.body;
//Query Job Info //Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: { headers: {
@@ -155,6 +155,27 @@ exports.default = async (req, res) => {
bodyshop bodyshop
); );
} }
// //No error. Mark the payment exported & insert export log.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QBO_MARK_PAYMENT_EXPORTED, {
paymentId: payment.id,
payment: {
exportedat: moment().tz(bodyshop.timezone),
},
logs: [
{
bodyshopid: bodyshop.id,
paymentid: payment.id,
successful: true,
useremail: req.user.email,
},
],
});
}
ret.push({ paymentid: payment.id, success: true }); ret.push({ paymentid: payment.id, success: true });
} catch (error) { } catch (error) {
logger.log("qbo-payment-create-error", "ERROR", req.user.email, { logger.log("qbo-payment-create-error", "ERROR", req.user.email, {
@@ -162,6 +183,25 @@ exports.default = async (req, res) => {
(error && error.authResponse && error.authResponse.body) || (error && error.authResponse && error.authResponse.body) ||
(error && error.message), (error && error.message),
}); });
//Add the export log error.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_EXPORT_LOG, {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: payment.id,
successful: false,
message: JSON.stringify([
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
]),
useremail: req.user.email,
},
],
});
}
ret.push({ ret.push({
paymentid: payment.id, paymentid: payment.id,

View File

@@ -46,7 +46,7 @@ exports.default = async (req, res) => {
await refreshOauthToken(oauthClient, req); await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization; const BearerToken = req.headers.authorization;
const { jobIds } = req.body; const { jobIds, elgen } = req.body;
//Query Job Info //Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: { headers: {
@@ -139,6 +139,28 @@ exports.default = async (req, res) => {
bodyshop, bodyshop,
jobTier jobTier
); );
// //No error. Mark the job exported & insert export log.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QBO_MARK_JOB_EXPORTED, {
jobId: job.id,
job: {
status:
bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: moment().tz(bodyshop.timezone),
},
logs: [
{
bodyshopid: bodyshop.id,
jobid: job.id,
successful: true,
useremail: req.user.email,
},
],
});
}
} }
ret.push({ jobid: job.id, success: true }); ret.push({ jobid: job.id, success: true });
} catch (error) { } catch (error) {
@@ -149,6 +171,25 @@ exports.default = async (req, res) => {
(error && error.authResponse && error.authResponse.body) || (error && error.authResponse && error.authResponse.body) ||
(error && error.message), (error && error.message),
}); });
//Add the export log error.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_EXPORT_LOG, {
logs: [
{
bodyshopid: bodyshop.id,
jobid: job.id,
successful: false,
message: JSON.stringify([
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
]),
useremail: req.user.email,
},
],
});
}
} }
} }
@@ -198,6 +239,7 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
const Customer = { const Customer = {
DisplayName: job.ins_co_nm, DisplayName: job.ins_co_nm,
BillWithParent: true,
BillAddr: { BillAddr: {
City: job.ownr_city, City: job.ownr_city,
Line1: insCo.street1, Line1: insCo.street1,
@@ -261,6 +303,7 @@ async function InsertOwner(
const ownerName = generateOwnerTier(job, true, null); const ownerName = generateOwnerTier(job, true, null);
const Customer = { const Customer = {
DisplayName: ownerName, DisplayName: ownerName,
BillWithParent: true,
BillAddr: { BillAddr: {
City: job.ownr_city, City: job.ownr_city,
Line1: job.ownr_addr1, Line1: job.ownr_addr1,
@@ -321,6 +364,7 @@ exports.QueryJob = QueryJob;
async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
const Customer = { const Customer = {
DisplayName: job.ro_number, DisplayName: job.ro_number,
BillWithParent: true,
BillAddr: { BillAddr: {
City: job.ownr_city, City: job.ownr_city,
Line1: job.ownr_addr1, Line1: job.ownr_addr1,

View File

@@ -37,14 +37,15 @@ exports.default = async (req, res) => {
.request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, { .request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, {
bills: billsToQuery, bills: billsToQuery,
}); });
const { bills } = result; const { bills, bodyshops } = result;
const bodyshop = bodyshops[0];
const QbXmlToExecute = []; const QbXmlToExecute = [];
bills.map((i) => { bills.map((i) => {
QbXmlToExecute.push({ QbXmlToExecute.push({
id: i.id, id: i.id,
okStatusCodes: ["0"], okStatusCodes: ["0"],
qbxml: generateBill(i), qbxml: generateBill(i, bodyshop),
}); });
}); });
@@ -62,7 +63,7 @@ exports.default = async (req, res) => {
} }
}; };
const generateBill = (bill) => { const generateBill = (bill, bodyshop) => {
const billQbxmlObj = { const billQbxmlObj = {
QBXML: { QBXML: {
QBXMLMsgsRq: { QBXMLMsgsRq: {
@@ -87,7 +88,7 @@ const generateBill = (bill) => {
ExpenseLineAdd: bill.billlines.map((il) => ExpenseLineAdd: bill.billlines.map((il) =>
generateBillLine( generateBillLine(
il, il,
bill.job.bodyshop.md_responsibility_centers, bodyshop.md_responsibility_centers,
bill.job.class bill.job.class
) )
), ),

View File

@@ -217,6 +217,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
accountingconfig accountingconfig
md_ins_cos md_ins_cos
timezone timezone
md_ro_statuses
} }
} }
`; `;
@@ -408,6 +409,13 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
exports.QUERY_BILLS_FOR_PAYABLES_EXPORT = ` exports.QUERY_BILLS_FOR_PAYABLES_EXPORT = `
query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) { query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) {
bodyshops(where: {associations: {active: {_eq: true}}}) {
id
md_responsibility_centers
timezone
region_config
accountingconfig
}
bills(where: {id: {_in: $bills}}) { bills(where: {id: {_in: $bills}}) {
id id
date date
@@ -424,12 +432,6 @@ query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) {
ownr_ln ownr_ln
ownr_co_nm ownr_co_nm
class class
bodyshop{
md_responsibility_centers
timezone
region_config
accountingconfig
}
} }
billlines{ billlines{
id id
@@ -1482,6 +1484,7 @@ mutation MARK_JOB_EXPORTED($jobId: uuid!, $job: jobs_set_input!, $log: exportlog
} }
} }
`; `;
exports.INSERT_EXPORT_LOG = ` exports.INSERT_EXPORT_LOG = `
mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) { mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
insert_exportlog_one(object: $log) { insert_exportlog_one(object: $log) {
@@ -1535,3 +1538,51 @@ exports.QUERY_JOB_ID_MIXDATA = `query QUERY_JOB_ID_MIXDATA($roNumbers: [String!]
} }
`; `;
exports.QBO_MARK_JOB_EXPORTED = `
mutation QBO_MARK_JOB_EXPORTED($jobId: uuid!, $job: jobs_set_input!, $logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
update_jobs(where: {id: {_eq: $jobId}}, _set: $job) {
returning {
id
}
}
}
`;
exports.QBO_MARK_BILL_EXPORTED = `
mutation QBO_MARK_BILL_EXPORTED($billId: uuid!, $bill: bills_set_input!, $logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
update_bills(where: { id: { _eq: $billId } }, _set: $bill) {
returning {
id
}
}
}
`;
exports.QBO_MARK_PAYMENT_EXPORTED = `
mutation QBO_MARK_PAYMENT_EXPORTED($paymentId: uuid!, $payment: payments_set_input!, $logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
update_payments(where: {id: {_eq: $paymentId}}, _set: $payment) {
returning {
id
}
}
}`;
exports.INSERT_EXPORT_LOG = `
mutation INSERT_EXPORT_LOG($logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
}
`;