diff --git a/client/package.json b/client/package.json index 3ec7c8907..9dcddf8ef 100644 --- a/client/package.json +++ b/client/package.json @@ -12,10 +12,10 @@ "@openreplay/tracker-assist": "^3.1.1", "@openreplay/tracker-graphql": "^3.0.0", "@openreplay/tracker-redux": "^3.0.0", - "@sentry/react": "^6.13.0", - "@sentry/tracing": "^6.13.0", - "@stripe/react-stripe-js": "^1.4.0", - "@stripe/stripe-js": "^1.17.1", + "@sentry/react": "^6.13.2", + "@sentry/tracing": "^6.13.2", + "@stripe/react-stripe-js": "^1.5.0", + "@stripe/stripe-js": "^1.18.0", "@tanem/react-nprogress": "^3.0.79", "antd": "^4.16.13", "apollo-link-logger": "^2.0.0", @@ -27,10 +27,10 @@ "env-cmd": "^10.1.0", "exifr": "^7.1.3", "firebase": "^9.0.2", - "graphql": "^15.5.3", - "i18next": "^21.0.0", + "graphql": "^15.6.0", + "i18next": "^21.0.2", "i18next-browser-languagedetector": "^6.1.2", - "jsoneditor": "^9.5.4", + "jsoneditor": "^9.5.6", "jsreport-browser-client-dist": "^1.3.0", "libphonenumber-js": "^1.9.34", "logrocket": "^2.0.0", @@ -59,13 +59,13 @@ "react-scripts": "^4.0.3", "react-sublime-video": "^0.2.5", "react-virtualized": "^9.22.3", - "recharts": "^2.1.3", + "recharts": "^2.1.4", "redux": "^4.1.1", "redux-persist": "^6.0.0", "redux-saga": "^1.1.3", "redux-state-sync": "^3.1.2", "reselect": "^4.0.0", - "sass": "^1.41.1", + "sass": "^1.42.1", "socket.io-client": "^4.2.0", "styled-components": "^5.3.1", "subscriptions-transport-ws": "^0.9.18", diff --git a/client/yarn.lock b/client/yarn.lock index 4d60fc2b2..3f4067a21 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2178,14 +2178,14 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@sentry/browser@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.13.0.tgz#fddcf1997d47c166a86494a6ea594f4cc60e773d" - integrity sha512-Eh0k2qYhqWiEP3N04AwSrpl4VRD0pzt6SRgJxgiGzSvBT43EOjyNQ3xzMylAechfjSCTWmzZMvwcgT5fNM9cuw== +"@sentry/browser@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.13.2.tgz#8b731ecf8c3cdd92a4b6893a26f975fd5844056d" + integrity sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag== dependencies: - "@sentry/core" "6.13.0" - "@sentry/types" "6.13.0" - "@sentry/utils" "6.13.0" + "@sentry/core" "6.13.2" + "@sentry/types" "6.13.2" + "@sentry/utils" "6.13.2" tslib "^1.9.3" "@sentry/cli@^1.68.0": @@ -2200,69 +2200,69 @@ progress "^2.0.3" proxy-from-env "^1.1.0" -"@sentry/core@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.13.0.tgz#35df709bb5c1979cc05a4dd0bbe8b15fb973ad6f" - integrity sha512-Aw0ljRJx5tq4w6ZXxvcu2Lr9NwD+MJ1SLL5+oB1hh4OlcOJ7OLwDjtJ3+ZOwp75GCAp7phh4+s/Sql6roX3Lpw== +"@sentry/core@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.13.2.tgz#2ce164f81667aa89cd116f807d772b4718434583" + integrity sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ== dependencies: - "@sentry/hub" "6.13.0" - "@sentry/minimal" "6.13.0" - "@sentry/types" "6.13.0" - "@sentry/utils" "6.13.0" + "@sentry/hub" "6.13.2" + "@sentry/minimal" "6.13.2" + "@sentry/types" "6.13.2" + "@sentry/utils" "6.13.2" tslib "^1.9.3" -"@sentry/hub@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.13.0.tgz#076a5f9b6d0efb8c1832820e3eecd5e24b5e1706" - integrity sha512-BmKgrTyotF008KPfFt1ySyFg0AAMf/ha9Bz9Rmi+usSJjnOLsObzBJQNAozp4Cu6i1kGvj1/R+ymPCD61Gqozw== +"@sentry/hub@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.13.2.tgz#ebc66fd55c96c7686a53ffd3521b6a63f883bb79" + integrity sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A== dependencies: - "@sentry/types" "6.13.0" - "@sentry/utils" "6.13.0" + "@sentry/types" "6.13.2" + "@sentry/utils" "6.13.2" tslib "^1.9.3" -"@sentry/minimal@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.13.0.tgz#276fc0ee5355954c2a9d9292d914988391f8a1e1" - integrity sha512-eJQs44sGY2wFuVDznHMDeShR+ZbnM/KS+T5753nJ4QtMqCNTiG9WDlNXAxwFJ6Q3DORtaxcWHyFZdOMUusJiZQ== +"@sentry/minimal@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.13.2.tgz#de3ecc62b9463bf56ccdbcf4c75f7ea1aeeebc11" + integrity sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw== dependencies: - "@sentry/hub" "6.13.0" - "@sentry/types" "6.13.0" + "@sentry/hub" "6.13.2" + "@sentry/types" "6.13.2" tslib "^1.9.3" -"@sentry/react@^6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.13.0.tgz#110aada56a5f952b63f8ce94ce0c71792bcffd4a" - integrity sha512-Pkkq2YQn7uvqHX+OvoGrd9Nq0ixrQyixm2SI0BjptDXkZ6AqlI+fhhP8Mm+ezBgPT9WqO2+BUW/YivyTjVYwHg== +"@sentry/react@^6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.13.2.tgz#481f1549b1509b4d94eb943934ebeba6430a1a8f" + integrity sha512-aLkWyn697LTcmK1PPnUg5UJcyBUPoI68motqgBY53SIYDAwOeYNUQt2aanDuOTY5aE2PdnJwU48klA8vuYkoRQ== dependencies: - "@sentry/browser" "6.13.0" - "@sentry/minimal" "6.13.0" - "@sentry/types" "6.13.0" - "@sentry/utils" "6.13.0" + "@sentry/browser" "6.13.2" + "@sentry/minimal" "6.13.2" + "@sentry/types" "6.13.2" + "@sentry/utils" "6.13.2" hoist-non-react-statics "^3.3.2" tslib "^1.9.3" -"@sentry/tracing@^6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.13.0.tgz#3d9fac18c2d6829ba14178729d4a7ac770462e3e" - integrity sha512-nTY8UnVK6OUh4ocONtphoaK3WXBZqw0HOFyFpBSzhldo4O1jLad1OEkz8vAxrIE9sXPmLWta5CtKq6PIaFwQSg== +"@sentry/tracing@^6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.13.2.tgz#512389ba459f48ae75e14f1528ab062dc46e4956" + integrity sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw== dependencies: - "@sentry/hub" "6.13.0" - "@sentry/minimal" "6.13.0" - "@sentry/types" "6.13.0" - "@sentry/utils" "6.13.0" + "@sentry/hub" "6.13.2" + "@sentry/minimal" "6.13.2" + "@sentry/types" "6.13.2" + "@sentry/utils" "6.13.2" tslib "^1.9.3" -"@sentry/types@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.13.0.tgz#a8ad870c6ecb407cbe9ca883b0688bacb30daacf" - integrity sha512-04ZVmz4txuI3w1KS81eByppvvMfOINj7jZYnO5zX/S3cjHiOpAJiZkN/k9tTi1Ua3td8bEkQLB6Cxrq9MSiH3Q== +"@sentry/types@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.13.2.tgz#8388d5b92ea8608936e7aae842801dc90e0184e6" + integrity sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg== -"@sentry/utils@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.13.0.tgz#c75647890a5a9dfdb3df321517aa4886c8715b03" - integrity sha512-e82DBwjYqWkNmafIkHnbqirK4t7WCmqCGaoo1Et6vTRCBS4GthWvS6EzaozY7EKs/TzsfIiDdTLGTbYMQOq9Zw== +"@sentry/utils@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.13.2.tgz#fb8010e7b67cc8c084d8067d64ef25289269cda5" + integrity sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w== dependencies: - "@sentry/types" "6.13.0" + "@sentry/types" "6.13.2" tslib "^1.9.3" "@sentry/webpack-plugin@^1.17.1": @@ -2291,17 +2291,17 @@ resolved "https://registry.yarnpkg.com/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz#03ecc29279e3c0c832f6185a5bfa3497858ac8ca" integrity sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw== -"@stripe/react-stripe-js@^1.4.0": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.4.1.tgz#884d59286fff00ba77389b32c045516f65d7a340" - integrity sha512-FjcVrhf72+9fUL3Lz3xi02ni9tzH1A1x6elXlr6tvBDgSD55oPJuodoP8eC7xTnBIKq0olF5uJvgtkJyDCdzjA== +"@stripe/react-stripe-js@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.5.0.tgz#7e4d80077e88e1f2c1f10ac255f2838d7c9488c2" + integrity sha512-A7+bNeb0O/kw3JdtMeiB6frokPcks5obi+TIjuFRXUMZ5o/o1Qe7eLgLnsb0KOO/g3KJqNCpHiYcKCLESZJJbQ== dependencies: prop-types "^15.7.2" -"@stripe/stripe-js@^1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.17.1.tgz#afcb7e86d0b05d1a7af53af89111abd2e8d437ae" - integrity sha512-c9MyDvdi5Xou0j0JPNy86NebtTDfh9o62Ifuzx6GSm2YO0oedBpy51WSyOue2L8Fb+mqESS5gd6mGVEIPUnXsA== +"@stripe/stripe-js@^1.18.0": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.18.0.tgz#687268d7cd68b44b92b86300d7c7f2a6e4df0b98" + integrity sha512-yBRHAMKHnF3kbzv0tpKB82kSow43wW5qXLK8ofg3V9NaaCyObSTO7wJfktWAtG/NBgkJOdUL+pV8dHBj0qvDkQ== "@surma/rollup-plugin-off-main-thread@^1.1.1": version "1.4.2" @@ -2472,41 +2472,41 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== -"@types/d3-color@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" - integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== +"@types/d3-color@^2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82" + integrity sha512-+0EtEjBfKEDtH9Rk3u3kLOUXM5F+iZK+WvASPb0MhIZl8J8NUvGeZRwKCXl+P3HkYx5TdU4YtcibpqHkSR9n7w== -"@types/d3-interpolate@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" - integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== +"@types/d3-interpolate@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz#78eddf7278b19e48e8652603045528d46897aba0" + integrity sha512-lElyqlUfIPyWG/cD475vl6msPL4aMU7eJvx1//Q177L8mdXoVPFl1djIESF2FKnc0NyaHvQlJpWwKJYwAhUoCw== dependencies: - "@types/d3-color" "*" + "@types/d3-color" "^2" -"@types/d3-path@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" - integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== +"@types/d3-path@^2": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-2.0.1.tgz#ca03dfa8b94d8add97ad0cd97e96e2006b4763cb" + integrity sha512-6K8LaFlztlhZO7mwsZg7ClRsdLg3FJRzIIi6SZXDWmmSJc2x8dd2VkESbLXdk3p8cuvz71f36S0y8Zv2AxqvQw== -"@types/d3-scale@^4.0.0": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.1.tgz#fbe8238e2eff27af577d2b7d0b933ae50a546970" - integrity sha512-GDuXcRcR6mKcpUVMhPNttpOzHi2dP6YcDqLZYSZHgwTZ+sfCa8e9q0VEBwZomblAPNMYpVqxojnSyIEb4s/Pwg== +"@types/d3-scale@^3.0.0": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-3.3.2.tgz#18c94e90f4f1c6b1ee14a70f14bfca2bd1c61d06" + integrity sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ== dependencies: - "@types/d3-time" "*" + "@types/d3-time" "^2" -"@types/d3-shape@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" - integrity sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw== +"@types/d3-shape@^2.0.0": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.3.tgz#35d397b9e687abaa0de82343b250b9897b8cacf3" + integrity sha512-HAhCel3wP93kh4/rq+7atLdybcESZ5bRHDEZUojClyZWsRuEMo3A52NGYJSh48SxfxEU6RZIVbZL2YFZ2OAlzQ== dependencies: - "@types/d3-path" "*" + "@types/d3-path" "^2" -"@types/d3-time@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" - integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== +"@types/d3-time@^2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" + integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== "@types/eslint@^7.2.6": version "7.28.0" @@ -5000,76 +5000,76 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -"d3-array@2 - 3", "d3-array@2.10.0 - 3": - version "3.0.4" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.0.4.tgz#60550bcc9818be9ace88d269ccd97038fc399b55" - integrity sha512-ShFl90cxNqDaSynDF/Bik/kTzISqePqU3qo2fv6kSJEvF7y7tDCDpcU6WiT01rPO6zngZnrvJ/0j4q6Qg+5EQg== +d3-array@2, d3-array@^2.3.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== dependencies: - internmap "1 - 2" + internmap "^1.0.0" d3-array@^1.2.0: version "1.2.4" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== -"d3-color@1 - 3": - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" - integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw== +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== -"d3-format@1 - 3": - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.0.1.tgz#e41b81b2ab79277141ec1404aa5d05001da64084" - integrity sha512-hdL7+HBIohpgfolhBxr1KX47VMD6+vVD/oEFrxk5yhmzV2prk99EkFKYpXuhVkFpTgHdJ6/4bYcjdLPPXV4tIA== +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== -"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" - integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== +"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== dependencies: - d3-color "1 - 3" + d3-color "1 - 2" -"d3-path@1 - 3": - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" - integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w== +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== d3-polygon@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e" integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ== -d3-scale@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.1.tgz#c65aa7a357e4f58954a66e1edacc712bab4b1034" - integrity sha512-akUAsUujCFnw6Sf1dF7y/FXTxz+VvEIOB3ValKtLhNrzFp8q5wPO3VCAmsbCLJWRTxyJCZDoooodjOI1plFqlw== +d3-scale@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== dependencies: - d3-array "2.10.0 - 3" - d3-format "1 - 3" - d3-interpolate "1.2.0 - 3" - d3-time "2.1.1 - 3" - d3-time-format "2 - 4" + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" -d3-shape@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.0.1.tgz#9ccdfb28fd9b0d12f2d8aec234cd5c4a9ea27931" - integrity sha512-HNZNEQoDhuCrDWEc/BMbF/hKtzMZVoe64TvisFLDp2Iyj0UShB/E6/lBsLlJTfBMbYgftHj90cXJ0SEitlE6Xw== +d3-shape@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== dependencies: - d3-path "1 - 3" + d3-path "1 - 2" -"d3-time-format@2 - 4": - version "4.0.0" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.0.0.tgz#930ded86a9de761702344760d8a25753467f28b7" - integrity sha512-nzaCwlj+ZVBIlFuVOT1RmU+6xb/7D5IcnhHzHQcBgS/aTa5K9fWZNN5LCXA27LgF5WxoSNJqKBbLcGMtM6Ca6A== - dependencies: - d3-time "1 - 3" - -"d3-time@1 - 3", "d3-time@2.1.1 - 3": +"d3-time-format@2 - 3": version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975" - integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ== + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== dependencies: - d3-array "2 - 3" + d3-time "1 - 2" + +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" d@1, d@^1.0.1: version "1.0.1" @@ -6736,10 +6736,10 @@ graphql-tag@^2.12.3: dependencies: tslib "^2.1.0" -graphql@^15.5.3: - version "15.5.3" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.3.tgz#c72349017d5c9f5446a897fe6908b3186db1da00" - integrity sha512-sM+jXaO5KinTui6lbK/7b7H/Knj9BpjGxZ+Ki35v7YbUJxxdBCUqNM0h3CRVU1ZF9t5lNiBzvBCSYPvIwxPOQA== +graphql@^15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.6.0.tgz#e69323c6a9780a1a4b9ddf7e35ca8904bb04df02" + integrity sha512-WJR872Zlc9hckiEPhXgyUftXH48jp2EjO5tgBBOyNMRJZ9fviL2mJBD6CAysk6N5S0r9BTs09Qk39nnJBkvOXQ== growly@^1.3.0: version "1.3.0" @@ -7093,10 +7093,10 @@ i18next-browser-languagedetector@^6.1.2: dependencies: "@babel/runtime" "^7.14.6" -i18next@^21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.0.0.tgz#ad730aced7df552515c92b318d16d847c6270612" - integrity sha512-17A00z5ASuOJkPUNSUMaZcfL3zOm2qYiN8UBIYhNYNmPbPrcP+IfabGEFEICv8CCStr/cbjF0Jk/IXNX9hj3ZA== +i18next@^21.0.2: + version "21.0.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.0.2.tgz#a394cbd0cc1879a9ad7a4a979a222d12033022cc" + integrity sha512-cSjRitffS1xm90YgPFTaqut89Y4Oc2i4aiCmAuhxHPryYdKzSIGl1YSv8PzlW+UPFOKRIUCOYZ5Rn+aUXKq/qQ== dependencies: "@babel/runtime" "^7.12.0" @@ -7292,10 +7292,10 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -"internmap@1 - 2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" - integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== invariant@^2.2.4: version "2.2.4" @@ -8327,10 +8327,10 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" -jsoneditor@^9.5.4: - version "9.5.5" - resolved "https://registry.yarnpkg.com/jsoneditor/-/jsoneditor-9.5.5.tgz#3275b4b327d51c5d6d30e386d326bed4da012a0b" - integrity sha512-q0bnZF8eyM+pXHfQGkI0ATZWI974eeWVLun3nRl4QP1uvaMlc2w9R5UkPerWcHmuX+p0EQLOw/IOWmRT1qHqHQ== +jsoneditor@^9.5.6: + version "9.5.6" + resolved "https://registry.yarnpkg.com/jsoneditor/-/jsoneditor-9.5.6.tgz#b2abca2aeb34bb5291025d0eeddcb2845e0d90a9" + integrity sha512-smu4CKCOeJiizGGGYQ7ZAvCclnuJP7gX/wcoHw/DWiJMUZq+3KjJNDhYnVTRgi+Zk0UlPngA4egmuJre/H2mXg== dependencies: ace-builds "^1.4.12" ajv "^6.12.6" @@ -11783,18 +11783,18 @@ recharts-scale@^0.4.4: dependencies: decimal.js-light "^2.4.1" -recharts@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.1.3.tgz#f7e82b274a569ca3c9665570bd6d4e78a0e8f24e" - integrity sha512-M0i8x+NKI2Bu/DTGm0LH3FzOhk0YJ4dmBY43YX/KIV82CXfkwkJ07obkje8yJbjdKMjEPZnS5K0XdP6VLx/hPw== +recharts@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.1.4.tgz#8d21750e3bb01a909e01ebe5573c48d813c25287" + integrity sha512-ZKaLgUo4g84FhIiLeRq0uWMqe5GTjjJk+5fxmIt2H4cn279QZCMEsuVFmBqYedJcmX1URYEII5qb9pOppr5fJA== dependencies: - "@types/d3-interpolate" "^3.0.0" - "@types/d3-scale" "^4.0.0" - "@types/d3-shape" "^3.0.0" + "@types/d3-interpolate" "^2.0.0" + "@types/d3-scale" "^3.0.0" + "@types/d3-shape" "^2.0.0" classnames "^2.2.5" - d3-interpolate "^3.0.0" - d3-scale "^4.0.0" - d3-shape "^3.0.0" + d3-interpolate "^2.0.0" + d3-scale "^3.0.0" + d3-shape "^2.0.0" eventemitter3 "^4.0.1" lodash "^4.17.19" react-is "16.10.2" @@ -12258,10 +12258,10 @@ sass-loader@^10.0.5: schema-utils "^3.0.0" semver "^7.3.2" -sass@^1.41.1: - version "1.41.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.41.1.tgz#bca5bed2154192779c29f48fca9c644c60c38d98" - integrity sha512-vIjX7izRxw3Wsiez7SX7D+j76v7tenfO18P59nonjr/nzCkZuoHuF7I/Fo0ZRZPKr88v29ivIdE9BqGDgQD/Nw== +sass@^1.42.1: + version "1.42.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.42.1.tgz#5ab17bebc1cb1881ad2e0c9a932c66ad64e441e2" + integrity sha512-/zvGoN8B7dspKc5mC6HlaygyCBRvnyzzgD5khiaCfglWztY99cYoiTUksVx11NlnemrcfH5CEaCpsUKoW0cQqg== dependencies: chokidar ">=3.0.0 <4.0.0" diff --git a/server.js b/server.js index efe533182..368fdf632 100644 --- a/server.js +++ b/server.js @@ -149,6 +149,7 @@ var qbo = require("./server/accounting/qbo/qbo"); app.post("/qbo/authorize", fb.validateFirebaseIdToken, qbo.authorize); app.get("/qbo/callback", qbo.callback); app.post("/qbo/receivables", fb.validateFirebaseIdToken, qbo.receivables); +app.post("/qbo/payables", fb.validateFirebaseIdToken, qbo.payables); var data = require("./server/data/data"); app.post("/data/ah", data.autohouse); diff --git a/server/accounting/qb-receivables-lines.js b/server/accounting/qb-receivables-lines.js new file mode 100644 index 000000000..f013600a1 --- /dev/null +++ b/server/accounting/qb-receivables-lines.js @@ -0,0 +1,555 @@ +const DineroQbFormat = require("./accounting-constants").DineroQbFormat; + +const Dinero = require("dinero.js"); +const logger = require("../utils/logger"); + +module.exports = function ({ + bodyshop, + jobs_by_pk, + qbo = false, + items, + taxCodes, +}) { + const InvoiceLineAdd = []; + const responsibilityCenters = bodyshop.md_responsibility_centers; + + const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name. + + //Determine if there are MAPA and MASH lines already on the estimate. + //If there are, don't do anything extra (mitchell estimate) + //Otherwise, calculate them and add them to the default MAPA and MASH centers. + let hasMapaLine = false; + let hasMashLine = false; + + //Create the invoice lines mapping. + jobs_by_pk.joblines.map((jobline) => { + if (jobline.db_ref === "936008") { + //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. + hasMapaLine = true; + } + if (jobline.db_ref === "936007") { + hasMashLine = true; + } + //Parts Lines Mappings. + if (jobline.profitcenter_part && jobline.act_price) { + let DineroAmount = Dinero({ + amount: Math.round(jobline.act_price * 100), + }).multiply(jobline.part_qty || 1); + + if (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) { + // console.log("Have a part discount", jobline); + DineroAmount = DineroAmount.add( + DineroAmount.percentage(Math.abs(jobline.prt_dsmk_p || 0)).multiply( + jobline.prt_dsmk_p > 0 ? 1 : -1 + ) + ); + } + const account = responsibilityCenters.profits.find( + (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() + ); + + if (!account) { + logger.log( + "qbxml-receivables-no-account", + "warn", + null, + jobline.id, + null + ); + throw new Error( + `A matching account does not exist for the part allocation. Center: ${jobline.profitcenter_part}` + ); + } + if (qbo) { + //Do the mapping as per QBO. + //Determine the Tax code grouping. + + //Going to always assume that we need to apply GST. + const taxAccountCode = findTaxCode( + { + local: false, + federal: true, + state: jobline.tax_part, + }, + bodyshop.md_responsibility_centers.sales_tax_codes + ); + + const QboTaxId = taxCodes[taxAccountCode]; + if (!invoiceLineHash[account.name]) invoiceLineHash[account.name] = {}; + if (!invoiceLineHash[account.name][QboTaxId]) { + invoiceLineHash[account.name][QboTaxId] = { + DetailType: "SalesItemLineDetail", + Amount: DineroAmount, + SalesItemLineDetail: { + ItemRef: { + value: items[account.accountitem], + }, + TaxCodeRef: { + value: QboTaxId, + }, + Qty: 1, + }, + }; + } else { + invoiceLineHash[account.name][QboTaxId].Amount = + invoiceLineHash[account.name][QboTaxId].Amount.add(DineroAmount); + } + } else { + if (!invoiceLineHash[account.name]) { + invoiceLineHash[account.name] = { + ItemRef: { FullName: account.accountitem }, + Desc: account.accountdesc, + Quantity: 1, //jobline.part_qty, + Amount: DineroAmount, //.toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }; + } else { + invoiceLineHash[account.name].Amount = + invoiceLineHash[account.name].Amount.add(DineroAmount); + } + } + } + //End Parts line mappings. + + // Labor Lines + if (jobline.profitcenter_labor && jobline.mod_lb_hrs) { + const DineroAmount = Dinero({ + amount: Math.round( + jobs_by_pk[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] * 100 + ), + }).multiply(jobline.mod_lb_hrs); + const account = responsibilityCenters.profits.find( + (i) => jobline.profitcenter_labor.toLowerCase() === i.name.toLowerCase() + ); + + if (!account) { + throw new Error( + `A matching account does not exist for the labor allocation. Center: ${jobline.profitcenter_labor}` + ); + } + if (qbo) { + //Going to always assume that we need to apply GST and PST for labor. + const taxAccountCode = findTaxCode( + { + local: false, + federal: true, + state: true, + }, + bodyshop.md_responsibility_centers.sales_tax_codes + ); + + const QboTaxId = taxCodes[taxAccountCode]; + if (!invoiceLineHash[account.name]) invoiceLineHash[account.name] = {}; + if (!invoiceLineHash[account.name][QboTaxId]) { + invoiceLineHash[account.name][QboTaxId] = { + DetailType: "SalesItemLineDetail", + Amount: DineroAmount, + SalesItemLineDetail: { + ItemRef: { + value: items[account.accountitem], + }, + TaxCodeRef: { + value: QboTaxId, + }, + Qty: 1, + }, + }; + } else { + invoiceLineHash[account.name][QboTaxId].Amount = + invoiceLineHash[account.name][QboTaxId].Amount.add(DineroAmount); + } + } else { + if (!invoiceLineHash[account.name]) { + invoiceLineHash[account.name] = { + ItemRef: { FullName: account.accountitem }, + Desc: account.accountdesc, + Quantity: 1, // jobline.mod_lb_hrs, + Amount: DineroAmount, + //Amount: DineroAmount.toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }; + } else { + invoiceLineHash[account.name].Amount = + invoiceLineHash[account.name].Amount.add(DineroAmount); + } + } + } + }); + + if (!hasMapaLine && jobs_by_pk.job_totals.rates.mapa.total.amount > 0) { + // console.log("Adding MAPA Line Manually."); + const mapaAccountName = responsibilityCenters.defaults.profits.MAPA; + + const mapaAccount = responsibilityCenters.profits.find( + (c) => c.name === mapaAccountName + ); + + if (mapaAccount) { + if (qbo) { + //Add QBO MAPA + + //Going to always assume that we need to apply GST and PST for labor. + const taxAccountCode = findTaxCode( + { + local: false, + federal: true, + state: true, + }, + bodyshop.md_responsibility_centers.sales_tax_codes + ); + + const QboTaxId = taxCodes[taxAccountCode]; + if (!invoiceLineHash[mapaAccount.name]) + invoiceLineHash[mapaAccount.name] = {}; + if (!invoiceLineHash[mapaAccount.name][QboTaxId]) { + invoiceLineHash[mapaAccount.name][QboTaxId] = { + DetailType: "SalesItemLineDetail", + Amount: Dinero(jobs_by_pk.job_totals.rates.mapa.total), + SalesItemLineDetail: { + ItemRef: { + value: items[mapaAccount.accountitem], + }, + TaxCodeRef: { + value: QboTaxId, + }, + Qty: 1, + }, + }; + } else { + invoiceLineHash[mapaAccount.name][QboTaxId].Amount = invoiceLineHash[ + mapaAccount.name + ][QboTaxId].Amount.add( + Dinero(jobs_by_pk.job_totals.rates.mapa.total) + ); + } + } else { + InvoiceLineAdd.push({ + ItemRef: { FullName: mapaAccount.accountitem }, + Desc: mapaAccount.accountdesc, + Quantity: 1, + Amount: Dinero(jobs_by_pk.job_totals.rates.mapa.total).toFormat( + DineroQbFormat + ), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } + } else { + //console.log("NO MAPA ACCOUNT FOUND!!"); + } + } + + if (!hasMashLine && jobs_by_pk.job_totals.rates.mash.total.amount > 0) { + // console.log("Adding MASH Line Manually."); + + const mashAccountName = responsibilityCenters.defaults.profits.MASH; + + const mashAccount = responsibilityCenters.profits.find( + (c) => c.name === mashAccountName + ); + + if (mashAccount) { + if (qbo) { + //Going to always assume that we need to apply GST and PST for labor. + const taxAccountCode = findTaxCode( + { + local: false, + federal: true, + state: true, + }, + bodyshop.md_responsibility_centers.sales_tax_codes + ); + + const QboTaxId = taxCodes[taxAccountCode]; + if (!invoiceLineHash[mashAccount.name]) + invoiceLineHash[mashAccount.name] = {}; + if (!invoiceLineHash[mashAccount.name][QboTaxId]) { + invoiceLineHash[mashAccount.name][QboTaxId] = { + DetailType: "SalesItemLineDetail", + Amount: Dinero(jobs_by_pk.job_totals.rates.mash.total), + SalesItemLineDetail: { + ItemRef: { + value: items[mashAccount.accountitem], + }, + TaxCodeRef: { + value: QboTaxId, + }, + Qty: 1, + }, + }; + } else { + invoiceLineHash[mashAccount.name][QboTaxId].Amount = invoiceLineHash[ + mashAccount.name + ][QboTaxId].Amount.add( + Dinero(jobs_by_pk.job_totals.rates.mash.total) + ); + } + } else { + InvoiceLineAdd.push({ + ItemRef: { FullName: mashAccount.accountitem }, + Desc: mashAccount.accountdesc, + Quantity: 1, + Amount: Dinero(jobs_by_pk.job_totals.rates.mash.total).toFormat( + DineroQbFormat + ), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } + } else { + // console.log("NO MASH ACCOUNT FOUND!!"); + } + } + + //Convert the hash to an array. + Object.keys(invoiceLineHash).forEach((key) => { + Object.keys(invoiceLineHash[key]).forEach((key2) => { + InvoiceLineAdd.push({ + ...invoiceLineHash[key][key2], + Amount: invoiceLineHash[key][key2].Amount.toFormat(DineroQbFormat), + }); + }); + }); + + //Add Towing, storage and adjustment lines. + + if (jobs_by_pk.towing_payable && jobs_by_pk.towing_payable !== 0) { + if (qbo) { + //Going to always assume that we need to apply GST and PST for labor. + const taxAccountCode = findTaxCode( + { + local: false, + federal: true, + state: true, + }, + bodyshop.md_responsibility_centers.sales_tax_codes + ); + const account = responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["TOW"] + ); + const QboTaxId = taxCodes[taxAccountCode]; + InvoiceLineAdd.push({ + DetailType: "SalesItemLineDetail", + Amount: Dinero({ + amount: Math.round((jobs_by_pk.towing_payable || 0) * 100), + }).toFormat(DineroQbFormat), + SalesItemLineDetail: { + ItemRef: { + value: items[account.accountitem], + }, + TaxCodeRef: { + value: QboTaxId, + }, + Qty: 1, + }, + }); + } else { + InvoiceLineAdd.push({ + ItemRef: { + FullName: responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["TOW"] + ).accountitem, + }, + Desc: "Towing", + Quantity: 1, + Amount: Dinero({ + amount: Math.round((jobs_by_pk.towing_payable || 0) * 100), + }).toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } + } + if (jobs_by_pk.storage_payable && jobs_by_pk.storage_payable !== 0) { + if (qbo) { + //Going to always assume that we need to apply GST and PST for labor. + const taxAccountCode = findTaxCode( + { + local: false, + federal: true, + state: true, + }, + bodyshop.md_responsibility_centers.sales_tax_codes + ); + const account = responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["TOW"] + ); + const QboTaxId = taxCodes[taxAccountCode]; + InvoiceLineAdd.push({ + DetailType: "SalesItemLineDetail", + Amount: Dinero({ + amount: Math.round((jobs_by_pk.storage_payable || 0) * 100), + }).toFormat(DineroQbFormat), + SalesItemLineDetail: { + ItemRef: { + value: items[account.accountitem], + }, + TaxCodeRef: { + value: QboTaxId, + }, + Qty: 1, + }, + }); + } else { + InvoiceLineAdd.push({ + ItemRef: { + FullName: responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["TOW"] + ).accountitem, + }, + Desc: "Storage", + Quantity: 1, + Amount: Dinero({ + amount: Math.round((jobs_by_pk.storage_payable || 0) * 100), + }).toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } + } + if ( + jobs_by_pk.adjustment_bottom_line && + jobs_by_pk.adjustment_bottom_line !== 0 + ) { + if (qbo) { + //Going to always assume that we need to apply GST and PST for labor. + const taxAccountCode = findTaxCode( + { + local: false, + federal: true, + state: true, + }, + bodyshop.md_responsibility_centers.sales_tax_codes + ); + const account = responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["PAO"] + ); + const QboTaxId = taxCodes[taxAccountCode]; + InvoiceLineAdd.push({ + DetailType: "SalesItemLineDetail", + Amount: Dinero({ + amount: Math.round((jobs_by_pk.adjustment_bottom_line || 0) * 100), + }).toFormat(DineroQbFormat), + SalesItemLineDetail: { + ItemRef: { + value: items[account.accountitem], + }, + TaxCodeRef: { + value: QboTaxId, + }, + Qty: 1, + }, + }); + } else { + InvoiceLineAdd.push({ + ItemRef: { + FullName: responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["PAO"] + ).accountitem, + }, + Desc: "Adjustment", + Quantity: 1, + Amount: Dinero({ + amount: Math.round((jobs_by_pk.adjustment_bottom_line || 0) * 100), + }).toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } + } + + //Add tax lines + const job_totals = jobs_by_pk.job_totals; + + const federal_tax = Dinero(job_totals.totals.federal_tax); + const state_tax = Dinero(job_totals.totals.state_tax); + const local_tax = Dinero(job_totals.totals.local_tax); + + if (federal_tax.getAmount() > 0) { + if (qbo) { + // do qbo + } else { + InvoiceLineAdd.push({ + ItemRef: { + FullName: + bodyshop.md_responsibility_centers.taxes.federal.accountitem, + }, + Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc, + Amount: federal_tax.toFormat(DineroQbFormat), + }); + } + } + + if (state_tax.getAmount() > 0) { + if (qbo) { + // do qbo + } else { + InvoiceLineAdd.push({ + ItemRef: { + FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, + }, + Desc: bodyshop.md_responsibility_centers.taxes.state.accountdesc, + Amount: state_tax.toFormat(DineroQbFormat), + }); + } + } + + if (local_tax.getAmount() > 0) { + if (qbo) { + // do qbo + } else { + InvoiceLineAdd.push({ + ItemRef: { + FullName: bodyshop.md_responsibility_centers.taxes.local.accountitem, + }, + Desc: bodyshop.md_responsibility_centers.taxes.local.accountdesc, + Amount: local_tax.toFormat(DineroQbFormat), + }); + } + } + + //Region Specific + const { ca_bc_pvrt } = jobs_by_pk; + if (ca_bc_pvrt) { + if (qbo) { + // do qbo + } else { + InvoiceLineAdd.push({ + ItemRef: { + FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, + }, + Desc: "PVRT", + Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( + DineroQbFormat + ), + }); + } + } + + return InvoiceLineAdd; +}; + +const findTaxCode = ({ local, state, federal }, taxcode) => { + const t = taxcode.filter( + (t) => + !!t.local === !!local && + !!t.state === !!state && + !!t.federal === !!federal + ); + if (t.length === 1) { + return t[0].code; + } else if (t.length > 1) { + return "Multiple Tax Codes Match"; + } else { + return "No Tax Code Matches"; + } +}; diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js new file mode 100644 index 000000000..4a42c9e87 --- /dev/null +++ b/server/accounting/qbo/qbo-payables.js @@ -0,0 +1,549 @@ +const urlBuilder = require("./qbo").urlBuilder; +const path = require("path"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); +const logger = require("../../utils/logger"); +const Dinero = require("dinero.js"); +const DineroQbFormat = require("../accounting-constants").DineroQbFormat; +const apiGqlClient = require("../../graphql-client/graphql-client").client; +const queries = require("../../graphql-client/queries"); +const { + refresh: refreshOauthToken, + setNewRefreshToken, +} = require("./qbo-callback"); +const OAuthClient = require("intuit-oauth"); + +const GraphQLClient = require("graphql-request").GraphQLClient; +const { generateOwnerTier } = require("../qbxml/qbxml-utils"); + +exports.default = async (req, res) => { + const oauthClient = new OAuthClient({ + clientId: process.env.QBO_CLIENT_ID, + clientSecret: process.env.QBO_SECRET, + environment: + process.env.NODE_ENV === "production" ? "production" : "sandbox", + redirectUri: process.env.QBO_REDIRECT_URI, + logging: true, + }); + try { + //Fetch the API Access Tokens & Set them for the session. + const response = await apiGqlClient.request(queries.GET_QBO_AUTH, { + email: req.user.email, + }); + + oauthClient.setToken(response.associations[0].qbo_auth); + + await refreshOauthToken(oauthClient, req); + + const BearerToken = req.headers.authorization; + const { bills: billsToQuery } = req.body; + //Query Job Info + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { + headers: { + Authorization: BearerToken, + }, + }); + logger.log("qbo-payable-create", "DEBUG", req.user.email, billsToQuery); + const result = await client + .setHeaders({ Authorization: BearerToken }) + .request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, { + bills: billsToQuery, + }); + const { bills } = result; + + for (const bill of bills) { + let vendorRecord; + vendorRecord = await QueryVendorRecord(oauthClient, req, bill); + + if (!vendorRecord) { + vendorRecord = await InsertVendorRecord(oauthClient, req, bill); + } + + const insertResults = await InsertBill(oauthClient, req, bill); + } + + res.json({}); + } catch (error) { + console.log(error); + logger.log("qbo-payable-create-error", "ERROR", req.user.email, { error }); + res.status(400).json(error); + } +}; + +async function QueryVendorRecord(oauthClient, req, bill) { + try { + const result = await oauthClient.makeApiCall({ + url: urlBuilder( + req.cookies.qbo_realmId, + "query", + `select * From vendor where DisplayName = '${bill.vendor.name}'` + ), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + setNewRefreshToken(req.user.email, result); + return ( + result.json && + result.json.QueryResponse && + result.json.QueryResponse.Vendor && + result.json.QueryResponse.Vendor[0] + ); + } catch (error) { + logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { + error, + method: "QueryVendorRecord", + }); + throw error; + } +} +async function InsertVendorRecord(oauthClient, req, bill) { + const Vendor = { + DisplayName: bill.vendor.name, + }; + try { + const result = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "vendor"), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(Vendor), + }); + setNewRefreshToken(req.user.email, result); + return result && result.Vendor; + } catch (error) { + logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { + error, + method: "InsertVendorRecord", + }); + throw error; + } +} + +async function InsertBill(oauthClient, req, bill) { + const vendor = { + DisplayName: job.ro_number, + BillAddr: { + City: job.ownr_city, + Line1: job.ownr_addr1, + Line2: job.ownr_addr2, + PostalCode: job.ownr_zip, + CountrySubDivisionCode: job.ownr_st, + }, + ...(isThreeTier + ? { + Job: true, + ParentRef: { + value: parentTierRef.Id, + }, + } + : {}), + }; + try { + const result = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "vendor"), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(vendor), + }); + setNewRefreshToken(req.user.email, result); + return result && result.Customer; + } catch (error) { + logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { + error, + method: "InsertOwner", + }); + throw error; + } +} + +async function QueryMetaData(oauthClient, req) { + const items = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Item`), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + setNewRefreshToken(req.user.email, items); + const taxCodes = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From TaxCode`), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const taxCodeMapping = {}; + + const accounts = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Account`), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + taxCodes.json && + taxCodes.json.QueryResponse && + taxCodes.json.QueryResponse.TaxCode.forEach((t) => { + taxCodeMapping[t.Name] = t.Id; + }); + + const itemMapping = {}; + + items.json && + items.json.QueryResponse && + items.json.QueryResponse.Item.forEach((t) => { + itemMapping[t.Name] = t.Id; + }); + + return { + items: itemMapping, + taxCodes: taxCodeMapping, + }; +} + +async function InsertInvoice(oauthClient, req, job, bodyshop, parentTierRef) { + const { items, taxCodes } = await QueryMetaData(oauthClient, req); + const InvoiceLineAdd = []; + const responsibilityCenters = bodyshop.md_responsibility_centers; + + const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name. + + //Determine if there are MAPA and MASH lines already on the estimate. + //If there are, don't do anything extra (mitchell estimate) + //Otherwise, calculate them and add them to the default MAPA and MASH centers. + let hasMapaLine = false; + let hasMashLine = false; + + //Create the invoice lines mapping. + job.joblines.map((jobline) => { + //Parts Lines + if (jobline.db_ref === "936008") { + //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. + hasMapaLine = true; + } + if (jobline.db_ref === "936007") { + hasMashLine = true; + } + + if (jobline.profitcenter_part && jobline.act_price) { + let DineroAmount = Dinero({ + amount: Math.round(jobline.act_price * 100), + }).multiply(jobline.part_qty || 1); + + if (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) { + // console.log("Have a part discount", jobline); + DineroAmount = DineroAmount.add( + DineroAmount.percentage(jobline.prt_dsmk_p || 0) + ); + } + const account = responsibilityCenters.profits.find( + (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() + ); + + if (!account) { + logger.log("qbxml-payables-no-account", "warn", null, jobline.id, null); + throw new Error( + `A matching account does not exist for the part allocation. Center: ${jobline.profitcenter_part}` + ); + } + if (!invoiceLineHash[account.name]) { + invoiceLineHash[account.name] = { + ItemRef: { FullName: account.accountitem }, + Desc: account.accountdesc, + Quantity: 1, //jobline.part_qty, + Amount: DineroAmount, //.toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }; + } else { + invoiceLineHash[account.name].Amount = + invoiceLineHash[account.name].Amount.add(DineroAmount); + } + } + // Labor Lines + if (jobline.profitcenter_labor && jobline.mod_lb_hrs) { + const DineroAmount = Dinero({ + amount: Math.round( + job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] * 100 + ), + }).multiply(jobline.mod_lb_hrs); + const account = responsibilityCenters.profits.find( + (i) => jobline.profitcenter_labor.toLowerCase() === i.name.toLowerCase() + ); + + if (!account) { + throw new Error( + `A matching account does not exist for the labor allocation. Center: ${jobline.profitcenter_labor}` + ); + } + if (!invoiceLineHash[account.name]) { + invoiceLineHash[account.name] = { + ItemRef: { FullName: account.accountitem }, + Desc: account.accountdesc, + Quantity: 1, // jobline.mod_lb_hrs, + Amount: DineroAmount, + //Amount: DineroAmount.toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }; + } else { + invoiceLineHash[account.name].Amount = + invoiceLineHash[account.name].Amount.add(DineroAmount); + } + } + }); + // console.log("Done creating hash", JSON.stringify(invoiceLineHash)); + + if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) { + // console.log("Adding MAPA Line Manually."); + const mapaAccountName = responsibilityCenters.defaults.profits.MAPA; + + const mapaAccount = responsibilityCenters.profits.find( + (c) => c.name === mapaAccountName + ); + + if (mapaAccount) { + InvoiceLineAdd.push({ + ItemRef: { FullName: mapaAccount.accountitem }, + Desc: mapaAccount.accountdesc, + Quantity: 1, + Amount: Dinero(job.job_totals.rates.mapa.total).toFormat( + DineroQbFormat + ), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } else { + //console.log("NO MAPA ACCOUNT FOUND!!"); + } + } + + if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) { + // console.log("Adding MASH Line Manually."); + + const mashAccountName = responsibilityCenters.defaults.profits.MASH; + + const mashAccount = responsibilityCenters.profits.find( + (c) => c.name === mashAccountName + ); + + if (mashAccount) { + InvoiceLineAdd.push({ + ItemRef: { FullName: mashAccount.accountitem }, + Desc: mashAccount.accountdesc, + Quantity: 1, + Amount: Dinero(job.job_totals.rates.mash.total).toFormat( + DineroQbFormat + ), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } else { + // console.log("NO MASH ACCOUNT FOUND!!"); + } + } + + //Convert the hash to an array. + Object.keys(invoiceLineHash).forEach((key) => { + InvoiceLineAdd.push({ + ...invoiceLineHash[key], + Amount: invoiceLineHash[key].Amount.toFormat(DineroQbFormat), + }); + }); + + //Add Towing, storage and adjustment lines. + + if (job.towing_payable && job.towing_payable !== 0) { + InvoiceLineAdd.push({ + ItemRef: { + FullName: responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["TOW"] + ).accountitem, + }, + Desc: "Towing", + Quantity: 1, + Amount: Dinero({ + amount: Math.round((job.towing_payable || 0) * 100), + }).toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } + if (job.storage_payable && job.storage_payable !== 0) { + InvoiceLineAdd.push({ + ItemRef: { + FullName: responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["TOW"] + ).accountitem, + }, + Desc: "Storage", + Quantity: 1, + Amount: Dinero({ + amount: Math.round((job.storage_payable || 0) * 100), + }).toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } + if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) { + InvoiceLineAdd.push({ + ItemRef: { + FullName: responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits["PAO"] + ).accountitem, + }, + Desc: "Adjustment", + Quantity: 1, + Amount: Dinero({ + amount: Math.round((job.adjustment_bottom_line || 0) * 100), + }).toFormat(DineroQbFormat), + SalesTaxCodeRef: { + FullName: "E", + }, + }); + } + + //Add tax lines + const job_totals = job.job_totals; + + const federal_tax = Dinero(job_totals.totals.federal_tax); + const state_tax = Dinero(job_totals.totals.state_tax); + const local_tax = Dinero(job_totals.totals.local_tax); + + if (federal_tax.getAmount() > 0) { + InvoiceLineAdd.push({ + ItemRef: { + FullName: bodyshop.md_responsibility_centers.taxes.federal.accountitem, + }, + Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc, + Amount: federal_tax.toFormat(DineroQbFormat), + }); + } + + if (state_tax.getAmount() > 0) { + InvoiceLineAdd.push({ + ItemRef: { + FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, + }, + Desc: bodyshop.md_responsibility_centers.taxes.state.accountdesc, + Amount: state_tax.toFormat(DineroQbFormat), + }); + } + + if (local_tax.getAmount() > 0) { + InvoiceLineAdd.push({ + ItemRef: { + FullName: bodyshop.md_responsibility_centers.taxes.local.accountitem, + }, + Desc: bodyshop.md_responsibility_centers.taxes.local.accountdesc, + Amount: local_tax.toFormat(DineroQbFormat), + }); + } + + //Region Specific + const { ca_bc_pvrt } = job; + if (ca_bc_pvrt) { + InvoiceLineAdd.push({ + ItemRef: { + FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, + }, + Desc: "PVRT", + Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( + DineroQbFormat + ), + }); + } + + //map each invoice line to the correct style for QBO. + + const invoiceObj = { + Line: [ + { + DetailType: "SalesItemLineDetail", + Amount: 100, + SalesItemLineDetail: { + // ItemRef: { + // // name: "Services", + // value: "16", + // }, + TaxCodeRef: { + value: "2", + }, + Qty: 1, + UnitPrice: 100, + }, + }, + ], + // Line: InvoiceLineAdd.map((i) => { + // return { + // DetailType: "SalesItemLineDetail", + // Amount: i.Amount, + // SalesItemLineDetail: { + // ItemRef: { + // //name: "Services", + // value: items[i.ItemRef.FullName], + // }, + // // TaxCodeRef: { + // // value: "2", + // // }, + // Qty: 1, + // }, + // }; + // }), + TxnTaxDetail: { + TaxLine: [ + { + DetailType: "TaxLineDetail", + + TaxLineDetail: { + NetAmountTaxable: 100, + TaxPercent: 7, + TaxRateRef: { + value: "16", + }, + PercentBased: true, + }, + }, + ], + }, + CustomerRef: { + value: parentTierRef.Id, + }, + }; + + try { + const result = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "invoice"), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(invoiceObj), + }); + setNewRefreshToken(req.user.email, result); + return result && result.Invoice; + } catch (error) { + logger.log("qbo-payables-error", "DEBUG", req.user.email, job.id, { + error, + method: "InsertOwner", + }); + throw error; + } +} diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js new file mode 100644 index 000000000..e69de29bb diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index d2e105393..87c7fe374 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -16,6 +16,7 @@ const { setNewRefreshToken, } = require("./qbo-callback"); const OAuthClient = require("intuit-oauth"); +const CreateInvoiceLines = require("../qb-receivables-lines"); const GraphQLClient = require("graphql-request").GraphQLClient; const { generateOwnerTier } = require("../qbxml/qbxml-utils"); @@ -36,7 +37,6 @@ exports.default = async (req, res) => { }); oauthClient.setToken(response.associations[0].qbo_auth); - const getToken = oauthClient.getToken(); await refreshOauthToken(oauthClient, req); @@ -48,6 +48,7 @@ exports.default = async (req, res) => { Authorization: BearerToken, }, }); + logger.log("qbo-payable-create", "DEBUG", req.user.email, jobIds); const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { @@ -111,6 +112,7 @@ exports.default = async (req, res) => { res.sendStatus(200); } catch (error) { console.log(error); + logger.log("qbo-payable-create-error", "ERROR", req.user.email, { error }); res.status(400).json(error); } }; @@ -347,325 +349,34 @@ async function QueryMetaData(oauthClient, req) { async function InsertInvoice(oauthClient, req, job, bodyshop, parentTierRef) { const { items, taxCodes } = await QueryMetaData(oauthClient, req); - const InvoiceLineAdd = []; - const responsibilityCenters = bodyshop.md_responsibility_centers; - - const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name. - - //Determine if there are MAPA and MASH lines already on the estimate. - //If there are, don't do anything extra (mitchell estimate) - //Otherwise, calculate them and add them to the default MAPA and MASH centers. - let hasMapaLine = false; - let hasMashLine = false; - - //Create the invoice lines mapping. - job.joblines.map((jobline) => { - //Parts Lines - if (jobline.db_ref === "936008") { - //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. - hasMapaLine = true; - } - if (jobline.db_ref === "936007") { - hasMashLine = true; - } - - if (jobline.profitcenter_part && jobline.act_price) { - let DineroAmount = Dinero({ - amount: Math.round(jobline.act_price * 100), - }).multiply(jobline.part_qty || 1); - - if (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) { - // console.log("Have a part discount", jobline); - DineroAmount = DineroAmount.add( - DineroAmount.percentage(jobline.prt_dsmk_p || 0) - ); - } - const account = responsibilityCenters.profits.find( - (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() - ); - - if (!account) { - logger.log( - "qbxml-receivables-no-account", - "warn", - null, - jobline.id, - null - ); - throw new Error( - `A matching account does not exist for the part allocation. Center: ${jobline.profitcenter_part}` - ); - } - if (!invoiceLineHash[account.name]) { - invoiceLineHash[account.name] = { - ItemRef: { FullName: account.accountitem }, - Desc: account.accountdesc, - Quantity: 1, //jobline.part_qty, - Amount: DineroAmount, //.toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }; - } else { - invoiceLineHash[account.name].Amount = - invoiceLineHash[account.name].Amount.add(DineroAmount); - } - } - // Labor Lines - if ( - jobline.profitcenter_labor && - jobline.mod_lb_hrs && - jobline.mod_lb_hrs > 0 - ) { - const DineroAmount = Dinero({ - amount: Math.round( - job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] * 100 - ), - }).multiply(jobline.mod_lb_hrs); - const account = responsibilityCenters.profits.find( - (i) => jobline.profitcenter_labor.toLowerCase() === i.name.toLowerCase() - ); - - if (!account) { - throw new Error( - `A matching account does not exist for the labor allocation. Center: ${jobline.profitcenter_labor}` - ); - } - if (!invoiceLineHash[account.name]) { - invoiceLineHash[account.name] = { - ItemRef: { FullName: account.accountitem }, - Desc: account.accountdesc, - Quantity: 1, // jobline.mod_lb_hrs, - Amount: DineroAmount, - //Amount: DineroAmount.toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }; - } else { - invoiceLineHash[account.name].Amount = - invoiceLineHash[account.name].Amount.add(DineroAmount); - } - } + const InvoiceLineAdd = CreateInvoiceLines({ + bodyshop, + jobs_by_pk: job, + qbo: true, + items, + taxCodes, }); - // console.log("Done creating hash", JSON.stringify(invoiceLineHash)); - - if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) { - // console.log("Adding MAPA Line Manually."); - const mapaAccountName = responsibilityCenters.defaults.profits.MAPA; - - const mapaAccount = responsibilityCenters.profits.find( - (c) => c.name === mapaAccountName - ); - - if (mapaAccount) { - InvoiceLineAdd.push({ - ItemRef: { FullName: mapaAccount.accountitem }, - Desc: mapaAccount.accountdesc, - Quantity: 1, - Amount: Dinero(job.job_totals.rates.mapa.total).toFormat( - DineroQbFormat - ), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } else { - //console.log("NO MAPA ACCOUNT FOUND!!"); - } - } - - if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) { - // console.log("Adding MASH Line Manually."); - - const mashAccountName = responsibilityCenters.defaults.profits.MASH; - - const mashAccount = responsibilityCenters.profits.find( - (c) => c.name === mashAccountName - ); - - if (mashAccount) { - InvoiceLineAdd.push({ - ItemRef: { FullName: mashAccount.accountitem }, - Desc: mashAccount.accountdesc, - Quantity: 1, - Amount: Dinero(job.job_totals.rates.mash.total).toFormat( - DineroQbFormat - ), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } else { - // console.log("NO MASH ACCOUNT FOUND!!"); - } - } - - //Convert the hash to an array. - Object.keys(invoiceLineHash).forEach((key) => { - InvoiceLineAdd.push({ - ...invoiceLineHash[key], - Amount: invoiceLineHash[key].Amount.toFormat(DineroQbFormat), - }); - }); - - //Add Towing, storage and adjustment lines. - - if (job.towing_payable && job.towing_payable !== 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["TOW"] - ).accountitem, - }, - Desc: "Towing", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((job.towing_payable || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - if (job.storage_payable && job.storage_payable !== 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["TOW"] - ).accountitem, - }, - Desc: "Storage", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((job.storage_payable || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["PAO"] - ).accountitem, - }, - Desc: "Adjustment", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((job.adjustment_bottom_line || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - - //Add tax lines - const job_totals = job.job_totals; - - const federal_tax = Dinero(job_totals.totals.federal_tax); - const state_tax = Dinero(job_totals.totals.state_tax); - const local_tax = Dinero(job_totals.totals.local_tax); - - if (federal_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.federal.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc, - Amount: federal_tax.toFormat(DineroQbFormat), - }); - } - - if (state_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.state.accountdesc, - Amount: state_tax.toFormat(DineroQbFormat), - }); - } - - if (local_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.local.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.local.accountdesc, - Amount: local_tax.toFormat(DineroQbFormat), - }); - } - - //Region Specific - const { ca_bc_pvrt } = job; - if (ca_bc_pvrt) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, - }, - Desc: "PVRT", - Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( - DineroQbFormat - ), - }); - } - - //map each invoice line to the correct style for QBO. const invoiceObj = { - Line: [ - { - DetailType: "SalesItemLineDetail", - Amount: 100, - SalesItemLineDetail: { - // ItemRef: { - // // name: "Services", - // value: "16", - // }, - TaxCodeRef: { - value: "2", - }, - Qty: 1, - UnitPrice: 100, - }, - }, - ], - // Line: InvoiceLineAdd.map((i) => { - // return { + // Line: [ + // { // DetailType: "SalesItemLineDetail", - // Amount: i.Amount, + // Amount: 100, // SalesItemLineDetail: { // ItemRef: { - // //name: "Services", - // value: items[i.ItemRef.FullName], + // // name: "Services", + // value: "1", + // }, + // TaxCodeRef: { + // value: "2", // }, - // // TaxCodeRef: { - // // value: "2", - // // }, // Qty: 1, + // UnitPrice: 100, // }, - // }; - // }), - TxnTaxDetail: { - TaxLine: [ - { - DetailType: "TaxLineDetail", + // }, + // ], + Line: InvoiceLineAdd, - TaxLineDetail: { - NetAmountTaxable: 100, - TaxPercent: 7, - TaxRateRef: { - value: "16", - }, - PercentBased: true, - }, - }, - ], - }, CustomerRef: { value: parentTierRef.Id, }, diff --git a/server/accounting/qbo/qbo.js b/server/accounting/qbo/qbo.js index ab785a9fc..190e5aa07 100644 --- a/server/accounting/qbo/qbo.js +++ b/server/accounting/qbo/qbo.js @@ -21,3 +21,4 @@ exports.callback = require("./qbo-callback").default; exports.authorize = require("./qbo-authorize").default; exports.refresh = require("./qbo-callback").refresh; exports.receivables = require("./qbo-receivables").default; +exports.payables = require("./qbo-payables").default; diff --git a/server/accounting/qbxml/qbxml-receivables.js b/server/accounting/qbxml/qbxml-receivables.js index 77f310cc2..1aa506f9a 100644 --- a/server/accounting/qbxml/qbxml-receivables.js +++ b/server/accounting/qbxml/qbxml-receivables.js @@ -7,6 +7,7 @@ const moment = require("moment"); var builder = require("xmlbuilder2"); const QbXmlUtils = require("./qbxml-utils"); const logger = require("../../utils/logger"); +const CreateInvoiceLines = require("../qb-receivables-lines"); require("dotenv").config({ path: path.resolve( @@ -227,273 +228,8 @@ const generateInvoiceQbxml = ( twoTierPref ) => { //Build the Invoice XML file. - const InvoiceLineAdd = []; - const responsibilityCenters = bodyshop.md_responsibility_centers; - const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name. - - //Determine if there are MAPA and MASH lines already on the estimate. - //If there are, don't do anything extra (mitchell estimate) - //Otherwise, calculate them and add them to the default MAPA and MASH centers. - let hasMapaLine = false; - let hasMashLine = false; - - //Create the invoice lines mapping. - jobs_by_pk.joblines.map((jobline) => { - //Parts Lines - if (jobline.db_ref === "936008") { - //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. - hasMapaLine = true; - } - if (jobline.db_ref === "936007") { - hasMashLine = true; - } - - if (jobline.profitcenter_part && jobline.act_price) { - let DineroAmount = Dinero({ - amount: Math.round(jobline.act_price * 100), - }).multiply(jobline.part_qty || 1); - - if (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) { - // console.log("Have a part discount", jobline); - DineroAmount = DineroAmount.add( - DineroAmount.percentage(Math.abs(jobline.prt_dsmk_p || 0)).multiply( - jobline.prt_dsmk_p > 0 ? 1 : -1 - ) - ); - } - const account = responsibilityCenters.profits.find( - (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() - ); - - if (!account) { - logger.log( - "qbxml-receivables-no-account", - "warn", - null, - jobline.id, - null - ); - throw new Error( - `A matching account does not exist for the part allocation. Center: ${jobline.profitcenter_part}` - ); - } - if (!invoiceLineHash[account.name]) { - invoiceLineHash[account.name] = { - ItemRef: { FullName: account.accountitem }, - Desc: account.accountdesc, - Quantity: 1, //jobline.part_qty, - Amount: DineroAmount, //.toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }; - } else { - invoiceLineHash[account.name].Amount = - invoiceLineHash[account.name].Amount.add(DineroAmount); - } - } - // Labor Lines - if (jobline.profitcenter_labor && jobline.mod_lb_hrs) { - const DineroAmount = Dinero({ - amount: Math.round( - jobs_by_pk[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] * 100 - ), - }).multiply(jobline.mod_lb_hrs); - const account = responsibilityCenters.profits.find( - (i) => jobline.profitcenter_labor.toLowerCase() === i.name.toLowerCase() - ); - - if (!account) { - throw new Error( - `A matching account does not exist for the labor allocation. Center: ${jobline.profitcenter_labor}` - ); - } - if (!invoiceLineHash[account.name]) { - invoiceLineHash[account.name] = { - ItemRef: { FullName: account.accountitem }, - Desc: account.accountdesc, - Quantity: 1, // jobline.mod_lb_hrs, - Amount: DineroAmount, - //Amount: DineroAmount.toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }; - } else { - invoiceLineHash[account.name].Amount = - invoiceLineHash[account.name].Amount.add(DineroAmount); - } - } - }); - // console.log("Done creating hash", JSON.stringify(invoiceLineHash)); - - if (!hasMapaLine && jobs_by_pk.job_totals.rates.mapa.total.amount > 0) { - // console.log("Adding MAPA Line Manually."); - const mapaAccountName = responsibilityCenters.defaults.profits.MAPA; - - const mapaAccount = responsibilityCenters.profits.find( - (c) => c.name === mapaAccountName - ); - - if (mapaAccount) { - InvoiceLineAdd.push({ - ItemRef: { FullName: mapaAccount.accountitem }, - Desc: mapaAccount.accountdesc, - Quantity: 1, - Amount: Dinero(jobs_by_pk.job_totals.rates.mapa.total).toFormat( - DineroQbFormat - ), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } else { - //console.log("NO MAPA ACCOUNT FOUND!!"); - } - } - - if (!hasMashLine && jobs_by_pk.job_totals.rates.mash.total.amount > 0) { - // console.log("Adding MASH Line Manually."); - - const mashAccountName = responsibilityCenters.defaults.profits.MASH; - - const mashAccount = responsibilityCenters.profits.find( - (c) => c.name === mashAccountName - ); - - if (mashAccount) { - InvoiceLineAdd.push({ - ItemRef: { FullName: mashAccount.accountitem }, - Desc: mashAccount.accountdesc, - Quantity: 1, - Amount: Dinero(jobs_by_pk.job_totals.rates.mash.total).toFormat( - DineroQbFormat - ), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } else { - // console.log("NO MASH ACCOUNT FOUND!!"); - } - } - - //Convert the hash to an array. - Object.keys(invoiceLineHash).forEach((key) => { - InvoiceLineAdd.push({ - ...invoiceLineHash[key], - Amount: invoiceLineHash[key].Amount.toFormat(DineroQbFormat), - }); - }); - - //Add Towing, storage and adjustment lines. - - if (jobs_by_pk.towing_payable && jobs_by_pk.towing_payable !== 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["TOW"] - ).accountitem, - }, - Desc: "Towing", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((jobs_by_pk.towing_payable || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - if (jobs_by_pk.storage_payable && jobs_by_pk.storage_payable !== 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["TOW"] - ).accountitem, - }, - Desc: "Storage", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((jobs_by_pk.storage_payable || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - if ( - jobs_by_pk.adjustment_bottom_line && - jobs_by_pk.adjustment_bottom_line !== 0 - ) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["PAO"] - ).accountitem, - }, - Desc: "Adjustment", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((jobs_by_pk.adjustment_bottom_line || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - - //Add tax lines - const job_totals = jobs_by_pk.job_totals; - - const federal_tax = Dinero(job_totals.totals.federal_tax); - const state_tax = Dinero(job_totals.totals.state_tax); - const local_tax = Dinero(job_totals.totals.local_tax); - - if (federal_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.federal.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc, - Amount: federal_tax.toFormat(DineroQbFormat), - }); - } - - if (state_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.state.accountdesc, - Amount: state_tax.toFormat(DineroQbFormat), - }); - } - - if (local_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.local.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.local.accountdesc, - Amount: local_tax.toFormat(DineroQbFormat), - }); - } - - //Region Specific - const { ca_bc_pvrt } = jobs_by_pk; - if (ca_bc_pvrt) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, - }, - Desc: "PVRT", - Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( - DineroQbFormat - ), - }); - } + const InvoiceLineAdd = CreateInvoiceLines({ bodyshop, jobs_by_pk }); const invoiceQbxmlObj = { QBXML: { diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index df1397caa..3a095f8f8 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -120,6 +120,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) { profitcenter_part db_ref prt_dsmk_p + tax_part } } bodyshops(where: {associations: {active: {_eq: true}}}) {