Added parts backorder + receiving for orders BOD-159

This commit is contained in:
Patrick Fic
2020-06-25 10:03:46 -07:00
parent db08be58ac
commit 2e30a9078e
21 changed files with 852 additions and 76 deletions

File diff suppressed because one or more lines are too long

View File

@@ -14513,6 +14513,53 @@
<folder_node> <folder_node>
<name>parts_orders</name> <name>parts_orders</name>
<children> <children>
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>backordered</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>receive</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>errors</name> <name>errors</name>
<children> <children>
@@ -14689,6 +14736,69 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>oem_partno</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>order_date</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>order_number</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>quantity</name> <name>quantity</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -14710,6 +14820,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>status</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>

View File

@@ -8,6 +8,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import queryString from "query-string";
import { useLocation } from "react-router-dom";
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
@@ -22,7 +24,6 @@ export function InvoicesListTableComponent({
job, job,
loading, loading,
invoicesQuery, invoicesQuery,
selectedInvoice,
handleOnRowClick, handleOnRowClick,
setPartsOrderContext, setPartsOrderContext,
setInvoiceEnterContext, setInvoiceEnterContext,
@@ -32,6 +33,8 @@ export function InvoicesListTableComponent({
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
}); });
const search = queryString.parse(useLocation().search);
const selectedInvoice = search.invoiceid;
const invoices = invoicesQuery.data ? invoicesQuery.data.invoices : []; const invoices = invoicesQuery.data ? invoicesQuery.data.invoices : [];
const { refetch } = invoicesQuery; const { refetch } = invoicesQuery;
@@ -91,7 +94,8 @@ export function InvoicesListTableComponent({
render: (text, record) => ( render: (text, record) => (
<div> <div>
<Link <Link
to={`/manage/invoices?invoiceid=${record.id}&vendorid=${record.vendorid}`}> to={`/manage/invoices?invoiceid=${record.id}&vendorid=${record.vendorid}`}
>
<Button>{t("invoices.actions.edit")}</Button> <Button>{t("invoices.actions.edit")}</Button>
</Link> </Link>
<Button <Button
@@ -116,7 +120,8 @@ export function InvoicesListTableComponent({
isReturn: true, isReturn: true,
}, },
}) })
}> }
>
{t("invoices.actions.return")} {t("invoices.actions.return")}
</Button> </Button>
</div> </div>
@@ -232,11 +237,11 @@ export function InvoicesListTableComponent({
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
<Table <Table
size='small' size="small"
scroll={{ x: "50%", y: "40rem" }} scroll={{ x: "50%", y: "40rem" }}
pagination={{ position: "top", defaultPageSize: 25 }} pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns} columns={columns}
rowKey='id' rowKey="id"
dataSource={record.invoicelines} dataSource={record.invoicelines}
/> />
</div> </div>
@@ -246,9 +251,9 @@ export function InvoicesListTableComponent({
return ( return (
<Table <Table
loading={loading} loading={loading}
size='small' size="small"
title={() => ( title={() => (
<div className='imex-table-header'> <div className="imex-table-header">
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
@@ -260,7 +265,8 @@ export function InvoicesListTableComponent({
job, job,
}, },
}); });
}}> }}
>
{t("jobs.actions.postInvoices")} {t("jobs.actions.postInvoices")}
</Button> </Button>
<Button <Button
@@ -273,10 +279,11 @@ export function InvoicesListTableComponent({
(invoicesQuery.data && invoicesQuery.data.invoices) || [], (invoicesQuery.data && invoicesQuery.data.invoices) || [],
}, },
}); });
}}> }}
>
{t("jobs.actions.reconcile")} {t("jobs.actions.reconcile")}
</Button>{" "} </Button>{" "}
<div className='imex-table-header__search'> <div className="imex-table-header__search">
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onChange={(e) => { onChange={(e) => {
@@ -290,7 +297,7 @@ export function InvoicesListTableComponent({
expandedRowRender={rowExpander} expandedRowRender={rowExpander}
pagination={{ position: "top", defaultPageSize: 25 }} pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns} columns={columns}
rowKey='id' rowKey="id"
dataSource={invoices} dataSource={invoices}
onChange={handleTableChange} onChange={handleTableChange}
expandable={{ expandable={{

View File

@@ -1,3 +1,4 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Dropdown, Input, Menu, Table } from "antd"; import { Button, Dropdown, Input, Menu, Table } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -11,6 +12,8 @@ import AllocationsAssignmentContainer from "../allocations-assignment/allocation
import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container"; import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container"; import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container"; import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import queryString from "query-string";
import { useHistory, useLocation } from "react-router-dom";
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setJobLineEditContext: (context) => setJobLineEditContext: (context) =>
@@ -35,6 +38,9 @@ export function JobLinesComponent({
}); });
const { t } = useTranslation(); const { t } = useTranslation();
const search = queryString.parse(useLocation().search);
const history = useHistory();
const columns = [ const columns = [
{ {
title: "#", title: "#",
@@ -213,7 +219,8 @@ export function JobLinesComponent({
actions: { refetch: refetch }, actions: { refetch: refetch },
context: record, context: record,
}); });
}}> }}
>
{t("general.actions.edit")} {t("general.actions.edit")}
</Button> </Button>
<AllocationsAssignmentContainer <AllocationsAssignmentContainer
@@ -237,9 +244,9 @@ export function JobLinesComponent({
const markMenu = ( const markMenu = (
<Menu onClick={handleMark}> <Menu onClick={handleMark}>
<Menu.Item key='PAA'>PAA</Menu.Item> <Menu.Item key="PAA">PAA</Menu.Item>
<Menu.Item key='PAN'>PAN</Menu.Item> <Menu.Item key="PAN">PAN</Menu.Item>
<Menu.Item key='PAL'>PAL</Menu.Item> <Menu.Item key="PAL">PAL</Menu.Item>
</Menu> </Menu>
); );
@@ -248,16 +255,19 @@ export function JobLinesComponent({
<PartsOrderModalContainer /> <PartsOrderModalContainer />
<Table <Table
columns={columns} columns={columns}
rowKey='id' rowKey="id"
loading={loading} loading={loading}
size='small' size="small"
pagination={{ position: "top", defaultPageSize: 50 }} pagination={{ position: "top", defaultPageSize: 50 }}
dataSource={jobLines} dataSource={jobLines}
onChange={handleTableChange} onChange={handleTableChange}
scroll={{ x: true, y: "40rem" }} scroll={{ x: true, y: "40rem" }}
title={() => { title={() => {
return ( return (
<div className='imex-table-header'> <div className="imex-table-header">
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Button <Button
disabled={selectedLines.length > 0 ? false : true} disabled={selectedLines.length > 0 ? false : true}
onClick={() => { onClick={() => {
@@ -268,7 +278,8 @@ export function JobLinesComponent({
linesToOrder: selectedLines, linesToOrder: selectedLines,
}, },
}); });
}}> }}
>
{t("parts.actions.order")} {t("parts.actions.order")}
</Button> </Button>
<Dropdown overlay={markMenu} trigger={["click"]}> <Dropdown overlay={markMenu} trigger={["click"]}>
@@ -284,10 +295,11 @@ export function JobLinesComponent({
actions: { refetch: refetch }, actions: { refetch: refetch },
context: { jobid: jobId }, context: { jobid: jobId },
}); });
}}> }}
>
{t("joblines.actions.new")} {t("joblines.actions.new")}
</Button> </Button>
<div className='imex-table-header__search'> <div className="imex-table-header__search">
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onChange={(e) => { onChange={(e) => {
@@ -300,11 +312,16 @@ export function JobLinesComponent({
); );
}} }}
expandedRowRender={(record) => ( expandedRowRender={(record) => (
<div style={{ margin: 0 }}> <div>
<strong>{t("parts_orders.labels.orderhistory")}</strong> <strong>{t("parts_orders.labels.orderhistory")}</strong>
{record.parts_order_lines.map((item) => ( {record.parts_order_lines.map((item) => (
<div key={item.id}> <div key={item.id}>
{`${item.parts_order.order_number || ""} from `} <Link
to={`/manage/jobs/${jobId}?tab=partssublet&partsorderid=${item.parts_order.id}`}
>
{item.parts_order.order_number || ""}
</Link>
-
<Link to={`/manage/shop/vendors/${item.parts_order.vendor.id}`}> <Link to={`/manage/shop/vendors/${item.parts_order.vendor.id}`}>
{item.parts_order.vendor.name || ""} {item.parts_order.vendor.name || ""}
</Link> </Link>

View File

@@ -44,7 +44,8 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
let PartnerResponse; let PartnerResponse;
try { try {
PartnerResponse = await axios.post( PartnerResponse = await axios.post(
"http://localhost:1337/qb/", // "http://localhost:1337/qb/",
"http://b47e67f9cbe3.ngrok.io/qb/",
QbXmlResponse.data, QbXmlResponse.data,
{ {
headers: { headers: {

View File

@@ -21,6 +21,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "schedule" })), dispatch(setModalContext({ context: context, modal: "schedule" })),
setInvoiceEnterContext: (context) => setInvoiceEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" })), dispatch(setModalContext({ context: context, modal: "invoiceEnter" })),
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
}); });
export function JobsDetailHeaderActions({ export function JobsDetailHeaderActions({
@@ -29,12 +31,13 @@ export function JobsDetailHeaderActions({
refetch, refetch,
setScheduleContext, setScheduleContext,
setInvoiceEnterContext, setInvoiceEnterContext,
setPaymentContext,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
const history = useHistory(); const history = useHistory();
const statusmenu = ( const statusmenu = (
<Menu key='popovermenu'> <Menu key="popovermenu">
<Menu.Item <Menu.Item
onClick={() => { onClick={() => {
setScheduleContext({ setScheduleContext({
@@ -44,30 +47,43 @@ export function JobsDetailHeaderActions({
job: job, job: job,
}, },
}); });
}}> }}
>
{t("jobs.actions.schedule")} {t("jobs.actions.schedule")}
</Menu.Item> </Menu.Item>
<Menu.Item
<Menu.Item key='cccontract'> key="enterpayments"
onClick={() => {
setPaymentContext({
actions: {},
context: { jobId: job.id },
});
}}
>
{t("menus.header.enterpayment")}
</Menu.Item>
<Menu.Item key="cccontract">
<Link <Link
to={{ to={{
pathname: "/manage/courtesycars/contracts/new", pathname: "/manage/courtesycars/contracts/new",
state: { jobId: job.id }, state: { jobId: job.id },
}}> }}
>
{t("menus.jobsactions.newcccontract")} {t("menus.jobsactions.newcccontract")}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
key='addtoproduction' key="addtoproduction"
disabled={!!!job.converted || !!job.inproduction} disabled={!!!job.converted || !!job.inproduction}
onClick={() => AddToProduction(client, job.id, refetch)}> onClick={() => AddToProduction(client, job.id, refetch)}
>
{t("jobs.actions.addtoproduction")} {t("jobs.actions.addtoproduction")}
</Menu.Item> </Menu.Item>
<Menu.Item key='duplicatejob'> <Menu.Item key="duplicatejob">
<Popconfirm <Popconfirm
title={t("jobs.labels.duplicateconfirm")} title={t("jobs.labels.duplicateconfirm")}
okText='Yes' okText="Yes"
cancelText='No' cancelText="No"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onConfirm={() => onConfirm={() =>
DuplicateJob( DuplicateJob(
@@ -79,12 +95,13 @@ export function JobsDetailHeaderActions({
} }
) )
} }
getPopupContainer={(trigger) => trigger.parentNode}> getPopupContainer={(trigger) => trigger.parentNode}
>
{t("menus.jobsactions.duplicate")} {t("menus.jobsactions.duplicate")}
</Popconfirm> </Popconfirm>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
key='postinvoices' key="postinvoices"
onClick={() => { onClick={() => {
setInvoiceEnterContext({ setInvoiceEnterContext({
actions: { refetch: refetch }, actions: { refetch: refetch },
@@ -92,14 +109,16 @@ export function JobsDetailHeaderActions({
job: job, job: job,
}, },
}); });
}}> }}
>
{t("jobs.actions.postInvoices")} {t("jobs.actions.postInvoices")}
</Menu.Item> </Menu.Item>
<Menu.Item key='closejob'> <Menu.Item key="closejob">
<Link <Link
to={{ to={{
pathname: `/manage/jobs/${job.id}/close`, pathname: `/manage/jobs/${job.id}/close`,
}}> }}
>
{t("menus.jobsactions.closejob")} {t("menus.jobsactions.closejob")}
</Link> </Link>
</Menu.Item> </Menu.Item>
@@ -108,10 +127,11 @@ export function JobsDetailHeaderActions({
); );
return ( return (
<Dropdown <Dropdown
className='imex-flex-row__margin' className="imex-flex-row__margin"
overlay={statusmenu} overlay={statusmenu}
trigger={["click"]} trigger={["click"]}
key='changestatus'> key="changestatus"
>
<Button> <Button>
{t("general.labels.actions")} <DownCircleFilled /> {t("general.labels.actions")} <DownCircleFilled />
</Button> </Button>

View File

@@ -4,6 +4,7 @@ import AlertComponent from "../alert/alert.component";
import InvoicesListTableComponent from "../invoices-list-table/invoices-list-table.component"; import InvoicesListTableComponent from "../invoices-list-table/invoices-list-table.component";
import JobInvoicesTotalsComponent from "../job-invoices-total/job-invoices-total.component"; import JobInvoicesTotalsComponent from "../job-invoices-total/job-invoices-total.component";
import PartsOrderModal from "../parts-order-modal/parts-order-modal.container"; import PartsOrderModal from "../parts-order-modal/parts-order-modal.container";
import { PartsOrderListTableComponent } from "../parts-order-list-table/parts-order-list-table.component";
const tableCol = { const tableCol = {
xs: { xs: {
span: 24, span: 24,
@@ -25,22 +26,29 @@ const totalsCol = {
export default function JobsDetailPliComponent({ export default function JobsDetailPliComponent({
job, job,
invoicesQuery, invoicesQuery,
handleOnRowClick, handleInvoiceOnRowClick,
handlePartsOrderOnRowClick,
selectedInvoice, selectedInvoice,
}) { }) {
return ( return (
<div> <div>
<PartsOrderModal /> <PartsOrderModal />
{invoicesQuery.error ? ( {invoicesQuery.error ? (
<AlertComponent message={invoicesQuery.error.message} type='error' /> <AlertComponent message={invoicesQuery.error.message} type="error" />
) : null} ) : null}
<Row> <Row>
<Col {...tableCol}> <Col {...tableCol}>
<PartsOrderListTableComponent
job={job}
loading={invoicesQuery.loading}
handleOnRowClick={handlePartsOrderOnRowClick}
selectedInvoice={selectedInvoice}
invoicesQuery={invoicesQuery}
/>
<InvoicesListTableComponent <InvoicesListTableComponent
job={job} job={job}
loading={invoicesQuery.loading} loading={invoicesQuery.loading}
handleOnRowClick={handleOnRowClick} handleOnRowClick={handleInvoiceOnRowClick}
selectedInvoice={selectedInvoice}
invoicesQuery={invoicesQuery} invoicesQuery={invoicesQuery}
/> />
</Col> </Col>

View File

@@ -13,7 +13,7 @@ export default function JobsDetailPliContainer({ job }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useHistory(); const history = useHistory();
const handleOnRowClick = (record) => { const handleInvoiceOnRowClick = (record) => {
if (record) { if (record) {
if (record.id) { if (record.id) {
search.invoiceid = record.id; search.invoiceid = record.id;
@@ -25,12 +25,24 @@ export default function JobsDetailPliContainer({ job }) {
} }
}; };
const handlePartsOrderOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsorderid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsorderid;
history.push({ search: queryString.stringify(search) });
}
};
return ( return (
<JobsDetailPliComponent <JobsDetailPliComponent
job={job} job={job}
invoicesQuery={invoicesQuery} invoicesQuery={invoicesQuery}
handleOnRowClick={handleOnRowClick} handleInvoiceOnRowClick={handleInvoiceOnRowClick}
selectedInvoice={search.invoiceid} handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
/> />
); );
} }

View File

@@ -0,0 +1,52 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "antd";
import { useMutation } from "@apollo/react-hooks";
import { MUTATION_BACKORDER_PART_LINE } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function PartsOrderLineBackorderButton({
partsOrderStatus,
partsLineId,
jobLineId,
bodyshop,
}) {
const [loading, setLoading] = useState(false);
const [backorderLine] = useMutation(MUTATION_BACKORDER_PART_LINE);
const { t } = useTranslation();
const isAlreadyBackordered =
bodyshop.md_order_statuses.default_bo === partsOrderStatus;
const handleOnClick = async () => {
setLoading(true);
const result = await backorderLine({
variables: {
jobLineId,
partsLineId,
status: isAlreadyBackordered
? bodyshop.md_order_statuses.default_received || "Received*"
: bodyshop.md_order_statuses.default_bo || "Backordered*",
},
});
setLoading(false);
};
return (
<Button loading={loading} onClick={handleOnClick}>
{isAlreadyBackordered
? t("parts_orders.actions.receive")
: t("parts_orders.actions.backordered")}
</Button>
);
}
export default connect(mapStateToProps, null)(PartsOrderLineBackorderButton);

View File

@@ -0,0 +1,215 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Input, Table } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
const mapDispatchToProps = (dispatch) => ({});
export function PartsOrderListTableComponent({
job,
loading,
invoicesQuery,
handleOnRowClick,
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const parts_orders = invoicesQuery.data
? invoicesQuery.data.parts_orders
: [];
const { refetch } = invoicesQuery;
const columns = [
{
title: t("vendors.fields.name"),
dataIndex: "vendorname",
key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder:
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
render: (text, record) => <span>{record.vendor.name}</span>,
},
{
title: t("parts_orders.fields.order_number"),
dataIndex: "order_number",
key: "order_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder:
state.sortedInfo.columnKey === "invoice_number" &&
state.sortedInfo.order,
},
{
title: t("parts_orders.fields.order_date"),
dataIndex: "order_date",
key: "order_date",
sorter: (a, b) => a.order_date - b.order_date,
sortOrder:
state.sortedInfo.columnKey === "order_date" && state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.order_date}</DateFormatter>
),
},
{
title: t("parts_orders.fields.deliver_by"),
dataIndex: "deliver_by",
key: "deliver_by",
sorter: (a, b) => a.deliver_by - b.deliver_by,
sortOrder:
state.sortedInfo.columnKey === "deliver_by" && state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.deliver_by}</DateFormatter>
),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const rowExpander = (record) => {
const columns = [
{
title: t("parts_orders.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.db_price"),
dataIndex: "db_price",
key: "db_price",
sorter: (a, b) => a.db_price - b.db_price,
sortOrder:
state.sortedInfo.columnKey === "db_price" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.db_price}</CurrencyFormatter>
),
},
{
title: t("parts_orders.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
),
},
{
title: t("parts_orders.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.line_remarks"),
dataIndex: "line_remarks",
key: "line_remarks",
},
{
title: t("parts_orders.fields.status"),
dataIndex: "status",
key: "status",
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<div>
<PartsOrderLineBackorderButton
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</div>
),
},
];
return (
<div>
<Table
size="small"
scroll={{ x: "50%", y: "40rem" }}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={record.parts_order_lines}
/>
</div>
);
};
return (
<Table
loading={loading}
size="small"
title={() => (
<div className="imex-table-header">
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<div className="imex-table-header__search">
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
e.preventDefault();
}}
/>
</div>
</div>
)}
scroll={{ x: "50%", y: "40rem" }}
expandedRowRender={rowExpander}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={parts_orders}
onChange={handleTableChange}
expandable={{
expandedRowKeys: [selectedpartsorder],
onExpand: (expanded, record) => {
handleOnRowClick(expanded ? record : null);
},
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selectedpartsorder],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
onDoubleClick: (event) => {}, // double click row
onContextMenu: (event) => {}, // right button click row
onMouseEnter: (event) => {}, // mouse enter row
onMouseLeave: (event) => {}, // mouse leave row
};
}}
/>
);
}
export default connect(null, mapDispatchToProps)(PartsOrderListTableComponent);

View File

@@ -1,6 +1,6 @@
import { useElements, useStripe, CardElement } from "@stripe/react-stripe-js"; import { useElements, useStripe, CardElement } from "@stripe/react-stripe-js";
import { Form, Modal, notification } from "antd"; import { Form, Modal, notification } from "antd";
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -37,6 +37,7 @@ function InvoiceEnterModalContainer({
const elements = useElements(); const elements = useElements();
const { t } = useTranslation(); const { t } = useTranslation();
const { context, actions, visible } = paymentModal; const { context, actions, visible } = paymentModal;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const stripeStateArr = useState({ const stripeStateArr = useState({
error: null, error: null,
@@ -118,6 +119,10 @@ function InvoiceEnterModalContainer({
toggleModalVisible(); toggleModalVisible();
}; };
useEffect(() => {
if (visible) form.resetFields();
}, [visible, form]);
return ( return (
<Modal <Modal
title={t("payments.labels.new")} title={t("payments.labels.new")}

View File

@@ -36,11 +36,6 @@ export function ProductionBoardKanbanComponent({ data, bodyshop }) {
bodyshop.md_ro_statuses.production_statuses, bodyshop.md_ro_statuses.production_statuses,
]); ]);
const findById = (id) => {
return id;
//return (data && data.find((x) => x.id === id).ro_number) || null;
};
const client = useApolloClient(); const client = useApolloClient();
const handleDragEnd = async (card, source, destination) => { const handleDragEnd = async (card, source, destination) => {

View File

@@ -1,6 +1,6 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { Button, Form, Input, InputNumber, Select, Row, Col } from "antd"; import { Button, Col, Form, Input, InputNumber, Row } from "antd";
import React, { useState } from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
//TODO Fix up styles. //TODO Fix up styles.
export default function ShopInfoSchedulingComponent({ form }) { export default function ShopInfoSchedulingComponent({ form }) {

View File

@@ -38,7 +38,32 @@ export const QUERY_ALL_INVOICES_PAGINATED = gql`
`; `;
export const QUERY_INVOICES_BY_JOBID = gql` export const QUERY_INVOICES_BY_JOBID = gql`
query QUERY_INVOICES_BY_JOBID($jobid: uuid!) { query QUERY_PARTS_INVOICES_BY_JOBID($jobid: uuid!) {
parts_orders(
where: { jobid: { _eq: $jobid } }
order_by: { order_date: desc }
) {
id
vendor {
id
name
}
order_date
deliver_by
parts_order_lines {
id
act_price
db_price
line_desc
oem_partno
status
line_remarks
quantity
job_line_id
}
order_number
user_email
}
invoices(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) { invoices(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) {
id id
vendorid vendorid

View File

@@ -9,3 +9,30 @@ export const INSERT_NEW_PARTS_ORDERS = gql`
} }
} }
`; `;
export const MUTATION_BACKORDER_PART_LINE = gql`
mutation MUTATION_BACKORDER_PART_LINE(
$jobLineId: uuid!
$partsLineId: uuid!
$status: String!
) {
update_parts_order_lines(
where: { id: { _eq: $partsLineId } }
_set: { status: $status }
) {
returning {
status
id
}
}
update_joblines(
where: { id: { _eq: $jobLineId } }
_set: { status: $status }
) {
returning {
status
id
}
}
}
`;

View File

@@ -899,6 +899,10 @@
} }
}, },
"parts_orders": { "parts_orders": {
"actions": {
"backordered": "Backordered",
"receive": "Receive"
},
"errors": { "errors": {
"creating": "Error encountered when creating parts order. " "creating": "Error encountered when creating parts order. "
}, },
@@ -910,7 +914,11 @@
"line_desc": "Line Description", "line_desc": "Line Description",
"line_remarks": "Remarks", "line_remarks": "Remarks",
"lineremarks": "Line Remarks", "lineremarks": "Line Remarks",
"quantity": "Qty." "oem_partno": "Part #",
"order_date": "Order Date",
"order_number": "Order Number",
"quantity": "Qty.",
"status": "Status"
}, },
"labels": { "labels": {
"email": "Send by Email", "email": "Send by Email",

View File

@@ -899,6 +899,10 @@
} }
}, },
"parts_orders": { "parts_orders": {
"actions": {
"backordered": "",
"receive": ""
},
"errors": { "errors": {
"creating": "Se encontró un error al crear el pedido de piezas." "creating": "Se encontró un error al crear el pedido de piezas."
}, },
@@ -910,7 +914,11 @@
"line_desc": "", "line_desc": "",
"line_remarks": "", "line_remarks": "",
"lineremarks": "Comentarios de línea", "lineremarks": "Comentarios de línea",
"quantity": "" "oem_partno": "",
"order_date": "",
"order_number": "",
"quantity": "",
"status": ""
}, },
"labels": { "labels": {
"email": "Enviar por correo electrónico", "email": "Enviar por correo electrónico",

View File

@@ -899,6 +899,10 @@
} }
}, },
"parts_orders": { "parts_orders": {
"actions": {
"backordered": "",
"receive": ""
},
"errors": { "errors": {
"creating": "Erreur rencontrée lors de la création de la commande de pièces." "creating": "Erreur rencontrée lors de la création de la commande de pièces."
}, },
@@ -910,7 +914,11 @@
"line_desc": "", "line_desc": "",
"line_remarks": "", "line_remarks": "",
"lineremarks": "Remarques sur la ligne", "lineremarks": "Remarques sur la ligne",
"quantity": "" "oem_partno": "",
"order_date": "",
"order_number": "",
"quantity": "",
"status": ""
}, },
"labels": { "labels": {
"email": "Envoyé par email", "email": "Envoyé par email",

View File

@@ -36,22 +36,20 @@
data = data.replace(/\s/g, " "); data = data.replace(/\s/g, " ");
var track = data.match(/(.*?\?)(.*?\?)(.*?\?)/); var track = data.match(/(.*?\?)(.*?\?)(.*?\?)/);
var res1 = track[1].match( var res1 = track[1].match(
/(\%)([A-Z]{2})([^\^]{0,13})\^?([^\^]{0,35})\^?([^\^]{0,60})\^?\s*?\?/ /(%)([A-Z]{2})([^^]{0,13})\^?([^^]{0,35})\^?([^^]{0,60})\^?\s*?\?/
); );
var res2 = track[2].match( var res2 = track[2].match(
/(;)(\d{6})(\d{0,13})(\=)(\d{4})(\d{8})(\d{0,5})\=?\?/ /(;)(\d{6})(\d{0,13})(=)(\d{4})(\d{8})(\d{0,5})=?\?/
); );
var res3 = track[3].match( var res3 = track[3].match(
/(\#|\%|\+)(\d|\!|\")(\d|\s|.)([0-9A-Z ]{11})([0-9A-Z ]{2})([0-9A-Z ]{10})([0-9A-Z ]{4})([12MF ]{1})([0-9A-Z ]{3})([0-9A-Z ]{3})([0-9A-Z ]{3})([0-9A-Z ]{3})(.*?)\?/ /(#|%|\+)(\d|!|")(\d|\s|.)([0-9A-Z ]{11})([0-9A-Z ]{2})([0-9A-Z ]{10})([0-9A-Z ]{4})([12MF ]{1})([0-9A-Z ]{3})([0-9A-Z ]{3})([0-9A-Z ]{3})([0-9A-Z ]{3})(.*?)\?/
); );
var state = res1[2]; var state = res1[2];
return { return {
state: state, state: state,
city: res1[3], city: res1[3],
name: (function () { name: (function () {
var res = res1[4].match( var res = res1[4].match(/([^$]{0,35})\$?([^$]{0,35})?\$?([^$]{0,35})?/);
/([^\$]{0,35})\$?([^\$]{0,35})?\$?([^\$]{0,35})?/
);
if (!res) return; if (!res) return;
return { return {
last: res[1], last: res[1],
@@ -90,13 +88,12 @@
switch (Number(res3[8])) { switch (Number(res3[8])) {
case 1: case 1:
return "MALE"; return "MALE";
break;
case 2: case 2:
return "FEMALE"; return "FEMALE";
break;
default: default:
return "MISSING/INVALID"; return "MISSING/INVALID";
break;
} }
})(), })(),
height: res3[9], height: res3[9],
@@ -452,7 +449,7 @@
var parsedData = {}; var parsedData = {};
var res = data.match(parseRegex); var res = data.match(parseRegex);
for (var i = 1; i < res.length; i++) { for (i = 1; i < res.length; i++) {
if (res[i] !== undefined) { if (res[i] !== undefined) {
parsedData[String(res[i]).substring(0, 3)] = res[i].substring(3).trim(); parsedData[String(res[i]).substring(0, 3)] = res[i].substring(3).trim();
} }
@@ -525,10 +522,10 @@
switch (Number(parsedData.DBC)) { switch (Number(parsedData.DBC)) {
case 1: case 1:
return "MALE"; return "MALE";
break;
case 2: case 2:
return "FEMALE"; return "FEMALE";
break;
default: default:
if (parsedData.DBC[0] === "M") { if (parsedData.DBC[0] === "M") {
return "MALE"; return "MALE";
@@ -536,7 +533,6 @@
return "FEMALE"; return "FEMALE";
} }
return "MISSING/INVALID"; return "MISSING/INVALID";
break;
} }
})(), })(),
height: undefined, height: undefined,

View File

@@ -1 +1 @@
exports.DineroQbFormat = "0,0.00"; exports.DineroQbFormat = "0.00";

View File

@@ -72,13 +72,224 @@ exports.default = async (req, res) => {
qbxml: generateInvoiceQbxml(jobs_by_pk, bodyshop), qbxml: generateInvoiceQbxml(jobs_by_pk, bodyshop),
}); });
res.status(200).json(QbXmlToExecute); res.status(200).json([{ id: jobId, okStatusCodes: ["0"], qbxml: t }]);
} catch (error) { } catch (error) {
console.log("error", error); console.log("error", error);
res.status(400).send(JSON.stringify(error)); res.status(400).send(JSON.stringify(error));
} }
}; };
const t = `<?xml version="1.0" encoding="utf-8"?>
<?qbxml version="13.0"?>
<QBXML>
<QBXMLMsgsRq onError="continueOnError">
<CustomerAddRq>
<CustomerAdd>
<Name>Insurance Corporation of British Co</Name>
<BillAddress>
<Addr1>21291 122B AVE</Addr1>
<City>MAPLE RIDGE</City>
<State>BC</State>
</BillAddress>
</CustomerAdd>
</CustomerAddRq>
<CustomerAddRq>
<CustomerAdd>
<Name>CLOVER LANDON #4</Name>
<ParentRef>
<FullName>Insurance Corporation of British Co</FullName>
</ParentRef>
</CustomerAdd>
</CustomerAddRq>
<CustomerAddRq>
<CustomerAdd>
<Name>RO29</Name>
<ParentRef>
<FullName>Insurance Corporation of British Co:CLOVER LANDON #4</FullName>
</ParentRef>
</CustomerAdd>
</CustomerAddRq>
<InvoiceAddRq>
<InvoiceAdd>
<CustomerRef>
<FullName>Insurance Corporation of British Co:CLOVER LANDON #4:RO29</FullName>
</CustomerRef>
<TxnDate/>
<RefNumber>RO29</RefNumber>
<BillAddress>
<Addr1>21291 122B AVE</Addr1>
<City>MAPLE RIDGE</City>
<State>BC</State>
</BillAddress>
<PONumber>BM27914-1-A</PONumber>
<InvoiceLineAdd>
<ItemRef>
<FullName>BODY SHOP_PAA</FullName>
</ItemRef>
<Desc>Aftermarketd</Desc>
<Quantity>1</Quantity>
<Amount>1351.03</Amount>
<SalesTaxCodeRef>
<FullName>E</FullName>
</SalesTaxCodeRef>
</InvoiceLineAdd>
<InvoiceLineAdd>
<ItemRef>
<FullName>BODY SHOP_PAN</FullName>
</ItemRef>
<Desc>BODY SHOP SALES:PARTS:OEM</Desc>
<Quantity>1</Quantity>
<Amount>292.45</Amount>
<SalesTaxCodeRef>
<FullName>E</FullName>
</SalesTaxCodeRef>
</InvoiceLineAdd>
<InvoiceLineAdd>
<ItemRef>
<FullName>BODY SHOP_ATP</FullName>
</ItemRef>
<Desc>ATPd</Desc>
<Quantity>1</Quantity>
<Amount>144.09</Amount>
<SalesTaxCodeRef>
<FullName>E</FullName>
</SalesTaxCodeRef>
</InvoiceLineAdd>
<InvoiceLineAdd>
<ItemRef>
<FullName>BODY SHOP_LAB</FullName>
</ItemRef>
<Desc>BODY SHOP SALESLABOR:BODY</Desc>
<Quantity>1</Quantity>
<Amount>653.35</Amount>
<SalesTaxCodeRef>
<FullName>E</FullName>
</SalesTaxCodeRef>
</InvoiceLineAdd>
<InvoiceLineAdd>
<ItemRef>
<FullName>BODY SHOP_LAR</FullName>
</ItemRef>
<Desc>BODY SHOP SALES:LABOR:REFINISH</Desc>
<Quantity>1</Quantity>
<Amount>565.26</Amount>
<SalesTaxCodeRef>
<FullName>E</FullName>
</SalesTaxCodeRef>
</InvoiceLineAdd>
<InvoiceLineAdd>
<ItemRef>
<FullName>BODY SHOP_MAPA</FullName>
</ItemRef>
<Desc>paintd</Desc>
<Quantity>1</Quantity>
<Amount>347.66</Amount>
<SalesTaxCodeRef>
<FullName>E</FullName>
</SalesTaxCodeRef>
</InvoiceLineAdd>
<InvoiceLineAdd>
<ItemRef>
<FullName>BODY SHOP_MASH</FullName>
</ItemRef>
<Desc>shopd</Desc>
<Quantity>1</Quantity>
<Amount>54.38</Amount>
<SalesTaxCodeRef>
<FullName>E</FullName>
</SalesTaxCodeRef>
</InvoiceLineAdd>
<InvoiceLineAdd>
<ItemRef>
<FullName>GST On Sales</FullName>
</ItemRef>
<Desc>Receiver General - GST</Desc>
<Amount>170.41</Amount>
</InvoiceLineAdd>
<InvoiceLineAdd>
<ItemRef>
<FullName>PST On Sales</FullName>
</ItemRef>
<Desc>Ministry of Finance (BC)</Desc>
<Amount>238.58</Amount>
</InvoiceLineAdd>
</InvoiceAdd>
</InvoiceAddRq>
</QBXMLMsgsRq>
</QBXML>
`;
// exports.default = async (req, res) => {
// const BearerToken = req.headers.authorization;
// const { jobId } = req.body;
// const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
// headers: {
// Authorization: BearerToken,
// },
// });
// try {
// const result = await client
// .setHeaders({ Authorization: BearerToken })
// .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { id: jobId });
// const { jobs_by_pk } = result;
// const { bodyshop } = jobs_by_pk;
// const QbXmlToExecute = [];
// //Is this a two tier, or 3 tier setup?
// const isThreeTier = bodyshop.accountingconfig.tiers === 3;
// const twoTierPref = bodyshop.accountingconfig.twotierpref;
// if (isThreeTier) {
// QbXmlToExecute.push({
// id: jobId,
// okStatusCodes: ["0", "3100"],
// qbxml: generateSourceCustomerQbxml(jobs_by_pk, bodyshop), // Create the source customer.
// });
// }
// QbXmlToExecute.push({
// id: jobId,
// okStatusCodes: ["0", "3100"],
// qbxml: generateJobQbxml(
// jobs_by_pk,
// bodyshop,
// isThreeTier,
// 2,
// twoTierPref
// ),
// });
// QbXmlToExecute.push({
// id: jobId,
// okStatusCodes: ["0", "3100"],
// qbxml: generateJobQbxml(
// jobs_by_pk,
// bodyshop,
// isThreeTier,
// 3,
// twoTierPref
// ),
// });
// //Generate the actual invoice.
// QbXmlToExecute.push({
// id: jobId,
// okStatusCodes: ["0"],
// qbxml: generateInvoiceQbxml(jobs_by_pk, bodyshop),
// });
// res.status(200).json(QbXmlToExecute);
// } catch (error) {
// console.log("error", error);
// res.status(400).send(JSON.stringify(error));
// }
// };
const generateSourceCustomerQbxml = (jobs_by_pk, bodyshop) => { const generateSourceCustomerQbxml = (jobs_by_pk, bodyshop) => {
const customerQbxmlObj = { const customerQbxmlObj = {
QBXML: { QBXML: {
@@ -129,7 +340,13 @@ const generateOwnerTier = (jobs_by_pk) => {
}`; }`;
}; };
const generateJobQbxml = (jobs_by_pk, bodyshop, isThreeTier, tierLevel, twoTierPref) => { const generateJobQbxml = (
jobs_by_pk,
bodyshop,
isThreeTier,
tierLevel,
twoTierPref
) => {
let Name; let Name;
let ParentRefName; let ParentRefName;