New report progress

This commit is contained in:
2022-10-15 10:58:08 -07:00
parent 68721520e6
commit 94d1ca75c0
6 changed files with 319 additions and 3 deletions

View File

@@ -0,0 +1,13 @@
Id,Client,Source,Vendor,Date,Account,Location,Debit,Credit,Note,Cleared Against
HELLO,DEMO,Payroll,HelloVend,09/06/2022,24000,A,0,100,,
HELLO,DEMO,Payroll,HelloVend,09/06/2022,21000,A,100,0,,
,,,,,,,,,,
,,,,,,,,,,
,,,,,,,,,,
,,,,,,,,,,
,,,,,,,,,,
,,,,,,,,,,
,,,,,,,,,,
,,,,,,,,,,
,,,,,,,,,,
,,,,,,,,,,
1 Id Client Source Vendor Date Account Location Debit Credit Note Cleared Against
2 HELLO DEMO Payroll HelloVend 09/06/2022 24000 A 0 100
3 HELLO DEMO Payroll HelloVend 09/06/2022 21000 A 100 0
4
5
6
7
8
9
10
11
12
13

View File

@@ -4,6 +4,7 @@
[auto-ap.datomic.accounts :as a]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.ledger :as l]
[auto-ap.ledger.reports :as l-reports]
[auto-ap.datomic.vendors :as d-vendors]
[auto-ap.graphql.utils
:refer [->graphql <-graphql assert-admin assert-can-see-client result->page]]
@@ -503,6 +504,18 @@
:stop (scheduler/stop running-balance-cache-worker))
(defn get-journal-detail-report [context input _]
(clojure.pprint/pprint
(for [category (:categories input)]
{:category category
:journal_entries (:journal_entries (get-ledger-page context {:filters (doto {:client_ids (:client_ids input)
:date_range (:date_range input)
:from_numeric_code (get-in l-reports/ranges [category 0])
:to_numeric_code (get-in l-reports/ranges [category 1])
:per_page Integer/MAX_VALUE}
clojure.pprint/pprint)}
nil))})))
(def objects
{:balance_sheet_account
@@ -567,7 +580,13 @@
{:fields {:successful {:type '(list :import_ledger_entry_result)}
:existing {:type '(list :import_ledger_entry_result)}
:ignored {:type '(list :import_ledger_entry_result)}
:errors {:type '(list :import_ledger_entry_result)}}}})
:errors {:type '(list :import_ledger_entry_result)}}}
:journal_detail_report_category
{:fields {:category {:type :ledger_category}
:journal_entries {:type '(list :journal_entry)}}}
:journal_detail_report
{:fields {:categories {:type '(list :journal_detail_report_category)}}}})
(def queries
{:balance_sheet {:type :balance_sheet
@@ -585,6 +604,13 @@
:column_per_location {:type 'Boolean}}
:resolve :get-profit-and-loss}
:journal_detail_report {:type :journal_detail_report
:args {:client_id {:type :id}
:client_ids {:type '(list :id)}
:date_range {:type :date_range}
:categories {:type '(list :ledger_category)}}
:resolve :get-journal-detail-report}
:profit_and_loss_pdf {:type :report_pdf
:args {:client_id {:type :id}
:client_ids {:type '(list :id)}
@@ -650,11 +676,12 @@
:note {:type 'String}
:cleared_against {:type 'String}
:line_items {:type '(list :import_ledger_line_item)}}}
})
(def enums
{})
{:ledger_category {:values [{:enum-value :sales}
{:enum-value :cogs}
{:enum-value :payroll}]}})
(def resolvers
@@ -663,6 +690,7 @@
:get-profit-and-loss get-profit-and-loss
:profit-and-loss-pdf profit-and-loss-pdf
:balance-sheet-pdf balance-sheet-pdf
:get-journal-detail-report get-journal-detail-report
:mutation/delete-external-ledger delete-external-ledger
:mutation/import-ledger import-ledger})

View File

@@ -35,6 +35,7 @@
"yodlee2" :yodlee2
"ledger/" {"" :ledger
"profit-and-loss" :profit-and-loss
"profit-and-loss-detail" :profit-and-loss-detail
"balance-sheet" :balance-sheet
"external" :external-ledger
"external-import" :external-import-ledger}}])

View File

@@ -15,6 +15,7 @@
[auto-ap.views.pages.ledger.external-import :refer [external-import-page]]
[auto-ap.views.pages.ledger.external-ledger :refer [external-ledger-page]]
[auto-ap.views.pages.ledger.profit-and-loss :refer [profit-and-loss-page]]
[auto-ap.views.pages.ledger.profit-and-loss-detail :refer [profit-and-loss-detail-page]]
[auto-ap.views.pages.login :refer [login-page]]
[auto-ap.views.pages.payments :refer [payments-page]]
[auto-ap.views.pages.pos.sales-orders :refer [sales-orders-page]]
@@ -85,6 +86,10 @@
(defmethod page :profit-and-loss [_]
(profit-and-loss-page))
(defmethod page :profit-and-loss-detail [_]
(profit-and-loss-detail-page))
(defmethod page :balance-sheet [_]
(balance-sheet-page))

View File

@@ -0,0 +1,263 @@
(ns auto-ap.views.pages.ledger.profit-and-loss-detail
(:require
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.ledger.reports :as l-reports]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.views.components :as com]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.pages.ledger.report-table :as rtable]
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
[auto-ap.views.utils
:refer [date->str date-picker dispatch-event query-params standard with-user local-now]]
[cljs-time.core :as t]
[clojure.string :as str]
[re-frame.core :as re-frame]
[react-dom :as react-dom]
[reagent.core :as reagent]
[vimsical.re-frame.cofx.inject :as inject]))
(defn encode-period [p]
{:start (date->str (:start p) standard)
:end (date->str (:end p) standard)})
;; SUBS
(re-frame/reg-event-db
::received
[(forms/in-form ::form)]
(fn [db [_ data]]
(-> db (assoc :report (:profit-and-loss data)))))
;; EVENTS
(re-frame/reg-event-fx
::report-requested
[with-user (forms/in-form ::form)]
(fn [{:keys [db user]}]
(cond-> {:graphql {:token user
:owns-state {:single ::page}
:query-obj {:venia/queries [[:journal-detail-report
{:client-ids (map (comp :id :client) (:clients (:data db)))
:date-range {:start (date->str (:start (:date-range (:data db))) standard)
:end (date->str (:end (:date-range (:data db))) standard)}
:categories [:sales
:cogs
:payroll]}
[[:categories [[:journal-entries [:id
:source
:original-entity
:note
:amount
:alternate-description
[:vendor
[:name :id]]
[:client
[:name :id]]
[:line-items
[:id :debit :credit :location :running-balance
[:account [:id :name]]]]
:date]]
:category]]]]]}
:on-success [::received]}
:set-uri-params {:date-range (:date-range (:data db))
:clients (mapv #(select-keys (:client %) [:name :id]) (:clients (:data db))) }
:db (-> db
(dissoc :report)
(update-in [:data :clients] #(into [] (filter seq %))))})))
(defn email-body [report-url]
(js/encodeURIComponent
(str
"Hello,
Click here (" report-url ") to download your financial reports. We have not finished reviewing and reconciling these numbers with you. Please review and let us know if anything seems missing or in need of correction.
Click here (http://app.integreatconsult.com/) to login to the Financials app to review the details here.
Click here (https://share.vidyard.com/watch/MHTo5PyXPxXUpVH93RWFM9?) for a video on how to run a P&L on your own.
To see a history of past financial reports, click here: https://app.integreatconsult.com/reports/
NOTE: Please review the transactions we may have question for you here: https://app.integreatconsult.com/transactions/requires-feedback. You can either edit the transaction to what expense account it should be or email back what it should be.")))
(re-frame/reg-event-fx
::received-pdf
[(re-frame/inject-cofx ::inject/sub [::subs/clients-by-id])]
(fn [{:keys [db ::subs/clients-by-id]} [_ result]]
(let [selected-clients (-> db ::forms/forms ::form :data :clients)
single-client? (= (count selected-clients)
1)
client-emails (when single-client?
(-> clients-by-id
(get (:id (:client (first selected-clients))))
:emails))]
{:dispatch [::modal/modal-requested {:title "Your report is ready"
:body [:div
[:div "Click "
[:a {:href (-> result :profit-and-loss-pdf :url) :target "_new"} "here"] " to view it."]
(when (and single-client? (seq client-emails))
[:div "Once you've confirmed you're happy with it, click "
[:a {:href (str "mailto:" (str/join ";" (map :email client-emails)) "?body=" (email-body (-> result :profit-and-loss-pdf :url))
"&subject=" (-> result :profit-and-loss-pdf :name) " is ready")}
"here"] " to open your email client and to send it to " (str/join "," (map (fn [e]
(str (:email e) " (" (:description e) ")"))
client-emails)) "."])]}]})))
(re-frame/reg-event-fx
::export-pdf
[with-user (forms/in-form ::form)]
(fn [{:keys [db user]}]
(cond-> {:graphql {:token user
:owns-state {:single ::page}
:query-obj {:venia/queries [[:profit-and-loss-pdf
{:client-ids (map (comp :id :client) (:clients (:data db)))
:include-deltas (:include-deltas (:data db))
:column-per-location (:column-per-location (:data db))
:periods (mapv #(select-keys % #{:start :end}) (:periods (:data db)))}
[:url :name]]]}
:on-success [::received-pdf]}
:set-uri-params {:date-range (:date-range (:data db))
:clients (mapv #(select-keys (:client %) [:name :id]) (:clients (:data db))) }
:db (dissoc db :report)})))
(re-frame/reg-event-fx
::change
[with-user (forms/in-form ::form)]
(fn [{:keys [db]} [_ & event]]
{:db (dissoc db :report)}))
(defn report-control-detail [{:keys [active box which]} children]
(when (and @box
(= which @active))
(react-dom/createPortal (reagent/as-element
[:div.notification.is-light
[:a.delete {:on-click (fn [] (reset! active nil))}]
children
])
@box)))
(defn report-controls []
(let [!box (atom nil)
active (reagent/atom nil)]
(fn []
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
{:keys []} data]
[form-builder/builder {:change-event [::change]
:submit-event [::report-requested]
:id ::form}
[:div.report-controls
[:div.level.mb-2
[:div.level-left
[:div.level-item
[buttons/dropdown {:on-click (fn [] (reset! active :clients))}
[:span (str "Companies"
(when-let [clients (:clients data)]
(str " (" (str/join ", " (map (comp :name :client) clients)) ")")))]]
[report-control-detail {:active active :box !box :which :clients}
[:div {:style {:width "20em"}}
[:h4.subtitle "Companies"]
[form-builder/raw-field-v2 {:field :clients}
[com/multi-field-v2 {:new-text "Add another company"
:template [[form-builder/raw-field-v2 {:field :client}
[com/entity-typeahead {:entities @(re-frame/subscribe [::subs/clients])
:style {:width "18em"}
:entity->text :name}]]]
:key-fn :id}]]]]]
[:div.level-item
"From"]
[:div.level-item
[form-builder/raw-field-v2 {:field [:date-range :start]}
[date-picker {:output :cljs-date}]]]
[:div.level-item
" To "]
[:div.level-item
[form-builder/raw-field-v2 {:field [:date-range :end]}
[date-picker {:output :cljs-date}]]]
[:div.level-right
[:div.buttons
(when @(re-frame/subscribe [::subs/is-admin?])
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
[:button.button.is-primary "Run"]]]]]
[:div.report-control-detail {:ref (fn [el]
(when (not= @!box el)
(reset! !box el)))}]]]))))
(defn pnl-report [{:keys [args report-data]}]
(let [pnl-data (->> report-data
:periods
(mapcat (fn [p1 p2]
(map
(fn [a]
(assoc a :period p1
:amount (js/parseFloat (:amount a)))
)
(:accounts p2)))
(:periods args)))
client-codes (->> @(re-frame/subscribe [::subs/clients-by-id])
(map (fn [[k v]]
[k (:code v)]))
(into {}))
pnl-data (l-reports/->PNLData args pnl-data client-codes)
report (l-reports/summarize-pnl pnl-data)
table (rtable/concat-tables (concat (:summaries report) (:details report)))]
[:div
[:h1.title "Profit and Loss - " (str/join ", " (map (comp :name :client) (:clients args)))]
(when (:warning report)
[:div.notification.is-warning.is-light
(:warning report)])
[rtable/table {:widths (into [20] (take (dec (rtable/cell-count table))
(mapcat identity
(repeat
(if (-> pnl-data :args :include-deltas)
[13 6 13]
[13 6])))))
:table table}]]))
;; TODO Break out one category per location
;; TODO filter ledger entry lines to only ones that match
;; TODO Render them!
(defn profit-and-loss-content []
(let [status @(re-frame/subscribe [::status/single ::page])
{:keys [data report]} @(re-frame/subscribe [::forms/form ::form])]
[:div
[:div
[status/status-notification {:statuses [[::status/single ::page]]}]
[report-controls]]
[status/big-loader status]
(when (and (not= :loading (:state status))
report)
[pnl-report {:report-data report
:args data}])]))
(re-frame/reg-event-fx
::mounted-pnl
(fn [{:keys [db]} _]
(let [qp (query-params)]
{:db (forms/start-form db ::form {:date-range {:end (local-now)
:start (t/minus (local-now) (t/years 1))}
:clients (mapv (fn [c] {:client c :id (random-uuid)})
(or (:clients qp)
[(some-> @(re-frame/subscribe [::subs/client]) (select-keys [:name :id]) )]))})
:dispatch [::status/dispose-single ::page]})))
(defn profit-and-loss-detail-page []
(reagent/create-class
{:display-name "profit-and-loss-detail-page"
:component-did-mount #(re-frame/dispatch [::mounted-pnl])
:reagent-render
(fn []
(let [user (re-frame/subscribe [::subs/user])]
(if (not= "manager" (:user/role @user))
[side-bar-layout
{:side-bar [ledger-side-bar]
:main [profit-and-loss-content]}]
[:div "Not authorized"])))}))

View File

@@ -30,6 +30,12 @@
[:span {:class "icon icon-performance-increase-1" :style {:font-size "25px"}}]
[:span {:class "name"} "Profit & Loss"]]]
[:li.menu-item
[:a.item {:href (bidi/path-for routes/routes :profit-and-loss-detail)
:class [(active-when ap = :profit-and-loss-detail)]}
[:span {:class "icon icon-performance-increase-1" :style {:font-size "25px"}}]
[:span {:class "name"} "Profit & Loss Detail"]]]
[:li.menu-item
[:a.item {:href (bidi/path-for routes/routes :balance-sheet)
:class [(active-when ap = :balance-sheet)]}