diff --git a/src/clj/auto_ap/routes/invoices.clj b/src/clj/auto_ap/routes/invoices.clj index 285c7afd..604a457f 100644 --- a/src/clj/auto_ap/routes/invoices.clj +++ b/src/clj/auto_ap/routes/invoices.clj @@ -3,6 +3,7 @@ [auto-ap.db.vendors :as vendors] [auto-ap.db.invoices :as invoices] [auto-ap.db.utils :refer [query]] + [auto-ap.yodlee.import :refer [manual-import]] [auto-ap.utils :refer [by]] [auto-ap.parse :as parse] [auto-ap.graphql.utils :refer [assert-admin]] @@ -48,12 +49,19 @@ (defn parse-amount [i] (try (Double/parseDouble (str/replace (or (second - (re-matches #"[^0-9\.,]*([0-9\.,]+)[^0-9\.,]*" (:amount i))) + (re-matches #"[^0-9\.,\\-]*([0-9\.,\\-]+)[^0-9\.,]*" (:amount i))) "0") #"," "")) (catch Exception e (throw (Exception. (str "Could not parse total from value '" (:amount i) "'") e))))) +(defn parse-account-id [i] + (try + (Integer/parseInt (second + (re-matches #"[^0-9,\\-]*([0-9,\\-]+)[^0-9,]*" (:account-id i)))) + (catch Exception e + (throw (Exception. (str "Could not parse account from value '" (:account-id i) "'") e))))) + (defn parse-date [{:keys [raw-date]}] (try (parse/parse-value :clj-time "MM/dd/yyyy" raw-date) @@ -70,67 +78,102 @@ (defroutes routes (wrap-routes - (context "/invoices" [] - #_(POST "/upload" - {{ files "file"} :params :as params} - (let [{:keys [filename tempfile]} files - companies (companies/get-all) - vendors (vendors/get-all)] - (invoices/import (parse/parse-file (.getPath tempfile) filename) companies vendors) - {:status 200 - :body (pr-str (invoices/get-pending ((:query-params params ) "company"))) - :headers {"Content-Type" "application/edn"}})) + (context "/" [] + (context "/transactions" [] + (POST "/batch-upload" + {{:keys [data company-id]} :edn-params user :identity} + (assert-admin user) + (let [columns [:status :raw-date :description-original :high-level-category nil nil :amount nil nil nil nil nil :account-id] + rows (->> (str/split data #"\n" ) + (drop 1) + (map #(str/split % #"\t")) + (map #(into {} (filter identity (map (fn [c k] [k c] ) % columns)))) + + + + + + + (map (parse-or-error :amount parse-amount)) + (map (parse-or-error :account-id parse-account-id)) + (map (parse-or-error :date parse-date))) + + raw-transactions (vec (->> rows + (filter #(not (seq (:errors %))) ) + (map (fn [{:keys [description-original status high-level-category amount account-id]}] + {:description-original description-original + :status status + :high-level-category high-level-category + :amount amount + :account-id account-id}))))] - (POST "/upload-integreat" - {{:keys [excel-rows]} :edn-params identity :identity} - (assert-admin identity) - (let [columns [:raw-date :vendor-name :check :location :invoice-number :amount :company :bill-entered :bill-rejected :added-on :exported-on] - - all-vendors (by :name (vendors/get-all)) + (manual-import raw-transactions company-id 1) - all-companies (companies/get-all) - all-companies (merge (by :code all-companies) (by :name all-companies)) + {:status 200 + :body (pr-str {:status "success"}) + :headers {"Content-Type" "application/edn"}})) + ) + (context "/invoices" [] + #_(POST "/upload" + {{ files "file"} :params :as params} + (let [{:keys [filename tempfile]} files + companies (companies/get-all) + vendors (vendors/get-all)] + (invoices/import (parse/parse-file (.getPath tempfile) filename) companies vendors) + {:status 200 + :body (pr-str (invoices/get-pending ((:query-params params ) "company"))) + :headers {"Content-Type" "application/edn"}})) - - rows (->> (str/split excel-rows #"\n" ) - (map #(str/split % #"\t")) - (map #(into {} (map (fn [c k] [k c] ) % columns))) - (map reset-id) - (map assoc-company-code) - - (map (parse-or-error :company-id #(parse-company % all-companies))) - (map (parse-or-error :vendor-id #(parse-vendor % all-vendors))) - (map (parse-or-error :invoice-number parse-invoice-number)) - (map (parse-or-error :total parse-amount)) - (map (parse-or-error :date parse-date))) - error-rows (filter :errors rows) - vendors-not-found (->> rows - (filter #(and (nil? (:vendor-id %)) - (not= "Cash" (:check %)))) - (map :vendor-name) - set) - insert-rows (vec (->> (filter #(not (seq (:errors %))) rows) - (map (fn [{:keys [vendor-id total company-id amount date invoice-number default-location]}] - {:vendor-id vendor-id - :company-id company-id - :default-location default-location - :total total - :outstanding-balance total - :imported true - :status "unpaid" - :invoice-number invoice-number - :date date})))) + (POST "/upload-integreat" + {{:keys [excel-rows]} :edn-params user :identity} + (assert-admin user) + (let [columns [:raw-date :vendor-name :check :location :invoice-number :amount :company :bill-entered :bill-rejected :added-on :exported-on] + + all-vendors (by :name (vendors/get-all)) - inserted-row-count (invoices/upsert-multi! insert-rows) - already-imported-count (- (count insert-rows) inserted-row-count)] - (expense-accounts/assign-defaults!) + all-companies (companies/get-all) + all-companies (merge (by :code all-companies) (by :name all-companies)) - + + rows (->> (str/split excel-rows #"\n" ) + (map #(str/split % #"\t")) + (map #(into {} (map (fn [c k] [k c] ) % columns))) + (map reset-id) + (map assoc-company-code) + + (map (parse-or-error :company-id #(parse-company % all-companies))) + (map (parse-or-error :vendor-id #(parse-vendor % all-vendors))) + (map (parse-or-error :invoice-number parse-invoice-number)) + (map (parse-or-error :total parse-amount)) + (map (parse-or-error :date parse-date))) + error-rows (filter :errors rows) + vendors-not-found (->> rows + (filter #(and (nil? (:vendor-id %)) + (not= "Cash" (:check %)))) + (map :vendor-name) + set) + insert-rows (vec (->> (filter #(not (seq (:errors %))) rows) + (map (fn [{:keys [vendor-id total company-id amount date invoice-number default-location]}] + {:vendor-id vendor-id + :company-id company-id + :default-location default-location + :total total + :outstanding-balance total + :imported true + :status "unpaid" + :invoice-number invoice-number + :date date})))) - {:status 200 - :body (pr-str {:imported inserted-row-count - :already-imported already-imported-count - :vendors-not-found vendors-not-found - :errors (map #(dissoc % :date) error-rows)}) - :headers {"Content-Type" "application/edn"}}))) + inserted-row-count (invoices/upsert-multi! insert-rows) + already-imported-count (- (count insert-rows) inserted-row-count)] + (expense-accounts/assign-defaults!) + + + + {:status 200 + :body (pr-str {:imported inserted-row-count + :already-imported already-imported-count + :vendors-not-found vendors-not-found + :errors (map #(dissoc % :date) error-rows)}) + :headers {"Content-Type" "application/edn"}})))) wrap-secure)) diff --git a/src/clj/auto_ap/yodlee/import.clj b/src/clj/auto_ap/yodlee/import.clj index 9c0cb085..15c5a550 100644 --- a/src/clj/auto_ap/yodlee/import.clj +++ b/src/clj/auto_ap/yodlee/import.clj @@ -22,9 +22,9 @@ (and company-id bank-account-id amount) (let [matching-checks (checks/get-graphql {:company-id company-id - :bank-account-id bank-account-id - :amount amount - :status "pending"})] + :bank-account-id bank-account-id + :amount (- amount) + :status "pending"})] (if (= 1 (count matching-checks)) (:id (first matching-checks)) nil)) @@ -48,14 +48,19 @@ description-simple :simple} :description {merchant-id :i merchant-name :name} :merchant + base-type :baseType type :type status :status} transaction + amount (if (= "DEBIT" base-type) + (- amount) + amount) check-number (extract-check-number transaction) company-id (yodlee-account-id->company account-id) bank-account-id (yodlee-account-id->bank-account-id account-id) check-id (transaction->check-id transaction check-number company-id bank-account-id amount) ]] + (println transaction) (try (transactions/upsert! @@ -88,8 +93,9 @@ (mapcat (fn [transaction-group] (map - (fn [index {:keys [date description-original high-level-category amount] :as transaction}] + (fn [index {:keys [date description-original high-level-category amount account-id] :as transaction}] {:id (str date "-" description-original "-" amount "-" index) + :date (time/unparse date "YYYY-MM-dd") :amount {:amount amount} :description {:original description-original diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs index 5b0d422c..795c2386 100644 --- a/src/cljs/auto_ap/views/main.cljs +++ b/src/cljs/auto_ap/views/main.cljs @@ -37,6 +37,7 @@ [:div {:class "navbar-end"} (if @user [:div {:class (str "navbar-item has-dropdown " (when (get-in @menu [:account :active?]) "is-active"))} + [:a {:class "navbar-link login" :on-click (fn [e] (re-frame/dispatch [::events/toggle-menu :account]))} (:name @user)] [:div {:class "navbar-dropdown"} [:a {:class "navbar-item"} "My profile"] @@ -206,6 +207,8 @@ + + ] [:div {:class "compose has-text-centered"} [:a {:class "button is-danger is-block is-bold" :href (bidi/path-for routes/routes :index) diff --git a/src/cljs/auto_ap/views/pages/transactions.cljs b/src/cljs/auto_ap/views/pages/transactions.cljs index d58646dc..97a90000 100644 --- a/src/cljs/auto_ap/views/pages/transactions.cljs +++ b/src/cljs/auto_ap/views/pages/transactions.cljs @@ -5,9 +5,10 @@ [reagent.core :as reagent] [goog.string :as gstring] [auto-ap.views.components.sorter :refer [sorted-column]] + [auto-ap.views.components.modal :refer [action-modal]] [auto-ap.views.components.paginator :refer [paginator]] [auto-ap.events :as events] - [auto-ap.views.utils :refer [dispatch-event date->str ]] + [auto-ap.views.utils :refer [dispatch-event date->str bind-field]] [auto-ap.utils :refer [by]] [auto-ap.views.components.invoice-table :refer [invoice-table] :as invoice-table] [auto-ap.subs :as subs])) @@ -135,19 +136,73 @@ [:a.tag {:href (:s3-url check) :target "_new"} [:i.fa.fa-money-check] [:span.icon [:i.fa.fa-money]] (str " " (:check-number check) " (" (gstring/format "$%.2f" amount ) ")")])] ]))]]])))) +(re-frame/reg-event-fx + ::manual-yodlee-import + (fn [{:keys [db]} _] + {:dispatch [::events/modal-status ::manual-yodlee-import {:visible? true}] + :db (assoc-in db [::manual-yodlee-import] {:company-id (:id @(re-frame/subscribe [::subs/company])) + :data ""})})) + +(re-frame/reg-sub + ::manual-yodlee-import + (fn [db] + (-> db ::manual-yodlee-import))) + +(re-frame/reg-event-fx + ::manual-yodlee-import-completed + (fn [{:keys [db]} _] + {:dispatch [::events/modal-completed ::manual-yodlee-import]})) + +(re-frame/reg-event-fx + ::manual-yodlee-import-started + (fn [{:keys [db]} _] + (let [manual-yodlee-import (::manual-yodlee-import db)] + {:http {:token (:user db) + :method :post + :body (pr-str manual-yodlee-import) + :headers {"Content-Type" "application/edn"} + :uri (str "/api/transactions/batch-upload") + :on-success [::manual-yodlee-import-completed] + :on-error [::manual-yodlee-import-error]}}))) + + +(defn manual-yodlee-import-modal [] + (let [data @(re-frame/subscribe [::manual-yodlee-import]) + change-event [::events/change-form [::manual-yodlee-import]]] + [action-modal {:id ::manual-yodlee-import + :title "Manual Yodlee Import" + :action-text "Import" + :save-event [::manual-yodlee-import-started]} + [:div.field + [:label.label + "Yodlee manual import table"] + [:div.control + [bind-field + [:textarea.textarea {:field [:data] + :event change-event + :subscription data}]]]]])) (def transactions-page (with-meta (fn [] - (let [current-company @(re-frame/subscribe [::subs/company])] + (let [current-company @(re-frame/subscribe [::subs/company]) + user @(re-frame/subscribe [::subs/user])] [:div [:h1.title "Transactions"] + (when (= "admin" (:role user)) + [:div.is-pulled-right + [:button.button.is-danger {:disabled (if current-company + "" + "disabled") + :on-click (dispatch-event [::manual-yodlee-import])} + "Manual Yodlee Import"]]) [transaction-table {:id :transactions :params (re-frame/subscribe [::params]) :transaction-page (re-frame/subscribe [::transaction-page]) :status (re-frame/subscribe [::subs/status]) :on-params-change (fn [params] - (re-frame/dispatch [::params-change params]))}]])) + (re-frame/dispatch [::params-change params]))}] + [manual-yodlee-import-modal]])) {:component-will-mount #(re-frame/dispatch-sync [::params-change {}]) }))