New report progress
This commit is contained in:
13
resources/sample-ledger-2.csv
Normal file
13
resources/sample-ledger-2.csv
Normal 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,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
,,,,,,,,,,
|
||||
|
@@ -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})
|
||||
|
||||
|
||||
@@ -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}}])
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
263
src/cljs/auto_ap/views/pages/ledger/profit_and_loss_detail.cljs
Normal file
263
src/cljs/auto_ap/views/pages/ledger/profit_and_loss_detail.cljs
Normal 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"])))}))
|
||||
@@ -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)]}
|
||||
|
||||
Reference in New Issue
Block a user