From ded5371e77353789cd742c15e466af365c293747 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sun, 31 Mar 2024 21:47:40 -0700 Subject: [PATCH] Makes simple sales summaries --- resources/schema.edn | 16 +- src/clj/auto_ap/jobs/sales_summaries.clj | 197 ++++++++++++++++-- src/clj/auto_ap/ssr/admin/sales_summaries.clj | 191 ++++++++++------- 3 files changed, 314 insertions(+), 90 deletions(-) diff --git a/resources/schema.edn b/resources/schema.edn index 6b6f79ac..6164a195 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -1902,6 +1902,18 @@ :db/noHistory true, :db/valueType :db.type/double :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-tax + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} +{:db/ident :sales-summary/total-returns + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} +{:db/ident :sales-summary/total-tip + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} {:db/ident :sales-summary/total-card-fees :db/noHistory true, :db/valueType :db.type/double @@ -1922,9 +1934,9 @@ :db/noHistory true, :db/valueType :db.type/double :db/cardinality :db.cardinality/one} -{:db/ident :sales-summary/total-food-app-refunds + {:db/ident :sales-summary/total-food-app-refunds :db/noHistory true, :db/valueType :db.type/double - :db/cardinality :db.cardinality/one} ] + :db/cardinality :db.cardinality/one}] diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj index 61410139..b501f21d 100644 --- a/src/clj/auto_ap/jobs/sales_summaries.clj +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -25,22 +25,20 @@ (.toDateMidnight (atime/localize (time/now)))]) (defn mark-all-dirty [days] - (doseq [c (dc/q '[:find ?c - :in $ - :where [_ :sales-order/client ?c]] - (dc/db conn))] - (apply mark-dirty [:client/code "NGOP"] (last-n-days days)))) + (doseq [[c] (dc/q '[:find ?c + :in $ + :where [_ :sales-order/client ?c]] + (dc/db conn))] + (apply mark-dirty c (last-n-days days)))) + -(comment - (mark-all-dirty 365)) -(str (c/to-local-date (atime/localize (time/now)))) (defn dirty-sales-summaries [c] (let [client-id (dc/entid (dc/db conn) c)] (->> (dc/index-pull (dc/db conn) {:index :avet - :selector '[:sales-summary/date :sales-summary/client] + :selector '[:sales-summary/date :sales-summary/client :db/id] :start [:sales-summary/client+dirty [client-id true]]}) (filter (fn [sales-summary] (= client-id (:db/id (:sales-summary/client sales-summary)))))))) @@ -48,9 +46,10 @@ (defn sales-summaries [] (doseq [[c client-code] (dc/q '[:find ?c ?client-code :in $ - :where [?c :client/code ?client-code]] + :where [?c :client/code ?client-code] + [(= ?client-code "NGCL")]] (dc/db conn)) - {:sales-summary/keys [date]} (dirty-sales-summaries c)] + {:sales-summary/keys [date] :db/keys [id]} (dirty-sales-summaries c)] (mu/with-context {:client-code client-code :date date} (alog/info ::updating) @@ -63,25 +62,157 @@ [?li :order-line-item/category ?category] [?li :order-line-item/total ?total] [?li :order-line-item/tax ?tax] - [?li :order-line-item/discount ?discount] ] + [?li :order-line-item/discount ?discount]] (dc/db conn) [[c] date date])) - result {:sales-summary/client c - :sales-summary/date (c/to-date (atime/parse date atime/normal-date)) + result {:db/id id + :sales-summary/client c + :sales-summary/date date :sales-summary/dirty false - :sales-summary/client+date [c (c/to-date (atime/parse date atime/normal-date))] + :sales-summary/client+date [c date] + :sales-summary/discount (or (ffirst (dc/q '[:find (sum ?discount) + :with ?e + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/discount ?discount]] + (dc/db conn) + [[c] date date])) + 0.0) +:sales-summary/total-returns (or (let [[t f] (first (dc/q '[:find (sum ?t) (sum ?f) + :with ?e + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-refund/total ?t] + [?e :sales-refund/fee ?f]] + (dc/db conn) + [[c] date date]))] + (when (and t f) + (- t f))) + + 0.0) :sales-summary/sales-items + + (for [[item-name category total tax discount] sales] - {:sales-summary-item/item-name item-name + {:db/id (str (java.util.UUID/randomUUID)) + :sales-summary-item/item-name item-name :sales-summary-item/category category :sales-summary-item/total total :sales-summary-item/tax tax - :sales-summary-item/discount discount})}] + :sales-summary-item/discount discount}) + + :sales-summary/total-tax + (or (ffirst (dc/q '[:find (sum ?tax) + :with ?e + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/tax ?tax] + #_[?e :sales-order/charges ?c] + #_[?c :charge/tax ?tax]] + (dc/db conn) + [[c] date date])) + 0.0) + :sales-summary/total-tip + (or (ffirst (dc/q '[:find (sum ?tip) + :with ?c + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/charges ?c] + [?c :charge/tip ?tip]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/total-card-payments + (or (ffirst (dc/q '[:find (sum ?total) + :with ?c + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/charges ?c] + [?c :charge/type-name "CARD"] + [?c :charge/total ?total]] + (dc/db conn) + [[c] date date])) + 0.0) + :sales-summary/total-card-fees + (or (ffirst (dc/q '[:find ?f + :in $ ?client ?d + :where + [?e :expected-deposit/client ?client] + [?e :expected-deposit/sales-date ?d] + [?e :expected-deposit/fee ?f]] + (dc/db conn) + c + date)) + 0.0) + :sales-summary/total-card-refunds + (or (ffirst (dc/q '[:find (sum ?t) + :in $ [?clients ?start-date ?end-date] + :where + :where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-refund/type "CARD"] + [?e :sales-refund/total ?t]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/total-cash-payments + (or (ffirst (dc/q '[:find (sum ?total) + :with ?c + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/charges ?c] + [?c :charge/total ?total] + [?c :charge/type-name "CASH"]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/total-cash-refunds + (or (ffirst (dc/q '[:find (sum ?t) + :in $ [?clients ?start-date ?end-date] + :where + :where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-refund/type "CASH"] + [?e :sales-refund/total ?t]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/total-food-app-payments + + (or (ffirst (dc/q '[:find (sum ?total) + :with ?c + :in $ [?clients ?start-date ?end-date] [?processor ...] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/charges ?c] + [?c :charge/processor ?processor] + [?c :charge/total ?total]] + (dc/db conn) + [[c] date date] + #{:ccp-processor/toast + #_:ccp-processor/ezcater + #_:ccp-processor/koala + :ccp-processor/doordash + :ccp-processor/grubhub + :ccp-processor/uber-eats})) + 0.0) + :sales-summary/total-food-app-refunds + (or (ffirst (dc/q '[:find (sum ?t) + :in $ [?clients ?start-date ?end-date] + :where + :where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + (not [?e :sales-refund/type "CASH"]) + (not [?e :sales-refund/type "CARD"]) + [?e :sales-refund/total ?t]] + (dc/db conn) + [[c] date date])) + 0.0)}] (when (seq (:sales-summary/sales-items result)) (alog/info ::upserting-summaries :category-count (count (:sales-summary/sales-items result))) - @(dc/transact conn [result])))))) + @(dc/transact conn [ [:upsert-entity result]])))))) (defn reset-summaries [] @(dc/transact conn (->> (dc/q '[:find ?sos @@ -91,6 +222,36 @@ (map (fn [[sos]] [:db/retractEntity sos]))))) + + + +(comment + (auto-ap.datomic/transact-schema conn) + + (apply mark-dirty [:client/code "NGCL"] (last-n-days 12)) + + #_(mark-all-dirty 700) + + (sales-summaries) + + (dc/q '[:find (pull ?sos [* {:sales-summary/sales-items [*]}]) + :in $ + :where [?sos :sales-summary/client [:client/code "NGCL"]] + [?sos :sales-summary/date ?d] + [(= ?d #inst "2024-03-25T00:00:00-07:00")]] + (dc/db conn)) + + + + + + + + + ) + + + (defn -main [& _] (execute "sales-summaries" sales-summaries)) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/admin/sales_summaries.clj b/src/clj/auto_ap/ssr/admin/sales_summaries.clj index 4fbd80b0..cf808d66 100644 --- a/src/clj/auto_ap/ssr/admin/sales_summaries.clj +++ b/src/clj/auto_ap/ssr/admin/sales_summaries.clj @@ -1,52 +1,50 @@ (ns auto-ap.ssr.admin.sales-summaries - (:require [auto-ap.client-routes :as client-routes] - [auto-ap.datomic - :refer [apply-pagination apply-sort-3 conn merge-query pull-many query2]] + (:require [auto-ap.datomic + :refer [apply-pagination apply-sort-3 conn merge-query pull-many + query2]] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.routes.admin.sales-summaries :as route] [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] - [auto-ap.ssr.components.date-range :refer [date-range-field]] [auto-ap.ssr.grid-page-helper :as helper] - [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers]] + :refer [apply-middleware-to-all-handlers]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [clj-time.coerce :as c] [datomic.api :as dc] - [hiccup.util :as hu])) + [iol-ion.query :refer [dollars=]])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes - ::route/table) + ::route/table) "hx-target" "#entity-table" "hx-indicator" "#entity-table"} - #_[:fieldset.space-y-6 - (date-range-field {:value {:start (:start-date (:parsed-query-params request)) - :end (:end-date (:parsed-query-params request))} - :id "date-range"}) - (com/field {:label "Source"} - (com/select {:name "source" - :class "hot-filter w-full" - :value (:source (:parsed-query-params request)) - :placeholder "" - :options (ref->select-options "import-source" :allow-nil? true)})) + #_[:fieldset.space-y-6 + (date-range-field {:value {:start (:start-date (:parsed-query-params request)) + :end (:end-date (:parsed-query-params request))} + :id "date-range"}) + (com/field {:label "Source"} + (com/select {:name "source" + :class "hot-filter w-full" + :value (:source (:parsed-query-params request)) + :placeholder "" + :options (ref->select-options "import-source" :allow-nil? true)})) - #_(com/field {:label "Code"} - (com/text-input {:name "code" - :id "code" - :class "hot-filter" - :value (:code (:parsed-query-params request)) - :placeholder "11101" - :size :small}))]]) + #_(com/field {:label "Code"} + (com/text-input {:name "code" + :id "code" + :class "hot-filter" + :value (:code (:parsed-query-params request)) + :placeholder "11101" + :size :small}))]]) (def default-read '[:db/id - [ :sales-summary/date :xform clj-time.coerce/from-date] + [:sales-summary/date :xform clj-time.coerce/from-date] *]) ;; TODO (defn fetch-ids [db request] @@ -58,7 +56,7 @@ [:client/code (:client-code query-params)])) query (cond-> {:query {:find [] :in '[$ [?client ...]] - :where '[ [?e :sales-summary/client ?client]]} + :where '[[?e :sales-summary/client ?client]]} :args [db valid-clients]} (or (:start-date query-params) (:end-date query-params)) @@ -74,7 +72,7 @@ :where [[(< ?d ?end-date)]]} :args [(-> query-params :end-date c/to-date)]}) - + true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :sales-summary/date ?sort-default]]}}))] @@ -97,6 +95,19 @@ [(->> (hydrate-results ids-to-retrieve db request)) matching-count])) +(defn get-credits [ss] + {:card-payments (+ (:sales-summary/total-card-payments ss 0.0) + (:sales-summary/total-card-fees ss 0.0) + (- (:sales-summary/total-card-refunds ss 0.0))) + :food-app-payments (+ (:sales-summary/total-food-app-payments ss 0.0) + (:sales-summary/total-food-app-fees ss 0.0) + (- (:sales-summary/total-food-app-refunds ss 0.0))) + :fees (- (:sales-summary/total-card-fees ss 0.0)) + :cash-payments (+ (:sales-summary/total-cash-payments ss 0.0) + (- (:sales-summary/total-cash-refunds ss 0.0))) + :discounts (+ (:sales-summary/discount ss 0.0)) + :returns (+ (:sales-summary/total-returns ss 0.0))}) + (def grid-page (helper/build {:id "entity-table" :id-fn :db/id @@ -108,10 +119,10 @@ :oob-render (fn [request] [#_(assoc-in (date-range-field {:value {:start (:start-date (:parsed-query-params request)) - :end (:end-date (:parsed-query-params request))} - :id "date-range"}) [1 :hx-swap-oob] true)]) ;; TODO + :end (:end-date (:parsed-query-params request))} + :id "date-range"}) [1 :hx-swap-oob] true)]) ;; TODO :parse-query-params (comp - (helper/default-parse-query-params grid-page)) + (helper/default-parse-query-params grid-page)) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} "Admin"] @@ -126,51 +137,91 @@ :name "Date" :sort-key "date" :render #(some-> % :sales-summary/date (atime/unparse-local atime/normal-date))} - {:key "items" - :name "items" - :sort-key "source" + {:key "credits" + :name "Credits" + :sort-key "credits" :render (fn [ss] - [:ul - (for [[ n x] (group-by :sales-summary-item/category (:sales-summary/sales-items ss))] - [:li n ": " (format "$%,.2f" (reduce + 0.0 (map :sales-summary-item/total x)))])] - - #_(count ))} - {:key "payments" - :name "payments" - :sort-key "payments" + (let [total-credits (reduce + 0.0 (vals (get-credits ss))) + total-debits (+ (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss))) + + (reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss)))) + (reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss)))) + + (:sales-summary/total-tax ss 0.0) + (:sales-summary/total-tip ss 0.0))] + [:ul + (for [[n x] (group-by :sales-summary-item/category (:sales-summary/sales-items ss))] + [:li n ": " (format "$%,.2f" (- (+ (reduce + 0.0 (map :sales-summary-item/total x)) + (reduce + 0.0 (map :sales-summary-item/discount x))) + (reduce + 0.0 (map :sales-summary-item/tax x))))]) + [:li "Sales subtotal: " (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss))) + (reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss)))) + + (reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss))))] + [:li "Tax: " (format "$%,.2f" (:sales-summary/total-tax ss))] + [:li "Tips: " (format "$%,.2f" (:sales-summary/total-tip ss))] + [:li (com/pill {:color (if (dollars= total-credits total-debits) + :primary + :red)} "Total: " (format "$%,.2f" total-debits))]]) + + #_(count))} + + {:key "debits" + :name "debits" + :sort-key "debits" :render (fn [ss] - [:ul - [:li "Card Payments: " - (format "$%,.2f" (:sales-summary/total-card-payments ss 0.0)) + (let [{:keys [card-payments food-app-payments + cash-payments discounts fees + returns] :as credits} (get-credits ss) + total-credits (reduce + 0.0 (vals credits)) + total-debits (+ (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss))) - ", fees: " - (format "$%,.2f" (:sales-summary/total-card-fees ss 0.0)) + (reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss)))) + (reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss)))) - ", refunds: " - (format "$%,.2f" (:sales-summary/total-card-refunds ss 0.0))] - - [:li "Cash Payments: " - (format "$%,.2f" (:sales-summary/total-cash-payments ss 0.0)) + (:sales-summary/total-tax ss 0.0) + (:sales-summary/total-tip ss 0.0))] + [:ul + [:li "Card Payments: " + (format "$%,.2f" card-payments)] - ", refunds: " - (format "$%,.2f" (:sales-summary/total-cash-refunds ss 0.0))] - - [:li "Food App Payments: " - (format "$%,.2f" (:sales-summary/total-food-app-payments ss 0.0)) + [:li "Food App Payments: " + (format "$%,.2f" food-app-payments)] + [:li "Cash Payments: " - ", refunds: " - (format "$%,.2f" (:sales-summary/total-food-app-refunds ss 0.0)) ]] - - #_(count ))}]})) + (format "$%,.2f" cash-payments)] + [:li "Discounts: " + + (format "$%,.2f" discounts)] + + [:li "Fees: " + (format "$%,.2f" fees)] + [:li "Returns: " + (format "$%,.2f" returns)] + + + [:li (com/pill {:color (if (dollars= total-credits total-debits) + :primary + :red)} "Total: " (format "$%,.2f" total-credits))]]) + + #_(count))}]})) + +;; TODO schema cleanup +;; Decide on what should be calculated as generating ledger entries, and what should be calculated +;; as part of the summary +;; default thought here is that the summary has more detail (e.g., line items), fees broken out by type +;; and aggregated into the final ledger entry +;; that allows customization at any level. +;; TODO rename refunds/returns (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) (def key->handler - (apply-middleware-to-all-handlers - (->> - {::route/page (helper/page-route grid-page) - ::route/table (helper/table-route grid-page)}) - (fn [h] - (-> h - (wrap-admin) - (wrap-client-redirect-unauthenticated))))) + (apply-middleware-to-all-handlers + (->> + {::route/page (helper/page-route grid-page) + ::route/table (helper/table-route grid-page)}) + (fn [h] + (-> h + (wrap-admin) + (wrap-client-redirect-unauthenticated)))))