supports bulk voiding invoices, searches for iol duplicates.

This commit is contained in:
2022-02-17 17:38:58 -08:00
parent 38befab81d
commit ea490c5859
7 changed files with 368 additions and 209 deletions

View File

@@ -25,7 +25,10 @@
(update :invoice/status :db/ident) (update :invoice/status :db/ident)
(rename-keys {:invoice-payment/_invoice :invoice/payments}))) (rename-keys {:invoice-payment/_invoice :invoice/payments})))
(defn raw-graphql-ids [db args] (defn raw-graphql-ids
([args]
(raw-graphql-ids (d/db conn) args))
([db args]
(->> (cond-> {:query {:find [] (->> (cond-> {:query {:find []
:in ['$] :in ['$]
:where ['[?e :invoice/client]]} :where ['[?e :invoice/client]]}
@@ -147,7 +150,7 @@
'[?e :invoice/date ?sort-default]]}}) ) '[?e :invoice/date ?sort-default]]}}) )
(d/query) (d/query)
(apply-sort-3 args) (apply-sort-3 args)
(apply-pagination args))) (apply-pagination args))))
(defn graphql-results [ids db args] (defn graphql-results [ids db args]
@@ -258,7 +261,16 @@
set)] set)]
(into vendored-results vendorless-results))) (into vendored-results vendorless-results)))
(defn filter-ids [ids]
(if ids
(->> {:query {:find ['?e]
:in ['$ '[?e ...]]
:where ['[?e :invoice/date]]}
:args [(d/db conn) ids]}
(d/query)
(map first)
vec)
[]))
(defn code-invoice [invoice] (defn code-invoice [invoice]
(let [db (d/db auto-ap.datomic/conn) (let [db (d/db auto-ap.datomic/conn)

View File

@@ -1,17 +1,18 @@
(ns auto-ap.graphql.invoices (ns auto-ap.graphql.invoices
(:require (:require
[auto-ap.datomic :refer [audit-transact conn remove-nils uri]] [auto-ap.datomic
:refer [audit-transact audit-transact-batch conn remove-nils uri]]
[auto-ap.datomic.clients :as d-clients] [auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.invoices :as d-invoices] [auto-ap.datomic.invoices :as d-invoices]
[auto-ap.datomic.vendors :as d-vendors] [auto-ap.datomic.vendors :as d-vendors]
[auto-ap.graphql.checks :as gq-checks] [auto-ap.graphql.checks :as gq-checks]
[auto-ap.graphql.utils [auto-ap.graphql.utils
:as u
:refer [<-graphql :refer [<-graphql
assert-admin assert-admin
assert-can-see-client assert-can-see-client
assert-power-user assert-power-user
enum->keyword] enum->keyword]]
:as u]
[auto-ap.utils :refer [dollars=]] [auto-ap.utils :refer [dollars=]]
[clj-time.coerce :as coerce] [clj-time.coerce :as coerce]
[clj-time.core :as time] [clj-time.core :as time]
@@ -31,6 +32,7 @@
(let [args (assoc args :id (:id context)) (let [args (assoc args :id (:id context))
[invoices invoice-count outstanding total-amount] (-> args [invoices invoice-count outstanding total-amount] (-> args
:filters
(assoc :id (:id context)) (assoc :id (:id context))
(<-graphql) (<-graphql)
(update :status enum->keyword "invoice-status") (update :status enum->keyword "invoice-status")
@@ -220,6 +222,43 @@
(-> (d-invoices/get-by-id id) (->graphql (:id context))))) (-> (d-invoices/get-by-id id) (->graphql (:id context)))))
(defn void-invoices [context args _]
(let [_ (assert-admin (:id context))
args (assoc args :id (:id context))
ids (some-> args
:filters
(assoc :id (:id context))
(<-graphql)
(update :status enum->keyword "invoice-status")
(assoc :per-page Integer/MAX_VALUE)
d-invoices/raw-graphql-ids
:ids)
specific-ids (d-invoices/filter-ids (:ids args))
all-ids (into (set ids) specific-ids)]
(log/info "Voiding " (count all-ids) args)
(audit-transact
(->> all-ids
(d/q '[:find [(pull ?i [:db/id {:invoice/expense-accounts [:db/id]}]) ...]
:in $ [?i ...]
:where (not [_ :invoice-payment/invoice ?i])]
(d/db conn)
)
(mapcat
(fn [i]
(into
[{:db/id (:db/id i)
:invoice/total 0.0
:invoice/outstanding-balance 0.0
:invoice/status :invoice-status/voided
}]
(map
(fn [iea]
[:db/retract (:db/id i) :invoice/expense-accounts (:db/id iea)])
(:invoice/expense-accounts i))))))
(:id context))
{:message (str "Succesfully voided " (count all-ids))}))
(defn unvoid-invoice [context {id :invoice_id} _] (defn unvoid-invoice [context {id :invoice_id} _]
(let [invoice (d-invoices/get-by-id id) (let [invoice (d-invoices/get-by-id id)
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice))) _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
@@ -316,22 +355,7 @@
(def queries (def queries
{:invoice_page {:type '(list :invoice_page) {:invoice_page {:type '(list :invoice_page)
:args {:import_status {:type 'String} :args {:filters {:type :invoice_filters}}
:exact_match_id {:type :id}
:date_range {:type :date_range}
:due_range {:type :date_range}
:status {:type :invoice_status}
:unresolved {:type 'Boolean}
:scheduled_payments {:type 'Boolean}
:client_id {:type :id}
:vendor_id {:type :id}
:amount_lte {:type :money}
:amount_gte {:type :money}
:invoice_number_like {:type 'String}
:location {:type 'String}
:start {:type 'Int}
:per_page {:type 'Int}
:sort {:type '(list :sort_item)}}
:resolve :get-invoice-page} :resolve :get-invoice-page}
:all_invoices {:type '(list :invoice) :all_invoices {:type '(list :invoice)
@@ -351,6 +375,10 @@
:void_invoice {:type :invoice :void_invoice {:type :invoice
:args {:invoice_id {:type :id}} :args {:invoice_id {:type :id}}
:resolve :mutation/void-invoice} :resolve :mutation/void-invoice}
:void_invoices {:type :message
:args {:filters {:type :invoice_filters}
:ids {:type '(list :id)}}
:resolve :mutation/void-invoices}
:unvoid_invoice {:type :invoice :unvoid_invoice {:type :invoice
:args {:invoice_id {:type :id}} :args {:invoice_id {:type :id}}
:resolve :mutation/unvoid-invoice} :resolve :mutation/unvoid-invoice}
@@ -400,7 +428,24 @@
{:fields {:id {:type :id} {:fields {:id {:type :id}
:account_id {:type :id} :account_id {:type :id}
:location {:type 'String} :location {:type 'String}
:amount {:type :money}}}}) :amount {:type :money}}}
:invoice_filters {:fields {:import_status {:type 'String}
:exact_match_id {:type :id}
:date_range {:type :date_range}
:due_range {:type :date_range}
:status {:type :invoice_status}
:unresolved {:type 'Boolean}
:scheduled_payments {:type 'Boolean}
:client_id {:type :id}
:vendor_id {:type :id}
:amount_lte {:type :money}
:amount_gte {:type :money}
:invoice_number_like {:type 'String}
:location {:type 'String}
:start {:type 'Int}
:per_page {:type 'Int}
:sort {:type '(list :sort_item)}}}})
(def enums (def enums
{:invoice_status {:values [{:enum-value :paid} {:invoice_status {:values [{:enum-value :paid}
@@ -416,6 +461,7 @@
:mutation/add-and-print-invoice add-and-print-invoice :mutation/add-and-print-invoice add-and-print-invoice
:mutation/edit-invoice edit-invoice :mutation/edit-invoice edit-invoice
:mutation/void-invoice void-invoice :mutation/void-invoice void-invoice
:mutation/void-invoices void-invoices
:mutation/unvoid-invoice unvoid-invoice :mutation/unvoid-invoice unvoid-invoice
:mutation/unautopay-invoice unautopay-invoice :mutation/unautopay-invoice unautopay-invoice
:mutation/edit-expense-accounts edit-expense-accounts}) :mutation/edit-expense-accounts edit-expense-accounts})

View File

@@ -197,6 +197,7 @@
:journal-entry-line/credit :journal-entry-line/credit
{:journal-entry-line/account [:bank-account/include-in-reports {:journal-entry-line/account [:bank-account/include-in-reports
:bank-account/bank-name :bank-account/bank-name
:bank-account/numeric-code
:bank-account/code :bank-account/code
:bank-account/visible :bank-account/visible
:bank-account/name :bank-account/name
@@ -312,6 +313,7 @@
(log/info "Executing raw query " (get query-params "query" )) (log/info "Executing raw query " (get query-params "query" ))
(statsd/time! [(str "export.time") {:tags #{"export:raw"}}] (statsd/time! [(str "export.time") {:tags #{"export:raw"}}]
(into (list) (apply d/q (read-string (get query-params "query" )) (into [(d/db conn)] (read-string (get query-params "args" "[]"))))))))) (into (list) (apply d/q (read-string (get query-params "query" )) (into [(d/db conn)] (read-string (get query-params "args" "[]")))))))))
(defroutes export-routes (defroutes export-routes
(routes (routes
(wrap-routes api-key-authed-routes (wrap-routes api-key-authed-routes

View File

@@ -11,6 +11,47 @@
[unilog.context :as lc] [unilog.context :as lc]
[yang.scheduler :as scheduler])) [yang.scheduler :as scheduler]))
(def known-bad-yodlee-ids ;; these yodlee ids are ones we wish we could manually delete
(set [
3432269925
3432269925
3440074569
3440074568
3451368964
3453413513
3453413398
3325396245
3451368962
2987488002
3021618678
3021618674
3025625892
3052944039
3052944035
3079067192
3079067193
3079067193
3122287847
3122287842
3189405320
3189405320
3189405321
3189405321
3189405319
3189405319
3248171256
3248171256
3248171257
3248171257
3248171254
3291944265
3291944267
3291944267
3310943251
3387770449
3387770447
3432269929]))
(defn auth-header (defn auth-header
([cob-session] (str "{cobSession=" cob-session "}")) ([cob-session] (str "{cobSession=" cob-session "}"))
([cob-session user-session] (str "{cobSession=" cob-session ",userSession=" user-session "}"))) ([cob-session user-session] (str "{cobSession=" cob-session ",userSession=" user-session "}")))

View File

@@ -20,10 +20,8 @@
[auto-ap.views.components.expense-accounts-dialog :as expense-accounts-dialog] [auto-ap.views.components.expense-accounts-dialog :as expense-accounts-dialog]
[auto-ap.views.pages.data-page :as data-page])) [auto-ap.views.pages.data-page :as data-page]))
(defn query [params] (defn data-params->query-params [params]
{:venia/queries [[:invoice_page {:exact-match-id (some-> params :exact-match-id str)
{
:exact-match-id (some-> params :exact-match-id str)
:start (:start params 0) :start (:start params 0)
:sort (:sort params) :sort (:sort params)
:per-page (:per-page params) :per-page (:per-page params)
@@ -44,7 +42,12 @@
:import-invoices nil :import-invoices nil
:unpaid-invoices :unpaid :unpaid-invoices :unpaid
:paid-invoices :paid :paid-invoices :paid
:voided-invoices :voided)} :voided-invoices :voided)})
(defn query [params]
{:venia/queries [[:invoice_page
{:filters (data-params->query-params params)}
[[:invoices [:id :total :outstanding-balance :invoice-number :date :due :status :client-identifier :scheduled-payment :source-url :similarity [[:invoices [:id :total :outstanding-balance :invoice-number :date :due :status :client-identifier :scheduled-payment :source-url :similarity
[:vendor [:name :id]] [:vendor [:name :id]]
[:expense_accounts [:amount :id :location [:expense_accounts [:amount :id :location
@@ -229,7 +232,7 @@
(defn invoice-table [{:keys [id check-boxes overrides actions data-page checkable-fn]}] (defn invoice-table [{:keys [id check-boxes overrides actions data-page checkable-fn]}]
(let [selected-client @(re-frame/subscribe [::subs/client]) (let [selected-client @(re-frame/subscribe [::subs/client])
{:keys [data status table-params]} @(re-frame/subscribe [::data-page/page data-page]) {:keys [data status params table-params]} @(re-frame/subscribe [::data-page/page data-page])
selected-client @(re-frame/subscribe [::subs/client]) selected-client @(re-frame/subscribe [::subs/client])
is-loading? (= :loading (:state status)) is-loading? (= :loading (:state status))
@@ -261,7 +264,8 @@
^{:key (or (:id (first invoices)) "init")} ^{:key (or (:id (first invoices)) "init")}
[grid/table {:fullwidth true} [grid/table {:fullwidth true}
[grid/header {} [grid/header {}
[grid/row {} [grid/row {:id "header"
:entity params}
(when-not selected-client (when-not selected-client
[grid/sortable-header-cell {:sort-key "client" :sort-name "Client"} "Client"]) [grid/sortable-header-cell {:sort-key "client" :sort-name "Client"} "Client"])
[grid/sortable-header-cell {:sort-key "vendor" :sort-name "Vendor"} [grid/sortable-header-cell {:sort-key "vendor" :sort-name "Vendor"}

View File

@@ -95,7 +95,6 @@
(for [{:keys [client account vendor approval-status payment expected-deposit status bank-account description-original date amount id yodlee-merchant ] :as i} (:data data)] (for [{:keys [client account vendor approval-status payment expected-deposit status bank-account description-original date amount id yodlee-merchant ] :as i} (:data data)]
^{:key id} ^{:key id}
[grid/row {:class (:class i) :id id :entity i} [grid/row {:class (:class i) :id id :entity i}
(println expected-deposit)
(when-not selected-client (when-not selected-client
[grid/cell {} (:name client)]) [grid/cell {} (:name client)])

View File

@@ -1,38 +1,35 @@
(ns auto-ap.views.pages.unpaid-invoices (ns auto-ap.views.pages.unpaid-invoices
(:require [auto-ap.effects.forward :as forward] (:require
[auto-ap.effects.forward :as forward]
[auto-ap.events :as events] [auto-ap.events :as events]
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.views.components.dropdown :refer [drop-down]] [auto-ap.views.components.dropdown :refer [drop-down]]
[auto-ap.views.components.expense-accounts-dialog [auto-ap.views.components.expense-accounts-dialog
:as :as expense-accounts-dialog]
expense-accounts-dialog]
[auto-ap.views.components.invoice-table :as table] [auto-ap.views.components.invoice-table :as table]
[auto-ap.views.components.invoices.side-bar [auto-ap.views.components.invoices.side-bar
:as :as side-bar
side-bar :refer [invoices-side-bar]]
:refer
[invoices-side-bar]]
[auto-ap.views.components.layouts [auto-ap.views.components.layouts
:refer :refer [appearing-side-bar side-bar-layout]]
[appearing-side-bar side-bar-layout]]
[auto-ap.views.components.modal :as modal] [auto-ap.views.components.modal :as modal]
[auto-ap.views.pages.data-page :as data-page] [auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.pages.invoices.advanced-print-checks [auto-ap.views.pages.invoices.advanced-print-checks
:as :as advanced-print-checks]
advanced-print-checks]
[auto-ap.views.pages.invoices.common :refer [invoice-read]] [auto-ap.views.pages.invoices.common :refer [invoice-read]]
[auto-ap.views.pages.invoices.form :as form] [auto-ap.views.pages.invoices.form :as form]
[auto-ap.views.pages.invoices.handwritten-checks :as handwritten-checks] [auto-ap.views.pages.invoices.handwritten-checks :as handwritten-checks]
[auto-ap.views.utils :refer [dispatch-event dispatch-event-with-propagation with-user]] [auto-ap.views.utils
:refer [dispatch-event dispatch-event-with-propagation with-user]]
[clojure.set :as set] [clojure.set :as set]
[clojure.string :as str] [clojure.string :as str]
[goog.string :as gstring] [goog.string :as gstring]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[reagent.core :as r] [reagent.core :as r]
[vimsical.re-frame.fx.track :as track] [vimsical.re-frame.fx.track :as track]))
[auto-ap.views.components.buttons :as buttons]))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::params-change ::params-change
@@ -131,15 +128,68 @@
:status :unpaid :status :unpaid
#_#_:date (date->str (c/now) standard) #_#_:date (date->str (c/now) standard)
:location (first (:locations @(re-frame/subscribe [::subs/client])))}]})) :location (first (:locations @(re-frame/subscribe [::subs/client])))}]}))
(re-frame/reg-event-fx
::voided-selected
(fn [cofx [_]]
{:dispatch-n [[::modal/modal-closed]
[::params-change @(re-frame/subscribe [::data-page/params ::page])]]}))
(re-frame/reg-event-fx
::void-selected
(fn [cofx [_ which]]
(let [checked-params (get which "header")
specific-invoices (map :id (vals (dissoc which "header")))]
{:graphql {:token (-> cofx :db :user)
:owns-state {:single ::void-selected}
:query-obj
{:venia/operation {:operation/type :mutation
:operation/name "DeleteTransactions"}
:venia/queries [{:query/data
[:void-invoices
{:filters (some-> checked-params table/data-params->query-params)
:ids specific-invoices}
[:message]]}]}
:on-success (fn [result]
[::voided-selected])}
:dispatch [::data-page/reset-checked ::page]})))
(re-frame/reg-event-fx
::void-selected-requested
(fn [_ [_ which]]
(let [to-delete (if (get which "header")
"all visible invoices"
(str (count which) " invoices"))]
{:dispatch [::modal/modal-requested {:title "Confirmation"
:body [:div (str "Are you sure you want to void " to-delete "?")]
:cancel? true
:confirm {:value "Void"
:class "is-danger"
:status-from [::status/single ::void-selected]
:on-click (dispatch-event [::void-selected which] )}
:close-event [::status/completed ::void-selected]}]})))
(defn void-selected-button []
(let [status @(re-frame/subscribe [::status/single ::void-selected])
checked-invoices @(re-frame/subscribe [::data-page/checked :invoices])
is-admin? @(re-frame/subscribe [::subs/is-admin?])]
(when is-admin?
[:button.button.is-danger {:on-click (dispatch-event [::void-selected-requested checked-invoices])
:class (status/class-for status)
:disabled (or (status/disabled-for status)
(not (seq checked-invoices)))}
"Void"])))
(defn pay-button [] (defn pay-button []
(let [current-client @(re-frame/subscribe [::subs/client]) (let [current-client @(re-frame/subscribe [::subs/client])
checked-invoices (vals @(re-frame/subscribe [::data-page/checked :invoices])) checked-invoices @(re-frame/subscribe [::data-page/checked :invoices])
print-checks-status @(re-frame/subscribe [::status/single ::print-checks])] print-checks-status @(re-frame/subscribe [::status/single ::print-checks])]
[:div [:div
[:div.is-pulled-right [:div.is-pulled-right
[:div.buttons [:div.buttons
[void-selected-button]
[buttons/new-button {:event [::new-invoice-clicked] [buttons/new-button {:event [::new-invoice-clicked]
:name "Invoice" :name "Invoice"
:class "is-primary"}] :class "is-primary"}]
@@ -172,7 +222,7 @@
(if (> balance 0.001) (if (> balance 0.001)
(list (list
^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::print-checks id :check]) ^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::print-checks id :check])
:disabled (doto (status/disabled-for print-checks-status) println)} "Print checks from " name] :disabled (status/disabled-for print-checks-status)} "Print checks from " name]
^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::print-checks id :debit]) ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::print-checks id :debit])
:disabled (status/disabled-for print-checks-status)} "Debit from " name]) :disabled (status/disabled-for print-checks-status)} "Debit from " name])
(list (list
@@ -181,18 +231,23 @@
(when (> balance 0.001) (when (> balance 0.001)
^{:key "advanced-divider"} [:hr.dropdown-divider]) ^{:key "advanced-divider"} [:hr.dropdown-divider])
(when (and (= 1 (count (set (map (comp :id :vendor) checked-invoices)))) (when (and (= 1 (count (set (map (comp :id :vendor) (vals checked-invoices)))))
(> balance 0.001)) (> balance 0.001))
^{:key "handwritten"} [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::handwritten-checks/show checked-invoices]) ^{:key "handwritten"} [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::handwritten-checks/show (vals checked-invoices)])
:disabled (status/disabled-for print-checks-status)} "Handwritten Check..."]) :disabled (status/disabled-for print-checks-status)} "Handwritten Check..."])
(when (> balance 0.001) (when (> balance 0.001)
^{:key "advanced"} [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::advanced-print-checks/show checked-invoices]) ^{:key "advanced"} [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::advanced-print-checks/show (vals checked-invoices)])
:disabled (status/disabled-for print-checks-status)} "Advanced..."]))]]))]] :disabled (status/disabled-for print-checks-status)} "Advanced..."]))]]))]]
[:div.is-pulled-right {:style {:margin-right "0.5rem"}} [:div.is-pulled-right {:style {:margin-right "0.5rem"}}
(into [:div.tags ] (map (fn [{:keys [id invoice-number]}] (into [:div.tags ] (map (fn [[z {:keys [id invoice-number]}]]
(if (= z "header")
[:span.tag.is-medium "All visible invoices"
[:button.delete.is-small {:on-click
(dispatch-event [::data-page/remove-check :invoices z])}]]
[:span.tag.is-medium invoice-number [:span.tag.is-medium invoice-number
[:button.delete.is-small {:on-click [:button.delete.is-small {:on-click
(dispatch-event [::data-page/remove-check :invoices id])}]]) checked-invoices))]])) (dispatch-event [::data-page/remove-check :invoices id])}]]))
checked-invoices))]]))
(defn unpaid-invoices-content [{:keys [status]}] (defn unpaid-invoices-content [{:keys [status]}]