222 lines
14 KiB
Clojure
222 lines
14 KiB
Clojure
(ns auto-ap.routes.invoices
|
|
(:require [auto-ap.datomic.clients :as d-clients]
|
|
[auto-ap.datomic.vendors :as d-vendors]
|
|
[auto-ap.datomic.invoices :as d-invoices]
|
|
[auto-ap.yodlee.import :refer [manual-import]]
|
|
[auto-ap.utils :refer [by]]
|
|
[auto-ap.datomic :refer [remove-nils uri]]
|
|
[datomic.api :as d]
|
|
[auto-ap.parse :as parse]
|
|
[auto-ap.graphql.utils :refer [assert-admin]]
|
|
[auto-ap.routes.utils :refer [wrap-secure]]
|
|
[clj-time.coerce :refer [to-date]]
|
|
[ring.middleware.json :refer [wrap-json-response]]
|
|
[compojure.core :refer [GET POST context defroutes
|
|
wrap-routes]]
|
|
[clojure.string :as str]))
|
|
|
|
(defn reset-id [i]
|
|
(update i :invoice-number
|
|
(fn [n] (if (re-matches #"#+" n)
|
|
nil
|
|
n))))
|
|
|
|
(defn assoc-client-code [i]
|
|
(-> i
|
|
(assoc :client-code (first (str/split (:location i) #"-" )))
|
|
(assoc :default-location (second (str/split (:location i) #"-" )))))
|
|
|
|
(defn parse-client [{:keys [client-code client]} clients]
|
|
(if-let [id (:db/id (or (clients client-code)
|
|
(clients client)))]
|
|
id
|
|
(throw (Exception. (str "Company code '" client-code "' and company named '" client "' not found.")))))
|
|
|
|
(defn parse-invoice-number [{:keys [invoice-number]}]
|
|
(or invoice-number ""))
|
|
|
|
(defn parse-vendor [{:keys [vendor-name check]} vendors]
|
|
(let [v (vendors vendor-name)]
|
|
(cond v
|
|
v
|
|
|
|
(= "Cash" check)
|
|
nil
|
|
|
|
:else
|
|
(throw (Exception. (str "Vendor '" vendor-name "' not found."))))))
|
|
|
|
(defn parse-vendor-id [{:keys [vendor]}]
|
|
(:db/id vendor))
|
|
|
|
(defn parse-amount [i]
|
|
(try
|
|
(Double/parseDouble (str/replace (or (second
|
|
(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-default-expense-account [i]
|
|
(try
|
|
(when-let [default-expense-account (:default-expense-account i)]
|
|
(Integer/parseInt default-expense-account))
|
|
(catch Exception e
|
|
(throw (Exception. (str "Could not parse expense account from value '" (:default-expense-account i) "'") e)))))
|
|
|
|
(defn parse-account-id [i]
|
|
(try
|
|
(Long/parseLong (second
|
|
(re-matches #"[^0-9,\\-]*([0-9,\\-]+)[^0-9,]*" (:bank-account-id i))))
|
|
(catch Exception e
|
|
(throw (Exception. (str "Could not parse account from value '" (:bank-account-id i) "'") e)))))
|
|
|
|
(defn parse-client-id [i]
|
|
(try
|
|
(Long/parseLong (second
|
|
(re-matches #"[^0-9,\\-]*([0-9,\\-]+)[^0-9,]*" (:client-id i))))
|
|
(catch Exception e
|
|
(throw (Exception. (str "Could not parse client from value '" (:client-id i) "'") e)))))
|
|
|
|
(defn parse-date [{:keys [raw-date]}]
|
|
(try
|
|
(parse/parse-value :clj-time "MM/dd/yyyy" raw-date)
|
|
(catch Exception e
|
|
(throw (Exception. (str "Could not parse date from '" raw-date "'") e)))))
|
|
|
|
(defn parse-or-error [key f]
|
|
(fn [x]
|
|
(try
|
|
(assoc x key (f x))
|
|
(catch Exception e
|
|
(update x :errors conj {:info (.getMessage e)
|
|
:details (str e)})))))
|
|
|
|
(defroutes routes
|
|
(wrap-routes
|
|
(context "/" []
|
|
(context "/transactions" []
|
|
(POST "/batch-upload"
|
|
{{:keys [data]} :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 :bank-account-id :client-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 :bank-account-id parse-account-id))
|
|
(map (parse-or-error :client-id parse-client-id))
|
|
(map (parse-or-error :date parse-date)))
|
|
error-rows (filter :errors rows)
|
|
|
|
raw-transactions (vec (->> rows
|
|
(filter #(not (seq (:errors %))) )
|
|
(map (fn [{:keys [description-original client-id status high-level-category amount bank-account-id date]}]
|
|
{:description-original description-original
|
|
:date date
|
|
:status status
|
|
:high-level-category high-level-category
|
|
:amount amount
|
|
:client-id client-id
|
|
:bank-account-id bank-account-id}))))]
|
|
|
|
(manual-import raw-transactions)
|
|
|
|
{:status 200
|
|
:body (pr-str {:imported (count raw-transactions)
|
|
:errors (map #(dissoc % :date) error-rows)})
|
|
: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"}}))
|
|
|
|
(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 :default-expense-account]
|
|
all-vendors (by :vendor/name (d-vendors/get-graphql {}))
|
|
all-clients (d-clients/get-all)
|
|
all-clients (merge (by :client/code all-clients) (by :client/name all-clients))
|
|
rows (->> (str/split excel-rows #"\n" )
|
|
(map #(str/split % #"\t"))
|
|
(map #(into {} (map (fn [c k] [k c] ) % columns)))
|
|
(map reset-id)
|
|
(map assoc-client-code)
|
|
(map (parse-or-error :client-id #(parse-client % all-clients)))
|
|
(map (parse-or-error :vendor #(parse-vendor % all-vendors)))
|
|
(map (parse-or-error :vendor-id #(parse-vendor-id %)))
|
|
(map (parse-or-error :default-expense-account parse-default-expense-account))
|
|
(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)
|
|
get-dupe-keys (fn [i]
|
|
(select-keys i [:invoice/vendor :invoice/client :invoice/invoice-number]))
|
|
existing-rows (->> (d-invoices/raw-graphql {})
|
|
(filter (fn [i] (not= :invoice-status/voided (:invoice/status i))))
|
|
(map (fn [i] (-> i
|
|
(update :invoice/client :db/id)
|
|
(update :invoice/vendor :db/id))))
|
|
(map get-dupe-keys)
|
|
set)
|
|
total-rows (->> (filter #(not (seq (:errors %))) rows)
|
|
(map (fn [{:keys [vendor-id total client-id amount date invoice-number default-location default-expense-account check vendor]}]
|
|
#:invoice {:db/id (.toString (java.util.UUID/randomUUID))
|
|
:vendor vendor-id
|
|
:client client-id
|
|
:default-location default-location
|
|
:default-expense-account default-expense-account
|
|
:total total
|
|
:outstanding-balance (if (= "Cash" check)
|
|
0.0
|
|
total)
|
|
:status (if (= "Cash" check)
|
|
:invoice-status/paid
|
|
:invoice-status/unpaid)
|
|
:invoice-number invoice-number
|
|
:date (to-date date)
|
|
:expense-accounts [#:invoice-expense-account {:expense-account-id (or default-expense-account (:vendor/default-expense-account vendor))
|
|
:location default-location
|
|
:amount total}]})))
|
|
insert-rows (vec (->> total-rows
|
|
(filter #(not (existing-rows (get-dupe-keys %) )))
|
|
(mapcat (fn [i]
|
|
(if (= :invoice-status/paid (:invoice/status i))
|
|
[i #:invoice-payment {:invoice (:db/id i)
|
|
:amount (:invoice/total i)
|
|
:payment (remove-nils #:payment {:db/id (.toString (java.util.UUID/randomUUID))
|
|
:vendor (:invoice/vendor i)
|
|
:client (:invoice/client i)
|
|
:type :payment-type/cash
|
|
:amount (:invoice/total i)
|
|
:status :payment-status/cleared
|
|
:date (:invoice/date i)})}]
|
|
[i])))
|
|
(map remove-nils)))
|
|
|
|
inserted-rows @(d/transact (d/connect uri) insert-rows)
|
|
already-imported-count (- (count total-rows) (count insert-rows))]
|
|
{:status 200
|
|
:body (pr-str {:imported (count total-rows)
|
|
:already-imported already-imported-count
|
|
:vendors-not-found vendors-not-found
|
|
:errors (map #(dissoc % :date) error-rows)})
|
|
:headers {"Content-Type" "application/edn"}}))))
|
|
wrap-secure))
|