so many bug fixes.

This commit is contained in:
Bryce Covert
2020-07-03 17:50:37 -07:00
parent 3de59d3fdc
commit 86f51f93e4
22 changed files with 1447 additions and 521 deletions

View File

@@ -1,6 +1,7 @@
(ns auto-ap.datomic.ledger
(:require [datomic.api :as d]
[auto-ap.graphql.utils :refer [->graphql limited-clients]]
[auto-ap.utils :refer [dollars-0?]]
[auto-ap.datomic :refer [merge-query apply-sort-3 apply-pagination add-sorter-fields]]
[auto-ap.datomic :refer [uri]]
[clj-time.coerce :as c]
@@ -88,7 +89,9 @@
:args [(:location args)]})
true
(merge-query {:query {:find ['?base-date '?e] :where ['[?e :journal-entry/date ?base-date]]}}))]
(merge-query {:query {:find ['?base-date '?e] :where ['[?e :journal-entry/date ?base-date]
'[?e :journal-entry/amount ?ja2]
'[(not= 0.0 ?ja2)]]}}))]
(->> query
(d/query)
(apply-sort-3 args)

View File

@@ -1,37 +1,37 @@
(ns auto-ap.graphql
(:require
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
[com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia :refer [execute]]
[com.walmartlabs.lacinia.executor :as executor]
[com.walmartlabs.lacinia.resolve :as resolve]
[buddy.auth :refer [throw-unauthorized]]
[auto-ap.utils :refer [by]]
[auto-ap.graphql.utils :refer [assert-admin can-see-client? assert-can-see-client]]
[auto-ap.datomic :refer [uri merge-query]]
[datomic.api :as d]
[clj-time.coerce :as coerce]
[clj-time.core :as t]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.checks :as d-checks]
[auto-ap.datomic.users :as d-users]
[auto-ap.datomic.invoices :as d-invoices]
[auto-ap.datomic.vendors :as d-vendors]
[auto-ap.graphql.users :as gq-users]
[auto-ap.graphql.yodlee-merchants :as ym]
[auto-ap.graphql.ledger :as gq-ledger]
[auto-ap.graphql.accounts :as gq-accounts]
[auto-ap.graphql.clients :as gq-clients]
[auto-ap.graphql.vendors :as gq-vendors]
[auto-ap.graphql.checks :as gq-checks]
[auto-ap.graphql.invoices :as gq-invoices]
[auto-ap.graphql.transactions :as gq-transactions]
[auto-ap.graphql.transaction-rules :as gq-transaction-rules]
[auto-ap.time :as time]
[clojure.walk :as walk]
[clojure.string :as str])
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
[com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia :refer [execute]]
[com.walmartlabs.lacinia.executor :as executor]
[com.walmartlabs.lacinia.resolve :as resolve]
[buddy.auth :refer [throw-unauthorized]]
[auto-ap.utils :refer [by]]
[auto-ap.graphql.utils :refer [assert-admin can-see-client? assert-can-see-client]]
[auto-ap.datomic :refer [uri merge-query]]
[datomic.api :as d]
[clj-time.coerce :as coerce]
[clj-time.core :as t]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.checks :as d-checks]
[auto-ap.datomic.users :as d-users]
[auto-ap.datomic.invoices :as d-invoices]
[auto-ap.datomic.vendors :as d-vendors]
[auto-ap.graphql.users :as gq-users]
[auto-ap.graphql.yodlee-merchants :as ym]
[auto-ap.graphql.ledger :as gq-ledger]
[auto-ap.graphql.accounts :as gq-accounts]
[auto-ap.graphql.clients :as gq-clients]
[auto-ap.graphql.vendors :as gq-vendors]
[auto-ap.graphql.checks :as gq-checks]
[auto-ap.graphql.invoices :as gq-invoices]
[auto-ap.graphql.transactions :as gq-transactions]
[auto-ap.graphql.transaction-rules :as gq-transaction-rules]
[auto-ap.time :as time]
[clojure.walk :as walk]
[clojure.string :as str])
(:import
(clojure.lang IPersistentMap)))
(clojure.lang IPersistentMap)))
(def integreat-schema
@@ -922,6 +922,20 @@
0
(- total outstanding-balance))})))
(defn has-fulfilled? [id date recent-fulfillments]
(seq (transduce
(filter (fn [[potential-id potential-date]]
(let [date (coerce/to-date-time date)
potential-date (coerce/to-date-time potential-date)]
#_(println "HERE" id potential-id potential-date date)
(and (= id potential-id)
(<= (t/in-days (apply t/interval (sort [date potential-date]))) 10)))))
conj
[]
recent-fulfillments))
)
(defn get-cash-flow [context {:keys [client_id]} value]
(when client_id
(let [{:client/keys [weekly-credits weekly-debits forecasted-transactions ]} (d/pull (d/db (d/connect uri)) '[*] client_id )
@@ -959,14 +973,25 @@
[?p :payment/type :payment-type/debit]
[?p :payment/type :payment-type/check])]}
:args [(d/db (d/connect uri)) client_id (coerce/to-date (t/plus (time/local-now) (t/days 31)))]})))
forecasted-transactions (->> forecasted-transactions
(filter (fn [{:forecasted-transaction/keys [day-of-month]}]
(>= day-of-month (t/day (time/local-now)))))
(mapv (fn [{:forecasted-transaction/keys [amount identifier day-of-month]}]
{:identifier identifier
:amount amount
:date (coerce/to-date-time (t/local-date (t/year (time/local-now)) (t/month (time/local-now)) day-of-month ))}
)))]
recent-fulfillments (d/query {:query {:find '[?f ?d]
:in '[$ ?client ?min-date]
:where ['[?t :transaction/forecast-match ?f]
'[?t :transaction/date ?d]
'[?t :transaction/client ?client]
'[(>= ?d ?min-date)]]}
:args [(d/db (d/connect uri)) client_id (coerce/to-date (t/plus (time/local-now) (t/months -2)))]})
forecasted-transactions (for [{:forecasted-transaction/keys [amount identifier day-of-month]
:db/keys [id]} forecasted-transactions
month (range -1 2)
:let [next (t/plus (t/local-date (t/year (time/local-now))
(t/month (time/local-now))
day-of-month )
(t/months month))]
:when (not (has-fulfilled? id next recent-fulfillments))]
{:identifier identifier
:amount amount
:date (coerce/to-date-time next)})]
(println "RECENT" forecasted-transactions)
{:beginning_balance total-cash
:outstanding_payments outstanding-checks
:invoices_due_soon (mapv (fn [[due outstanding]]
@@ -979,13 +1004,12 @@
:date (coerce/to-date-time date)})
(take 5 (time/day-of-week-seq 1)))
(filter #(>= (:amount %) 0) forecasted-transactions))
:upcoming_debits (doto (into (mapv
(fn [date]
{:amount (- (or weekly-debits 0))
:date (coerce/to-date-time date)})
(take 5 (time/day-of-week-seq 1)))
(filter #(< (:amount %) 0) forecasted-transactions))
println)
:upcoming_debits (into (mapv
(fn [date]
{:amount (- (or weekly-debits 0))
:date (coerce/to-date-time date)})
(take 5 (time/day-of-week-seq 1)))
(filter #(< (:amount %) 0) forecasted-transactions))
})))
(def schema

View File

@@ -184,7 +184,6 @@
(:entries args))
all-vendors (into all-vendors new-hidden-vendors)
all-accounts (transduce (map (comp str :account/numeric-code)) conj #{} (a/get-accounts))
_ (println all-accounts)
transaction (doall (map
(assoc-error (fn [entry]
(let [entry (-> entry
@@ -202,6 +201,8 @@
(throw (Exception. (str "Client '" (:client_code entry )"' not found.")) ))
(when-not vendor
(throw (Exception. (str "Vendor '" (:vendor_name entry) "' not found."))))
(when-not (re-find #"\d{1,2}/\d{1,2}/\d{4}" (:date entry))
(throw (Exception. (str "Date must be MM/dd/yyyy"))))
(when-not (dollars= (doto (reduce + 0.0 (map :debit (:line_items entry))))
(reduce + 0.0 (map :credit (:line_items entry))))
(throw (Exception. (str "Debits '"
@@ -249,6 +250,7 @@
success (filter (comp not :error) transaction)
retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:journal-entry/external-id x)]])
success)]
(println (take 4 success))
(run! (fn [batch] (println "transacting retraction batch") @(d/transact (d/connect uri) batch)) (partition-all 100 retraction))
(run! (fn [batch] (println "transacting success batch") @(d/transact (d/connect uri) batch)) (partition-all 100 success))
{:successful (map (fn [x] {:external_id (:journal-entry/external-id x)}) success)

View File

@@ -81,33 +81,36 @@
[file filename]
(excel/parse-file file filename))
(defn best-match [clients invoice-client-name]
(let [fuzzy-match (->> clients
(mapcat (fn [{:keys [:db/id :client/matches :client/name] :as client :or {matches []}}]
(map (fn [m]
[client (m/jaccard (.toLowerCase invoice-client-name) (.toLowerCase m))])
(conj matches name))))
(filter #(< (second %) 0.25))
(sort-by second)
ffirst)
(defn best-match
([clients invoice-client-name]
(best-match clients invoice-client-name 0.25))
([clients invoice-client-name threshold]
(let [fuzzy-match (->> clients
(mapcat (fn [{:keys [:db/id :client/matches :client/name] :as client :or {matches []}}]
(map (fn [m]
[client (m/jaccard (.toLowerCase invoice-client-name) (.toLowerCase m))])
(conj matches name))))
(filter #(<= (second %) threshold))
(sort-by second)
ffirst)
word-set (set (filter (complement str/blank?) (str/split (.toLowerCase invoice-client-name) #"[\s:\-]" )))
client-word-match (->> clients
(map
(fn [{:keys [:db/id :client/matches :client/name] :as client :or {matches []}}]
(let [client-words (-> #{}
(into
(mapcat
(fn [match] (str/split (.toLowerCase match) #"\s" ))
matches))
(into
(str/split (.toLowerCase name) #"\s" )))]
[client (count (set/intersection client-words word-set))])))
(filter (fn [[_ c]] (> c 0)))
(sort-by (fn [[_ c]] c))
reverse
ffirst)]
(or fuzzy-match client-word-match)))
word-set (set (filter (complement str/blank?) (str/split (.toLowerCase invoice-client-name) #"[\s:\-]" )))
client-word-match (->> clients
(map
(fn [{:keys [:db/id :client/matches :client/name] :as client :or {matches []}}]
(let [client-words (-> #{}
(into
(mapcat
(fn [match] (str/split (.toLowerCase match) #"\s" ))
matches))
(into
(str/split (.toLowerCase name) #"\s" )))]
[client (count (set/intersection client-words word-set))])))
(filter (fn [[_ c]] (> c 0)))
(sort-by (fn [[_ c]] c))
reverse
ffirst)]
(or fuzzy-match client-word-match))))
(defn best-location-match [client text full-text]
(or (->> client

View File

@@ -42,7 +42,8 @@
:extract {:invoice-number #"Invoice #\s*\n\s*[\w\.]+\s+[\w\./]+(.*)\s*\n"
:customer-identifier #"Bill To[^\n]+\n[^\n]*\n([\w ]+)\s{2,}"
:date #"Invoice #\s*\n\s*[\w\.]+\s+([\w\./]+)"
:total #"Total\s+\$([0-9.,]+)"}
:total #"Total\s+\$([0-9.,]+)"
:account-number #"Account #\s+(\d+)"}
:parser {:date [:clj-time "MM/dd/yy"]
:total [:trim-commas nil]}}
@@ -211,7 +212,8 @@
:extract {:date #"INVOICE DATE\s+([0-9]+/[0-9]+/[0-9]+)"
:customer-identifier #"SHIP-TO-PARTY.*\n(.*?)(?=\s{2,})"
:invoice-number #"INV #\s+(\d+)"
:total #"PLEASE PAY THIS AMOUNT\s+([0-9]+\.[0-9]{2})"}
:total #"PLEASE PAY THIS AMOUNT\s+([0-9]+\.[0-9]{2})"
:account-number #"CUSTOMER NUMBER\s+(\d+)"}
:parser {:date [:clj-time "MM/dd/yyyy"]
:total [:trim-commas nil]}}

View File

@@ -48,7 +48,7 @@
(let [jwt (jwt/sign (doto {:user (:name profile)
:exp (time/plus (time/now) (time/days 30))
:user/clients (map (fn [c]
(dissoc c :client/bank-accounts :client/location-matches))
(dissoc c :client/bank-accounts :client/location-matches :client/forecasted-transactions :client/matches :client/weekly-debits :client/weekly-credits :client/signature-file :client/address))
(:user/clients user))
:user/role (name (:user/role user))
:user/name (:name profile)}

View File

@@ -93,7 +93,11 @@
(defn parse-date [{:keys [raw-date]}]
(when-not
(re-find #"\d{1,2}/\d{1,2}/\d{4}" raw-date)
(throw (Exception. (str "Date " raw-date " must match MM/dd/yyyy"))))
(try
(parse-u/parse-value :clj-time "MM/dd/yyyy" raw-date)
(catch Exception e
(throw (Exception. (str "Could not parse date from '" raw-date "'") e)))))
@@ -197,10 +201,12 @@
(defn import-uploaded-invoice [client forced-location forced-vendor imports]
(let [clients (d-clients/get-all)
transactions (reduce (fn [result {:keys [invoice-number customer-identifier total date vendor-code text full-text] :as info}]
transactions (reduce (fn [result {:keys [invoice-number customer-identifier account-number total date vendor-code text full-text] :as info}]
(println "searching for" vendor-code)
(let [ _ (println "matching" customer-identifier)
matching-client (or (and customer-identifier
matching-client (or (and account-number
(parse/best-match clients account-number 0.0))
(and customer-identifier
(parse/best-match clients customer-identifier))
(if client
(first (filter (fn [c]
@@ -274,8 +280,113 @@
(throw (ex-info "No invoices found."
{:imports (str imports)})))
@(d/transact (d/connect uri) (vec (set transactions)))))
(defn validate-account-rows [rows code->existing-account]
(when-let [bad-types (seq (->> rows
(filter (fn [[account _ _ type :as row]]
(and (not (code->existing-account (Integer/parseInt account)))
(not (#{"Asset" "Liability" "Revenue" "Expense" "Equity" "Dividend"} type)))
))))]
(throw (ex-info (str "You are adding accounts without a valid type" )
{:rows bad-types})))
(when-let [duplicate-rows (seq (->> rows
(filter (fn [[account]]
(not-empty account)))
(group-by (fn [[account]]
account))
vals
(filter #(> (count %) 1))
(filter (fn [duplicates]
(apply not= duplicates)))
#_(map (fn [[[_ account]]]
account))
))]
(throw (ex-info (str "You have duplicated rows with different values." )
{:rows duplicate-rows}))))
(defn import-account-overrides [customer filename]
(let [conn (d/connect uri)
[header & rows] (-> filename (io/reader) csv/read-csv)
[client-id] (first (d/query (-> {:query {:find ['?e]
:in ['$ '?z]
:where [['?e :client/code '?z]]}
:args [(d/db (d/connect uri)) customer]})))
_ (println client-id)
headers (map read-string header)
code->existing-account (by :account/numeric-code (map first (d/query {:query {:find ['(pull ?e [:account/numeric-code
{:account/applicability [:db/ident]}
:db/id])]
:in ['$]
:where ['[?e :account/name]]}
:args [(d/db conn)]})))
existing-account-overrides (d/query (-> {:query {:find ['?e]
:in ['$ '?client-id]
:where [['?e :account-client-override/client '?client-id]]}
:args [(d/db (d/connect uri)) client-id]}))
rows (transduce (comp
(map (fn [[_ account account-name override-name _ type]]
[account account-name override-name type]))
(filter (fn [[account]]
(not-empty account))))
conj
[]
rows)
_ (validate-account-rows rows code->existing-account)
rows (vec (set rows))
txes (transduce
(comp
(mapcat (fn parse-map [[account account-name override-name type]]
(let [code (some-> account
not-empty
Integer/parseInt)
existing (code->existing-account code)]
(cond (not code)
[]
(and existing (or (#{:account-applicability/optional :account-applicability/customized}
(:db/ident (:account/applicability existing)))
(and (not-empty override-name)
(not-empty account-name)
(not= override-name account-name)
)))
[{:db/id (:db/id existing)
:account/client-overrides [{:account-client-override/client client-id
:account-client-override/name (or (not-empty override-name)
(not-empty account-name))}]}]
(not existing)
[{:account/applicability :account-applicability/customized
:account/name account-name
:account/account-set "default"
:account/numeric-code code
:account/code (str code)
:account/type (if (str/blank? type)
:account-type/expense
(keyword "account-type" (str/lower-case type)))
:account/client-overrides [{:account-client-override/client client-id
:account-client-override/name (or (not-empty override-name)
(not-empty account-name))}]}]
:else
[])))))
conj
(mapv
(fn [[x]]
[:db/retractEntity x])
existing-account-overrides)
rows)]
@(d/transact conn txes)
txes))
(defn import-transactions-cleared-against [file]
(let [[header & rows] (-> file (io/reader) csv/read-csv)
@@ -418,5 +529,25 @@
:body (pr-str {:message (.getMessage e)
:error (.toString e)
:data (ex-data e)})
:headers {"Content-Type" "application/edn"}})))))
:headers {"Content-Type" "application/edn"}}))))
(wrap-json-response (POST "/account-overrides"
{{files :file
files-2 "file"
client :client
client-2 "client"} :params :as params
user :identity}
(let [files (or files files-2)
client (or client client-2)
{:keys [filename tempfile]} files]
(assert-admin user)
(try
{:status 200
:body (import-account-overrides client (.getPath tempfile))
:headers {"Content-Type" "application/json"}}
(catch Exception e
(println e)
{:status 500
:body {:message (.getMessage e)
:data (ex-data e)}
:headers {"Content-Type" "application/json"}}))))))
wrap-secure))

View File

@@ -36,10 +36,36 @@
{:status 200
:headers {"Content-Type" "application/edn"}
:body (pr-str (yodlee/get-accounts)) }))
(POST "/accounts/:id" {:keys [query-params identity] {:keys [id]} :route-params :as request}
(GET "/provider-accounts" {:keys [query-params identity] :as request}
(assert-admin identity)
(let [[session token] (yodlee/get-access-token)]
{:status 200
:headers {"Content-Type" "application/edn"}
:body (pr-str (yodlee/update-yodlee (Long/parseLong id))) })))
:body (pr-str (yodlee/get-provider-accounts-with-accounts)) }))
(POST "/reauthenticate/:id" {:keys [query-params identity] {:keys [id]} :route-params
data :edn-params
:as request}
(assert-admin identity)
(try
(let [[session token] (yodlee/get-access-token)]
{:status 200
:headers {"Content-Type" "application/edn"}
:body (pr-str (yodlee/reauthenticate (Long/parseLong id) data)) })
(catch Exception e
{:status 500
:headers {"Content-Type" "application/edn"}
:body (pr-str {:message (.getMessage e)
:error (.toString e)})})))
(POST "/provider-accounts/:id" {:keys [query-params identity] {:keys [id]} :route-params :as request}
(assert-admin identity)
(try
(let [[session token] (yodlee/get-access-token)]
{:status 200
:headers {"Content-Type" "application/edn"}
:body (pr-str (yodlee/update-yodlee (Long/parseLong id))) })
(catch Exception e
{:status 400
:headers {"Content-Type" "application/edn"}
:body (pr-str e)}))))
wrap-secure))

View File

@@ -1,5 +1,6 @@
(ns auto-ap.yodlee.core
(:require [clj-http.client :as client]
[auto-ap.utils :refer [by]]
[cemerick.url :as u]
[clojure.data.json :as json]
[config.core :refer [env]]))
@@ -65,10 +66,12 @@
user-session (login-user cob-session)]
(-> (str (:yodlee-base-url env) "/providerAccounts")
(-> (client/get {:headers (merge base-headers {"Authorization" (auth-header cob-session user-session)})
:query-params {"include" "credentials,questions,preferences"}
:as :json})
:body
:providerAccount)
)))
:providerAccount))))
(defn get-transactions []
(let [cob-session (login-cobrand)
@@ -124,6 +127,22 @@
:body
:providerAccount)))
(defn get-provider-account-detail [id]
(let [cob-session (login-cobrand)
user-session (login-user cob-session)
batch-size 100]
(-> (str (:yodlee-base-url env) "/providerAccounts/" id )
(client/get {:headers (doto
(merge base-headers {"Authorization" (auth-header cob-session user-session)})
println)
:query-params {"include" "credentials,questions,preferences"}
:as :json})
:body
:providerAccount
first)))
(defn update-provider-account [pa]
(let [cob-session (login-cobrand)
user-session (login-user cob-session)
@@ -137,9 +156,21 @@
:body "{\"dataSetName\": [\"BASIC_AGG_DATA\"]}"
:as :json}))))
(defn reauthenticate [pa data]
(let [cob-session (login-cobrand)
user-session (login-user cob-session)
batch-size 100]
(-> (str (:yodlee-base-url env) "/providerAccounts?providerAccountIds=" pa)
(client/put {:headers (merge base-headers {"Authorization" (auth-header cob-session user-session)})
:body (json/write-str data)
:as :json}))))
(defn update-yodlee [id]
(update-provider-account (:providerAccountId (first (filter #(= (:id %) id) (get-accounts)))))
(get-provider-account (:providerAccountId (first (filter #(= (:id %) id) (get-accounts))))))
(update-provider-account id)
(get-provider-account id))
(defn get-specific-transactions [account]
(let [cob-session (login-cobrand)
@@ -205,6 +236,24 @@
:as :json})
:body)))
(defn get-provider-accounts-with-details []
(let [provider-accounts (get-provider-accounts)]
(reduce
(fn [pas pa]
(conj pas (try (get-provider-account-detail (:id pa))
(catch Exception e
pa))))
[]
provider-accounts)))
(defn get-provider-accounts-with-accounts []
(let [provider-accounts (by :id (get-provider-accounts-with-details))
accounts (get-accounts)]
(->> accounts
(reduce
(fn [provider-accounts a]
(update-in provider-accounts [(:providerAccountId a) :accounts] conj a)) provider-accounts)
vals)))
#_(defn get-users []
(let [cob-session (login-cobrand)]

View File

@@ -90,12 +90,9 @@
:id (sha-256 (str id))
:account-id account-id
:date (coerce/to-date (time/parse date "YYYY-MM-dd"))
:yodlee-merchant (when (and merchant-id merchant-name (not (str/blank? merchant-id)))
{:yodlee-merchant/yodlee-id merchant-id
:yodlee-merchant/name merchant-name})
:amount (double amount)
:description-original description-original
:description-simple description-simple
:description-original (some-> description-original (str/replace #"\s+" " "))
:description-simple (some-> description-simple (str/replace #"\s+" " "))
:approval-status (if check
:transaction-approval-status/approved
:transaction-approval-status/unapproved)

View File

@@ -196,11 +196,17 @@
_ (println client-id)
headers (map read-string header)
code->existing-account (by :account/numeric-code (map first (d/query {:query {:find ['(pull ?e [:account/numeric-code
{:account/applicability [:db/ident]}
:db/id])]
:in ['$]
:where ['[?e :account/name]]}
:args [(d/db conn)]})))
existing-account-overrides (d/query (-> {:query {:find ['?e]
:in ['$ '?client-id]
:where [['?e :account-client-override/client '?client-id]]}
:args [(d/db (d/connect uri)) client-id]}))
_ (if-let [bad-rows (seq (->> rows
@@ -219,27 +225,46 @@
txes (transduce
(comp
(filter (fn [[account _ override-name]]
(and
(not (str/blank? override-name))
(not (str/blank? account)))))
(map (fn parse-map [[account account-name override-name _ type]]
(let [code (Integer/parseInt account)
existing-id (:db/id (code->existing-account code))]
(cond-> {:account/client-overrides [{:account-client-override/client client-id
:account-client-override/name override-name}]}
existing-id (assoc :db/id existing-id)
(not existing-id) (assoc :account/applicability :account-applicability/customized
:account/name account-name
:account/account-set "default"
:account/numeric-code code
:account/code (str code)
:account/type (if (str/blank? type)
:account-type/expense
(keyword "account-type" (str/lower-case type)))))))))
(mapcat (fn parse-map [[account account-name override-name _ type]]
(let [code (some-> account
not-empty
Integer/parseInt)
existing (code->existing-account code)]
(cond (not code)
[]
(and existing (or (#{:account-applicability/optional :account-applicability/customized}
(:db/ident (:account/applicability existing)))
(and (not-empty override-name)
(not-empty account-name)
(not= override-name account-name)
)))
[{:db/id (:db/id existing)
:account/client-overrides [{:account-client-override/client client-id
:account-client-override/name (or (not-empty override-name)
(not-empty account-name))}]}]
(not existing)
[{:account/applicability :account-applicability/customized
:account/name account-name
:account/account-set "default"
:account/numeric-code code
:account/code (str code)
:account/type (if (str/blank? type)
:account-type/expense
(keyword "account-type" (str/lower-case type)))
:account/client-overrides [{:account-client-override/client client-id
:account-client-override/name (or (not-empty override-name)
(not-empty account-name))}]}]
:else
[])))))
conj
[]
(mapv
(fn [[x]]
[:db/retractEntity x])
existing-account-overrides)
rows)]
txes

View File

@@ -7,7 +7,7 @@
(re-frame/reg-sub
::form
(fn [db [_ x]]
(-> db ::forms x)))
(get (-> db ::forms) x)))
(re-frame/reg-sub
@@ -47,7 +47,7 @@
:after (fn [context]
(let [db (i/get-coeffect context :db)
result (get-in (i/get-coeffect context :event) [1 data-key])]
(println (get-in db [::forms form :complete-listener]))
(cond-> context
true
(i/assoc-effect :db (update-in db
@@ -95,7 +95,8 @@
(re-frame/reg-event-db
::save-error
(fn [db [_ form result]]
(println form result)
(println result)
(-> db
(assoc-in [::forms form :status] :error)
(assoc-in [::forms form :error] (or (:message (first result))
@@ -114,7 +115,6 @@
(i/->interceptor
:id :settles
:befor (fn [context]
(println "here2")
context)
:after (fn [context]
(i/assoc-effect context :dispatch-debounce {:event event
@@ -143,18 +143,20 @@
(defn vertical-form [{:keys [can-submit id change-event submit-event ]}]
{:form (fn [{:keys [title] :as params} & children]
(let [{:keys [data active? error]} @(re-frame/subscribe [::form id])
can-submit @(re-frame/subscribe can-submit)]
(into ^{:key id} [:form { :on-submit (fn [e]
(when (.-stopPropagation e)
(.stopPropagation e)
(.preventDefault e))
(when can-submit
(re-frame/dispatch-sync (vec (conj submit-event params)))))}
[:h1.title.is-2 title]]
children)))
{:form ^{:key "form"}
(fn [{:keys [title] :as params} & children]
(let [{:keys [data active? error]} @(re-frame/subscribe [::form id])
can-submit @(re-frame/subscribe can-submit)]
[:form { :on-submit (fn [e]
(when (.-stopPropagation e)
(.stopPropagation e)
(.preventDefault e))
(when can-submit
(re-frame/dispatch-sync (vec (conj submit-event params)))))}
[:h1.title.is-2 title]
[:<>
children]]))
:raw-field (fn [control]
(let [{:keys [data]} @(re-frame/subscribe [::form id])]
[bind-field (-> control
@@ -164,18 +166,20 @@
[:div.field
(when label [:p.help label])
[:div.control control]])
:field (fn [label control]
(let [{:keys [data]} @(re-frame/subscribe [::form id])]
[:div.field
(when label [:p.help label])
[:div.control [bind-field (-> control
(assoc-in [1 :subscription] data)
(assoc-in [1 :event] change-event))]]]))
:field ^{:key "field"}
(fn [label control]
(let [{:keys [data]} @(re-frame/subscribe [::form id])]
[:div.field
(when label [:p.help label])
[:div.control [bind-field (-> control
(assoc-in [1 :subscription] data)
(assoc-in [1 :event] change-event))]]]))
:error-notification (fn []
(when-let [error (:error @(re-frame/subscribe [::form id]))]
^{:key error}
[:div.notification.is-warning.animated.fadeInUp error]))
:error-notification
(fn []
(when-let [error (:error @(re-frame/subscribe [::form id]))]
^{:key error}
[:div.notification.is-warning.animated.fadeInUp {} error]))
:submit-button (fn [child]
(let [error (:error @(re-frame/subscribe [::form id]))]
[:button.button.is-medium.is-primary.is-fullwidth {:disabled (if @(re-frame/subscribe can-submit)

View File

@@ -32,31 +32,4 @@
:event on-change-event
:step "0.01"
:subscription value}]]]]]]
#_[:div
[:div.field.has-addons
[:div.control
[bind-field
[date-picker {:class-name "input is-fullwidth"
:class "input"
:format-week-number (fn [] "")
:previous-month-button-label ""
:placeholder-text "Start"
:next-month-button-label ""
:next-month-label ""
:event on-change-event
:type "date"
:field [:start]
:subscription value}]]]
[:div.control
[bind-field
[date-picker {:class-name "input is-fullwidth"
:class "input"
:format-week-number (fn [] "")
:previous-month-button-label ""
:placeholder-text "End"
:next-month-button-label ""
:event on-change-event
:next-month-label ""
:type "date"
:field [:end]
:subscription value}]]]]])
)

View File

@@ -147,28 +147,33 @@
[horizontal-field
[:label.label "Default"]
[bind-field
(assoc-in template [1 :field ] default-key)]]
(template default-key nil)
#_(assoc-in template [1 :field ] default-key)]]
(when is-admin?
[horizontal-field
[:label.label "Overrides"]
(for [[i override] (map vector (range) (conj (override-key data) {:key (random-uuid)}))]
^{:key (or
(:id override)
(:key override))}
[:div.columns
[:div.column
[bind-field
[typeahead-entity {:matches clients
:match->text :name
:type "typeahead-entity"
:field [override-key i :client]
:event change-event
:subscription data}]]]
[:div.column
[bind-field
(assoc-in template [1 :field ] [override-key i :override])]]
[:div.column.is-1
[:a.button {:on-click (dispatch-event [::removed-override override-key i])} [:span.icon [:span.icon-remove]]]]])])]))
(doall
(for [[i override] (map vector (range) (conj (override-key data) {:key (random-uuid)}))]
^{:key (or
(:id override)
(:key override))}
[:div.columns
[:div.column
[bind-field
[typeahead-entity {:matches clients
:match->text :name
:type "typeahead-entity"
:field [override-key i :client]
:event change-event
:subscription data}]]]
[:div.column
[bind-field
(template
[override-key i :override]
(get-in data [override-key i :client])
)]]
[:div.column.is-1
[:a.button {:on-click (dispatch-event [::removed-override override-key i])} [:span.icon [:span.icon-remove]]]]]))])]))
(defn form-content [{:keys [data change-event]}]
(let [accounts @(re-frame/subscribe [::subs/accounts])
@@ -210,23 +215,28 @@
[default-with-overrides {:data data :change-event change-event
:default-key :terms
:override-key :terms-overrides}
[:input.input {:type "number"
:step "1"
:style {:width "4em"}
:size 3
:spec ::entity/terms
:event change-event
:subscription data}]]
(fn [field client]
[:input.input {:type "number"
:step "1"
:style {:width "4em"}
:field field
:size 3
:spec ::entity/terms
:event change-event
:subscription data}])]
[:h2.subtitle "Expense Accounts"]
[default-with-overrides {:data data :change-event change-event
:default-key :default-account
:override-key :account-overrides}
[typeahead-entity {:matches accounts
:match->text (fn [x ] (str (:numeric-code x) " - " (:name x)))
:type "typeahead-entity"
:event change-event
:subscription data}]]
(fn [field client]
[typeahead-entity {:matches @(re-frame/subscribe [::subs/accounts client])
:match->text (fn [x ] (str (:numeric-code x) " - " (:name x)))
:field field
:type "typeahead-entity"
:event change-event
:subscription data}])]
[:h2.subtitle "Address"]
[address-field {:field [:address]

View File

@@ -1,15 +1,17 @@
(ns auto-ap.views.pages.admin.yodlee
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [re-frame.core :as re-frame]
[auto-ap.forms :as forms]
[reagent.core :as reagent]
[clojure.string :as str]
[cljs-time.format :as f]
[auto-ap.subs :as subs]
[auto-ap.events.admin.clients :as events]
[auto-ap.entities.clients :as entity]
[auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
[auto-ap.views.components.address :refer [address-field]]
[auto-ap.views.utils :refer [login-url dispatch-event dispatch-value-change bind-field horizontal-field]]
[auto-ap.views.utils :refer [login-url dispatch-event dispatch-value-change bind-field horizontal-field str->date date->str with-user]]
[auto-ap.views.components.modal :refer [action-modal]]
[cljs.reader :as edn]
[auto-ap.routes :as routes]
@@ -22,6 +24,11 @@
(fn [db]
(-> db ::yodlee :authentication)))
(re-frame/reg-sub
::can-submit
(fn [db]
true))
(re-frame/reg-sub
::loading?
(fn [db]
@@ -37,6 +44,16 @@
(fn [db]
(-> db ::yodlee :accounts-loading?)))
(re-frame/reg-sub
::provider-accounts-loading?
(fn [db]
(-> db ::provider-accounts-loading?)))
(re-frame/reg-sub
::provider-accounts
(fn [db]
(-> db ::provider-accounts)))
(re-frame/reg-event-fx
::authenticate-with-yodlee
(fn [{:keys [db]} _]
@@ -51,40 +68,34 @@
(re-frame/reg-event-fx
::mounted
(fn [{:keys [db]} _]
{:db (assoc-in db [::yodlee] {:accounts-loading? true})
{:db (-> db
(assoc ::yodlee {:provider-accounts-loading? true})
#_(assoc ::provider-accounts [])
#_(assoc ::provider-accounts-loading? true))
:http {:token (:user db)
:method :get
:headers {"Content-Type" "application/edn"}
:uri (str "/api/yodlee/accounts")
:on-success [::got-accounts]
:uri (str "/api/yodlee/provider-accounts")
:on-success [::got-provider-accounts]
:on-error [::save-error]}}))
(re-frame/reg-event-fx
::kicked
(fn [{:keys [db]} [_ id state]]
{:db (update-in db [::yodlee :accounts]
(fn [as]
(map (fn [a]
(if (= (:id a) id)
(assoc a :status state)
a))
as)))}))
{:dispatch [::mounted]}))
(re-frame/reg-event-fx
::kicked
(fn [{:keys [db]} [_ id state]]
{:dispatch [::mounted]}))
(re-frame/reg-event-fx
::kick
(fn [{:keys [db]} [_ id]]
{:db (update-in db [::yodlee :accounts]
(fn [as]
(map (fn [a]
(if (= (:id a) id)
(assoc a :status :kicking)
a))
as)))
:http {:token (:user db)
{:http {:token (:user db)
:method :post
:headers {"Content-Type" "application/edn"}
:uri (str "/api/yodlee/accounts/" id)
:uri (str "/api/yodlee/provider-accounts/" id)
:on-success [::kicked id :kicked]
:on-error [::kicked id :errored]}}))
@@ -95,14 +106,22 @@
(assoc-in [::yodlee :accounts] accounts)
(assoc-in [::yodlee :accounts-loading?] false))}))
(re-frame/reg-event-fx
::got-provider-accounts
(fn [{:keys [db]} [_ accounts]]
{:db (-> db
(assoc-in [::provider-accounts] accounts)
(assoc-in [::provider-accounts-loading?] false))}))
(re-frame/reg-event-fx
::authenticated
(fn [{:keys [db]} [_ authentication]]
{:db (-> db
(assoc-in [::yodlee :authentication] authentication)
(assoc-in [::yodlee :loading?] false))}))
{:dispatch [::mounted]}))
(re-frame/reg-event-fx
::save-error
(fn [{:keys [db]} [_ authentication]]
{:dispatch [::mounted]}))
(defn yodlee-link-button []
[:div
@@ -130,7 +149,16 @@
[:button.button.is-primary {:class (if loading? "is-loading" "") :on-click (dispatch-event [::authenticate-with-yodlee])} "Authenticate with Yodlee"]))])
(defn yodlee-accounts-table []
(defn yodlee-date->str [d]
(try
(or (some-> d
(str->date (:date-time-no-ms f/formatters))
date->str)
"N/A")
(catch js/Error e
"N/A")))
(defn yodlee-accounts-table [accounts]
[:div
[:table.table
@@ -139,41 +167,132 @@
[:th "Account Name"]
[:th "Account Number"]
[:th "Yodlee Account Number"]
[:th "Yodlee Last updated"]
[:th "Yodlee Status"]
[:th]]]
[:th "Balance"]
[:th "Yodlee Status"]]]
[:tbody
(if @(re-frame/subscribe [::accounts-loading?])
[:tr [:td {:col-span "6"} "Loading..."]
]
(for [account @(re-frame/subscribe [::accounts])]
^{:key (:id account)} [:tr
[:td (:accountName account)]
[:td (:accountNumber account)]
[:td (:id account)]
[:td (str/join ", " (map :lastUpdated (:dataset account)))]
[:td (str/join ", " (map :additionalStatus (:dataset account)))]
[:td
(cond (= (:status account) :kicking)
[:button.button.is-success.is-loading {:disabled "disabled"} "Kick."]
(for [account accounts]
^{:key (:id account)} [:tr
[:td (:accountName account)]
[:td (:accountNumber account)]
[:td (:id account)]
[:td.has-text-right (:amount (:balance account))]
[:td (str/join ", " (map :additionalStatus (:dataset account)))]
])]]])
(= (:status account) :kicked)
[:button.button {:disabled "disabled"} "In progress..."]
(re-frame/reg-event-fx
::reauthenticate-mfa
[with-user ]
(fn [{:keys [user db]} [_ provider-account-id ]]
{:db (forms/loading db [::mfa-form provider-account-id])
:http {:token user
:method :post
:headers {"Content-Type" "application/edn"}
:uri (str "/api/yodlee/reauthenticate/" provider-account-id )
:body {"field"
(mapv (fn [[k v]]
{"id" k
"value" v})
(:data (get-in db [::forms/forms [::mfa-form provider-account-id]])))}
:on-success [::authenticated]
:on-error [::forms/save-error [::mfa-form provider-account-id] ]}}))
(= (:status account) :errored)
[:button.button.is-danger {:disabled "disabled"} "Error."]
(re-frame/reg-event-fx
::reauthenticate
[with-user ]
(fn [{:keys [user db]} [_ provider-account-id ]]
{:db (forms/loading db [::login-form provider-account-id])
:http {:token user
:method :post
:headers {"Content-Type" "application/edn"}
:uri (str "/api/yodlee/reauthenticate/" provider-account-id )
:body {"loginForm"
{"row"
[{"field"
(mapv (fn [[k v]]
{"id" k
"value" v})
(:data (get-in db [::forms/forms [::login-form provider-account-id]])))}]}}
:on-success [::authenticated]
:on-error [::forms/save-error [::login-form provider-account-id]]}}))
:else
[:button.button.is-success {:on-click (dispatch-event [::kick (:id account)] )} "Kick." ])]]))]])
(defn yodlee-provider-accounts-table []
(if @(re-frame/subscribe [::provider-accounts-loading?])
[:div "Loading..."]
[:div.columns
[:div.column.is-three-quarters
(doall
(for [account @(re-frame/subscribe [::provider-accounts])]
^{:key (:id account)}
[:div.card {:style {:margin-bottom "1em"}}
[:div.card-header
[:div.card-header-title "Provider account " (:id account)
]]
[:div.card-content
[:div.notification.is-info.is-light
[:div.level
[:div.level-left
[:div.level-item
[:p
"This account was last updated on "
(yodlee-date->str (-> account :dataset first :lastUpdated))
", and last attempted "
(yodlee-date->str (-> account :dataset first :lastUpdateAttempt))
"."]]]
[:div.level-right [:button.button.is-success {:on-click (dispatch-event [::kick (:id account)] )} "Force refresh" ]]]
]
[:div.notification.is-info.is-warning
[:div.level
[:div.level-left
[:div.level-item
"This provider account's status is '"
(-> account :dataset first :additionalStatus)
"'. If this is in error, it might help to try reauthenticating by filling out the form below."]]]]
[yodlee-accounts-table (:accounts account)]
[:div
(if (:field account)
(for [f (:field account)]
(let [{error :error account-data :data } @(re-frame/subscribe [::forms/form [::mfa-form (:id account)]])
change-event [::forms/change [::mfa-form (:id account)]]
{:keys [form field field-holder raw-field error-notification submit-button]} (forms/vertical-form {:can-submit [::can-submit]
:change-event change-event
:submit-event [::reauthenticate-mfa (:id account)]
:id [::mfa-form (:id account)]} )]
(form {:title "Reauthenticate (login)"}
(error-notification)
(for [f (-> account :field)]
^{:key (:id f)}
(field (:label f)
[:input.input {:type "text" :field [(:id f)] :value (-> f :field first :value)}]))
(submit-button "Reauthenticate"))))
(let [{error :error account-data :data } @(re-frame/subscribe [::forms/form [::login-form (:id account)]])
change-event [::forms/change [::login-form (:id account)]]
{:keys [form field field-holder raw-field error-notification submit-button]} (forms/vertical-form {:can-submit [::can-submit]
:change-event change-event
:submit-event [::reauthenticate (:id account)]
:id [::login-form (:id account)]} )]
(form {:title "Reauthenticate (MFA)"}
(error-notification)
(for [f (-> account :loginForm first :row)]
^{:key (:id f)}
(field (:label f)
[:input.input {:type "text" :field [(:id f)] :value (-> f :field first :value)}]))
(submit-button "Reauthenticate"))))]]]))]]))
(defn admin-yodlee-content []
[(with-meta
(fn []
[:div
[:h1.title "Yodlee"]
[:h1.title "Yodlee provider accounts"]
[yodlee-accounts-table]
[yodlee-provider-accounts-table]
[yodlee-link-button]])
{:component-did-mount (fn []
(re-frame/dispatch [::mounted]))})])

View File

@@ -11,7 +11,7 @@
(def ranges
{:sales [40000 48999]
:cogs [50000 59999]
:payroll [60000 62999]
:payroll [60000 69999]
:controllable [70000 79999]
:fixed-overhead [80000 89999]
:ownership-controllable [90000 99999]})
@@ -98,6 +98,16 @@
(fn [[accounts] _]
(reduce + 0 (map :amount (vals accounts)))))
(re-frame/reg-sub
::percent-of-sales
(fn [[_ type only-location]]
[(re-frame/subscribe [::amount :sales only-location])
(re-frame/subscribe [::amount type only-location])])
(fn [[sales accounts] _]
(if (> (or sales 0) 0 )
(/ accounts sales)
0.0)))
(re-frame/reg-sub
::comparable-percent-of-sales
(fn [[_ type only-location]]
@@ -293,78 +303,76 @@
(defn grouping [{:keys [header accounts comparable-accounts groupings location sales comparable-sales]}]
(for [[grouping-name from to] groupings
:let [matching-accounts (filter
#(<= from (:numeric-code %) to)
accounts)
total (reduce + 0 (map :amount matching-accounts))
comparable-total (reduce + 0 (map #(:amount (get comparable-accounts (:id %)) 0) matching-accounts))]
:when (seq matching-accounts)
]
(list
^{:key "title"}
[:tr [:th "---" grouping-name "---"]
[:td]
[:td]
[:td]
[:td]
[:td]
]
^{:key "detail"}
(for [account matching-accounts]
^{:key (:name account)}
[:tr [:td (:name account)]
[:td.has-text-right [:a {:on-click (dispatch-event [::investigate-clicked location (:numeric-code account) (:numeric-code account) :current])}
(->$ (:amount account))]]
[:td.has-text-right (->% (if (> sales 0)
(/ (:amount account) sales)
0.0))]
[:td.has-text-right [:a {:on-click (dispatch-event [::investigate-clicked location (:numeric-code account) (:numeric-code account) :comparable])}
(->$ (:amount (get comparable-accounts (:id account)) 0))]]
[:td.has-text-right (->% (if (> comparable-sales 0)
(/ (:amount (get comparable-accounts (:id account)) 0) sales)
0.0))]
[:td.has-text-right (->$ (- (:amount account ) (:amount (get comparable-accounts (:id account)) 0)))]])
[:<>
(for [[grouping-name from to] groupings
:let [matching-accounts (filter
#(<= from (:numeric-code %) to)
accounts)
total (reduce + 0 (map :amount matching-accounts))
comparable-total (reduce + 0 (map #(:amount (get comparable-accounts (:id %)) 0) matching-accounts))]
:when (seq matching-accounts)
]
^{:key grouping-name}
[:<>
[:tr [:td "---" grouping-name "---"]
[:td]
[:td]
[:td]
[:td]
[:td]
]
[:<>
(for [account matching-accounts]
^{:key (:name account)}
[:tr [:td (:name account)]
[:td.has-text-right [:a {:on-click (dispatch-event [::investigate-clicked location (:numeric-code account) (:numeric-code account) :current])}
(->$ (:amount account))]]
[:td.has-text-right (->% (if (> sales 0)
(/ (:amount account) sales)
0.0))]
[:td.has-text-right [:a {:on-click (dispatch-event [::investigate-clicked location (:numeric-code account) (:numeric-code account) :comparable])}
(->$ (:amount (get comparable-accounts (:id account)) 0))]]
[:td.has-text-right (->% (if (> comparable-sales 0)
(/ (:amount (get comparable-accounts (:id account)) 0) sales)
0.0))]
[:td.has-text-right (->$ (- (:amount account ) (:amount (get comparable-accounts (:id account)) 0)))]])]
^{:key "total"}
[:tr [:th "---" grouping-name "---"]
[:th.has-text-right.total [:a
{:on-click (dispatch-event [::investigate-clicked location from to :current])}
(->$ total)] ]
[:th.has-text-right.total (->% (if (> sales 0)
(/ total sales)
0.0))]
[:th.has-text-right.total [:a
{:on-click (dispatch-event [::investigate-clicked location from to :comparable])}
(->$ comparable-total)]]
[:th.has-text-right.total (->% (if (> comparable-sales 0)
(/ comparable-total sales)
0.0))]
[:th.has-text-right.total (->$ (- total comparable-total))]
[:td]
])))
[:tr [:th ]
[:th.has-text-right.total [:a
{:on-click (dispatch-event [::investigate-clicked location from to :current])}
(->$ total)] ]
[:th.has-text-right.total (->% (if (> sales 0)
(/ total sales)
0.0))]
[:th.has-text-right.total [:a
{:on-click (dispatch-event [::investigate-clicked location from to :comparable])}
(->$ comparable-total)]]
[:th.has-text-right.total (->% (if (> comparable-sales 0)
(/ comparable-total sales)
0.0))]
[:th.has-text-right.total (->$ (- total comparable-total))]
[:td]
]
[:tr [:td]]])])
(defn overall-grouping [type title location]
(let [accounts @(re-frame/subscribe [::accounts type location])
min-numeric-code (or (first (map :numeric-code accounts)) 0)
max-numeric-code (or (last (map :numeric-code accounts)) 0)]
(list
^{:key "title"}
[:tr [:th.has-text-centered title]
[:<>
[:tr [:th.is-size-5 title]
[:td]
[:td]
[:td]]
^{:key "grouping"}
(grouping {:accounts accounts
[grouping {:accounts accounts
:location location
:groupings (type groupings)
:comparable-accounts @(re-frame/subscribe [::comparable-accounts-by-id type location])
:sales @(re-frame/subscribe [::amount :sales location])
:comparable-sales @(re-frame/subscribe [::comparable-amount :sales location])})
:comparable-sales @(re-frame/subscribe [::comparable-amount :sales location])}]
^{:key "total"}
[:tr [:th.has-text-centered title]
[:tr [:th.is-size-5 title]
[:th.has-text-right [:a
{:on-click (dispatch-event [::investigate-clicked location min-numeric-code max-numeric-code :current])}
(->$ @(re-frame/subscribe [::amount type location]))]]
@@ -374,7 +382,7 @@
(->$ @(re-frame/subscribe [::comparable-amount type location]))]]
[:th.has-text-right (->% @(re-frame/subscribe [::comparable-percent-of-sales type location]))]
[:th.has-text-right (->$ (- @(re-frame/subscribe [::amount type location])
@(re-frame/subscribe [::comparable-amount type location])))]])))
@(re-frame/subscribe [::comparable-amount type location])))]]]))
(defn subtotal [types negs title location]
(let [accounts (transduce (comp
@@ -400,7 +408,7 @@
max-numeric-code (or (last (map :numeric-code accounts)) 0)
sales @(re-frame/subscribe [::amount :sales location])
comparable-sales @(re-frame/subscribe [::comparable-amount :sales location])]
[:tr [:th.has-text-centered title]
[:tr [:th.is-size-5 title]
[:td.has-text-right [:a
{:on-click (dispatch-event [::investigate-clicked location min-numeric-code max-numeric-code :current])}
(->$ (reduce + 0 (map :amount accounts)))]]
@@ -418,6 +426,42 @@
[:td.has-text-right (->$ (- (reduce + 0 (map :amount accounts))
(reduce + 0 (map :amount comparable))))]]))
(defn location-rows [location]
[:<>
[overall-grouping :sales (str location " Sales") location]
[overall-grouping :cogs (str location " COGS") location]
[overall-grouping :payroll (str location " Payroll") location]
[subtotal [:payroll :cogs] #{} (str location " Prime Costs") location]
[subtotal [:sales :payroll :cogs] #{:payroll :cogs} (str location " Gross Profits") location]
[overall-grouping :controllable (str location " Controllable Expenses") location]
[overall-grouping :fixed-overhead (str location " Fixed Overhead") location]
[overall-grouping :ownership-controllable (str location " Ownership Controllable") location]
[subtotal [:controllable :fixed-overhead :ownership-controllable] #{} (str location " Overhead") location]
[subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} (str location " Net Income") location]
[subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" nil]])
(defn location-summary [location params]
[:div
[:h2.title.is-4 {:style {:margin-bottom "1rem"}} location " Summary"]
[:table.table.compact.balance-sheet {:style {:margin-bottom "2.5rem"}}
[:tbody
[:tr
[:td.has-text-right "Period ending"]
[:td.has-text-right (date->str (str->date (:to-date params) standard))]
[:td]
[:td.has-text-right (when (:to-date params)
(date->str (t/minus (str->date (:to-date params) standard) (t/years 1))))]
[:td]
[:td]]
[subtotal [:sales ] #{} "Sales" location]
[subtotal [:cogs ] #{} "Cogs" location]
[subtotal [:payroll ]#{} "Payroll" location]
[subtotal [:sales :payroll :cogs] #{:payroll :cogs} "Gross Profits" location]
[subtotal [:controllable :fixed-overhead :ownership-controllable] #{} "Overhead" location]
[subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" location]]]
])
(def profit-and-loss-content
(with-meta
(fn []
@@ -550,41 +594,27 @@
:else
[:div
[:<>
(for [location @(re-frame/subscribe [::locations])]
^{:key (str location "-summary")}
[location-summary location params]
)]
[:h2.title.is-4 {:style {:margin-bottom "1rem"}} "Detail"]
[:table.table.compact.balance-sheet
(list
^{:key "title"}
[:tbody
[:tr
[:td.has-text-right "Period ending"]
[:td.has-text-right "Period Ending"]
[:td.has-text-right (date->str (str->date (:to-date params) standard))]
[:td]
[:td.has-text-right (when (:to-date params)
(date->str (t/minus (str->date (:to-date params) standard) (t/years 1))))]
[:td]]
^{:key "report"}
(for [location @(re-frame/subscribe [::locations])]
^{:key location}
(list
^{:key "sales"}
(overall-grouping :sales (str location " Sales") location)
^{:key "cogs"}
(overall-grouping :cogs (str location " COGS") location)
^{:key "payroll"}
(overall-grouping :payroll (str location " Payroll") location)
^{:key "prime"}
(subtotal [:payroll :cogs] #{} (str location " Prime Costs") location)
^{:key "gross profit"}
(subtotal [:sales :payroll :cogs] #{:payroll :cogs} (str location " Gross Profits") location)
^{:key "controllable"}
(overall-grouping :controllable (str location " Controllable Expenses") location)
^{:key "fixed overhead"}
(overall-grouping :fixed-overhead (str location " Fixed Overhead") location)
^{:key "ownership"}
(overall-grouping :ownership-controllable (str location " Ownership Controllable") location)
^{:key "sub-overhead"}
(subtotal [:controllable :fixed-overhead :ownership-controllable] #{} (str location " Overhead") location)
^{:key "sub-loc-income"}
(subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} (str location " Net Income") location)
^{:key "sub-net-income"}
(subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" nil))))]])])))
[:td]
[:td.has-text-right "𝝙"]]
[:<>
(for [location @(re-frame/subscribe [::locations])]
^{:key location}
[location-rows location]
)]]]])])))
{:component-will-mount #(re-frame/dispatch-sync [::params-change {:from-date (date->str (t/minus (local-now) (t/period :years 1)) standard)
:to-date (date->str (local-now) standard)}]) }))

View File

@@ -226,8 +226,8 @@
(re-frame/dispatch (-> event
(conj field)
(conj (let [val (.. e -target -value)]
(cond (and val (not (str/blank? val))
(not (str/ends-with? val ".")))
(cond (and val
(re-matches #"[\-]?(\d+)(\.\d{2})?" val))
(js/parseFloat val)
(str/blank? val )