progress on a shared pnl with pdf.
This commit is contained in:
@@ -119,27 +119,7 @@
|
||||
:phone {:type 'String}}}
|
||||
|
||||
|
||||
:balance_sheet_account
|
||||
{:fields {:id {:type 'String}
|
||||
:amount {:type 'String}
|
||||
:location {:type 'String}
|
||||
:client_id {:type :id}
|
||||
:count {:type 'Int}
|
||||
:numeric_code {:type 'Int}
|
||||
:account_type {:type :account_type}
|
||||
:name {:type 'String}}}
|
||||
|
||||
:balance_sheet
|
||||
{:fields {:balance_sheet_accounts {:type '(list :balance_sheet_account)}
|
||||
:comparable_balance_sheet_accounts {:type '(list :balance_sheet_account)}}}
|
||||
|
||||
:profit_and_loss_report_period
|
||||
{:fields {:accounts {:type '(list :balance_sheet_account)}}}
|
||||
|
||||
:profit_and_loss_report
|
||||
{:fields {:periods {:type '(list :profit_and_loss_report_period)}}}
|
||||
|
||||
:address
|
||||
:address
|
||||
{:fields {:street1 {:type 'String}
|
||||
:street2 {:type 'String}
|
||||
:city {:type 'String}
|
||||
@@ -201,26 +181,7 @@
|
||||
:sent {:type 'String}
|
||||
:vendor {:type :vendor}}}
|
||||
|
||||
:journal_entry_line
|
||||
{:fields {:id {:type :id}
|
||||
:account {:type :account}
|
||||
:location {:type 'String}
|
||||
:debit {:type 'String}
|
||||
:credit {:type 'String}
|
||||
:running_balance {:type :money}}}
|
||||
:journal_entry
|
||||
{:fields {:id {:type :id}
|
||||
:source {:type 'String}
|
||||
:external_id {:type 'String}
|
||||
:original_entity {:type :id}
|
||||
:amount {:type 'String}
|
||||
:note {:type 'String}
|
||||
:cleared_against {:type 'String}
|
||||
:client {:type :client}
|
||||
:vendor {:type :vendor}
|
||||
:alternate_description {:type 'String}
|
||||
:date {:type 'String}
|
||||
:line_items {:type '(list :journal_entry_line)}}}
|
||||
|
||||
|
||||
|
||||
:order_line_item
|
||||
@@ -251,10 +212,6 @@
|
||||
:date {:type 'String}
|
||||
:charges {:type '(list :charge)}
|
||||
:line_items {:type '(list :order_line_item)}}}
|
||||
|
||||
|
||||
|
||||
|
||||
:yodlee_merchant {:fields {:id {:type :id}
|
||||
:yodlee_id {:type 'String}
|
||||
:name {:type 'String}}}
|
||||
@@ -322,11 +279,7 @@
|
||||
:start {:type 'Int}
|
||||
:end {:type 'Int}}}
|
||||
|
||||
:ledger_page {:fields {:journal_entries {:type '(list :journal_entry)}
|
||||
:count {:type 'Int}
|
||||
:total {:type 'Int}
|
||||
:start {:type 'Int}
|
||||
:end {:type 'Int}}}
|
||||
|
||||
|
||||
:sales_order_page {:fields {:sales_orders {:type '(list :sales_order)}
|
||||
:count {:type 'Int}
|
||||
@@ -351,16 +304,7 @@
|
||||
:paid {:type 'String}
|
||||
:unpaid {:type 'String}}}
|
||||
|
||||
:import_ledger_entry_result {:fields {:external_id {:type 'String}
|
||||
:error {:type 'String}
|
||||
:status {:type 'String}}}
|
||||
|
||||
:import_ledger_result {: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)}}}
|
||||
|
||||
:upcoming_transaction {:fields {:amount {:type :money}
|
||||
:upcoming_transaction {:fields {:amount {:type :money}
|
||||
:identifier {:type 'String}
|
||||
:date {:type :iso_date}}}
|
||||
|
||||
@@ -394,20 +338,7 @@
|
||||
:cash_flow {:type :cash_flow_result
|
||||
:args {:client_id {:type :id}}
|
||||
:resolve :get-cash-flow}
|
||||
:balance_sheet {:type :balance_sheet
|
||||
:args {:client_id {:type :id}
|
||||
:include_comparison {:type 'Boolean}
|
||||
:date {:type :iso_date}
|
||||
:comparison_date {:type :iso_date}}
|
||||
:resolve :get-balance-sheet}
|
||||
|
||||
:profit_and_loss {:type :profit_and_loss_report
|
||||
:args {:client_id {:type :id}
|
||||
:client_ids {:type '(list :id)}
|
||||
:periods {:type '(list :date_range)}}
|
||||
:resolve :get-profit-and-loss}
|
||||
|
||||
:yodlee_provider_account_page {:type :yodlee_provider_account_page
|
||||
:yodlee_provider_account_page {:type :yodlee_provider_account_page
|
||||
:args {:client_id {:type :id}}
|
||||
:resolve :get-yodlee-provider-account-page}
|
||||
|
||||
@@ -442,9 +373,7 @@
|
||||
:description {:type 'String}}
|
||||
:resolve :get-transaction-rule-page}
|
||||
|
||||
:ledger_page {:type :ledger_page
|
||||
:args {:filters {:type :ledger_filters}}
|
||||
:resolve :get-ledger-page}
|
||||
|
||||
|
||||
:sales_order_page {:type :sales_order_page
|
||||
:args {:client_id {:type :id}
|
||||
@@ -472,44 +401,9 @@
|
||||
:sort_name {:type 'String}
|
||||
:asc {:type 'Boolean}}}
|
||||
|
||||
:ledger_filters {:fields {:client_id {:type :id}
|
||||
:vendor_id {:type :id}
|
||||
:account_id {:type :id}
|
||||
:amount_lte {:type :money}
|
||||
:amount_gte {:type :money}
|
||||
:bank_account_id {:type :id}
|
||||
:date_range {:type :date_range}
|
||||
:location {:type 'String}
|
||||
:from_numeric_code {:type 'Int}
|
||||
:to_numeric_code {:type 'Int}
|
||||
:start {:type 'Int}
|
||||
:per_page {:type 'Int}
|
||||
:only_external {:type 'Boolean}
|
||||
:external_id_like {:type 'String}
|
||||
:source {:type 'String}
|
||||
:sort {:type '(list :sort_item)}}}
|
||||
|
||||
|
||||
|
||||
|
||||
:date_range {:fields {:start {:type :iso_date}
|
||||
:end {:type :iso_date}}}
|
||||
|
||||
:import_ledger_line_item {:fields {:account_identifier {:type 'String}
|
||||
:location {:type 'String}
|
||||
:debit {:type :money}
|
||||
:credit {:type :money}}}
|
||||
:import_ledger_entry {:fields {:source {:type 'String}
|
||||
:external_id {:type 'String}
|
||||
:client_code {:type 'String}
|
||||
:date {:type 'String}
|
||||
:vendor_name {:type 'String}
|
||||
:amount {:type :money}
|
||||
:note {:type 'String}
|
||||
:cleared_against {:type 'String}
|
||||
:line_items {:type '(list :import_ledger_line_item)}}}
|
||||
|
||||
|
||||
:edit_user
|
||||
{:fields {:id {:type :id}
|
||||
:name {:type 'String}
|
||||
@@ -612,7 +506,6 @@
|
||||
:type_1099 {:values [{:enum-value :none}
|
||||
{:enum-value :misc}
|
||||
{:enum-value :landlord}]}
|
||||
|
||||
:applicability {:values [{:enum-value :global}
|
||||
{:enum-value :optional}
|
||||
{:enum-value :customized}]}
|
||||
@@ -623,46 +516,40 @@
|
||||
{:enum-value :equity}
|
||||
{:enum-value :revenue}]}}
|
||||
:mutations
|
||||
{:request_import {:type 'String
|
||||
:args {:which {:type 'String}}
|
||||
:resolve :mutation/request-import}
|
||||
{:request_import
|
||||
{:type 'String
|
||||
:args {:which {:type 'String}}
|
||||
:resolve :mutation/request-import}
|
||||
|
||||
:delete_external_ledger {:type :message
|
||||
:args {:filters {:type :ledger_filters}
|
||||
:ids {:type '(list :id)}}
|
||||
:resolve :mutation/delete-external-ledger}
|
||||
:delete_transaction_rule
|
||||
{:type :id
|
||||
:args {:transaction_rule_id {:type :id}}
|
||||
:resolve :mutation/delete-transaction-rule}
|
||||
:merge_vendors
|
||||
{:type :id
|
||||
:args {:from {:type :id}
|
||||
:to {:type :id}}
|
||||
:resolve :mutation/merge-vendors}
|
||||
|
||||
:delete_transaction_rule {:type :id
|
||||
:args {:transaction_rule_id {:type :id}}
|
||||
:resolve :mutation/delete-transaction-rule}
|
||||
:merge_vendors {:type :id
|
||||
:args {:from {:type :id}
|
||||
:to {:type :id}}
|
||||
:resolve :mutation/merge-vendors}
|
||||
:edit_user
|
||||
{:type :user
|
||||
:args {:edit_user {:type :edit_user}}
|
||||
:resolve :mutation/edit-user}
|
||||
|
||||
:edit_user {:type :user
|
||||
:args {:edit_user {:type :edit_user}}
|
||||
:resolve :mutation/edit-user}
|
||||
:upsert_vendor
|
||||
{:type :vendor
|
||||
:args {:vendor {:type :add_vendor}}
|
||||
:resolve :mutation/upsert-vendor}
|
||||
|
||||
|
||||
:upsert_transaction_rule
|
||||
{:type :transaction_rule
|
||||
:args {:transaction_rule {:type :edit_transaction_rule}}
|
||||
:resolve :mutation/upsert-transaction-rule}
|
||||
|
||||
:upsert_vendor {:type :vendor
|
||||
:args {:vendor {:type :add_vendor}}
|
||||
:resolve :mutation/upsert-vendor}
|
||||
|
||||
:upsert_transaction_rule {:type :transaction_rule
|
||||
:args {:transaction_rule {:type :edit_transaction_rule}}
|
||||
:resolve :mutation/upsert-transaction-rule}
|
||||
|
||||
:import_ledger {:type :import_ledger_result
|
||||
:args {:entries {:type '(list :import_ledger_entry)}}
|
||||
:resolve :mutation/import-ledger}
|
||||
|
||||
:upsert_account {:type :account
|
||||
:args {:account {:type :edit_account}}
|
||||
:resolve :mutation/upsert-account}
|
||||
|
||||
}})
|
||||
:upsert_account
|
||||
{:type :account
|
||||
:args {:account {:type :edit_account}}
|
||||
:resolve :mutation/upsert-account}}})
|
||||
|
||||
|
||||
(defn snake->kebab [s]
|
||||
@@ -881,10 +768,7 @@
|
||||
:get-yodlee-provider-account-page gq-yodlee2/get-yodlee-provider-account-page
|
||||
:get-all-sales-orders get-all-sales-orders
|
||||
:get-accounts gq-accounts/get-accounts
|
||||
:get-ledger-page gq-ledger/get-ledger-page
|
||||
:get-sales-order-page gq-sales-orders/get-sales-orders-page
|
||||
:get-balance-sheet gq-ledger/get-balance-sheet
|
||||
:get-profit-and-loss gq-ledger/get-profit-and-loss
|
||||
:get-transaction-rule-page gq-transaction-rules/get-transaction-rule-page
|
||||
:get-transaction-rule-matches gq-transaction-rules/get-transaction-rule-matches
|
||||
:get-expense-account-stats get-expense-account-stats
|
||||
@@ -895,17 +779,16 @@
|
||||
:get-user get-user
|
||||
:mutation/delete-transaction-rule gq-transaction-rules/delete-transaction-rule
|
||||
:mutation/edit-user gq-users/edit-user
|
||||
:mutation/delete-external-ledger gq-ledger/delete-external-ledger
|
||||
:mutation/upsert-transaction-rule gq-transaction-rules/upsert-transaction-rule
|
||||
:test-transaction-rule gq-transaction-rules/test-transaction-rule
|
||||
:run-transaction-rule gq-transaction-rules/run-transaction-rule
|
||||
:mutation/upsert-vendor gq-vendors/upsert-vendor
|
||||
:mutation/upsert-account gq-accounts/upsert-account
|
||||
:mutation/merge-vendors gq-vendors/merge-vendors
|
||||
:mutation/import-ledger gq-ledger/import-ledger
|
||||
:mutation/request-import gq-requests/request-import
|
||||
:get-vendor gq-vendors/get-graphql})
|
||||
gq-checks/attach
|
||||
gq-ledger/attach
|
||||
gq-plaid/attach
|
||||
gq-import-batches/attach
|
||||
gq-transactions/attach
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
(ns auto-ap.graphql.ledger
|
||||
(:require [auto-ap.datomic :refer [audit-transact-batch remove-nils uri conn]]
|
||||
[auto-ap.datomic.accounts :as a]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.datomic.ledger :as l]
|
||||
[auto-ap.datomic.vendors :as d-vendors]
|
||||
[auto-ap.graphql.utils
|
||||
:refer
|
||||
[->graphql <-graphql assert-admin assert-can-see-client result->page]]
|
||||
[auto-ap.parse.util :as parse]
|
||||
[auto-ap.utils :refer [by dollars=]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clojure.tools.logging :as log]
|
||||
[datomic.api :as d]
|
||||
[unilog.context :as lc]
|
||||
[mount.core :as mount]
|
||||
[yang.scheduler :as scheduler]))
|
||||
(:require
|
||||
[auto-ap.datomic :refer [audit-transact-batch conn remove-nils uri]]
|
||||
[auto-ap.datomic.accounts :as a]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.datomic.ledger :as l]
|
||||
[auto-ap.datomic.vendors :as d-vendors]
|
||||
[auto-ap.graphql.utils
|
||||
:refer [->graphql <-graphql assert-admin assert-can-see-client result->page]]
|
||||
[auto-ap.parse.util :as parse]
|
||||
[auto-ap.utils :refer [by dollars=]]
|
||||
[auto-ap.pdf.ledger :refer [print-pnl]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clojure.tools.logging :as log]
|
||||
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
|
||||
[datomic.api :as d]
|
||||
[mount.core :as mount]
|
||||
[unilog.context :as lc]
|
||||
[yang.scheduler :as scheduler]))
|
||||
|
||||
(mount/defstate running-balance-cache
|
||||
:start (atom {}))
|
||||
|
||||
@@ -205,6 +208,34 @@
|
||||
#(roll-up-until (lookup-account %) (all-ledger-entries %) (coerce/to-date end) (coerce/to-date start) )
|
||||
client-ids)})))})))
|
||||
|
||||
(defn profit-and-loss-pdf [context args value]
|
||||
(let [data (get-profit-and-loss context args value)
|
||||
result (print-pnl args data)]
|
||||
|
||||
(->graphql {:report_url result}))
|
||||
#_(let [client-id (:client_id args)
|
||||
client-ids (or (some-> client-id vector)
|
||||
(filter identity (:client_ids args)))
|
||||
_ (when (not (seq client-ids))
|
||||
(throw (ex-info "Please select a client." {:validation-error "Please select a client."})))
|
||||
_ (doseq [client-id client-ids]
|
||||
(assert-can-see-client (:id context) client-id))
|
||||
all-ledger-entries (->> client-ids
|
||||
(map (fn [client-id]
|
||||
[client-id (full-ledger-for-client client-id)]))
|
||||
(into {}))
|
||||
lookup-account (->> client-ids
|
||||
(map (fn [client-id]
|
||||
[client-id (build-account-lookup client-id)]))
|
||||
(into {}))]
|
||||
(->graphql
|
||||
{:periods
|
||||
(->> (:periods args)
|
||||
(mapv (fn [{:keys [start end]}]
|
||||
{:accounts (mapcat
|
||||
#(roll-up-until (lookup-account %) (all-ledger-entries %) (coerce/to-date end) (coerce/to-date start) )
|
||||
client-ids)})))})))
|
||||
|
||||
|
||||
(defn assoc-error [f]
|
||||
(fn [entry]
|
||||
@@ -490,3 +521,169 @@
|
||||
:start (scheduler/every (* 15 60 1000) refresh-running-balance-cache)
|
||||
:stop (scheduler/stop running-balance-cache-worker))
|
||||
|
||||
|
||||
|
||||
(def objects
|
||||
{:balance_sheet_account
|
||||
{:fields {:id {:type 'String}
|
||||
:amount {:type 'String}
|
||||
:location {:type 'String}
|
||||
:client_id {:type :id}
|
||||
:count {:type 'Int}
|
||||
:numeric_code {:type 'Int}
|
||||
:account_type {:type :account_type}
|
||||
:name {:type 'String}}}
|
||||
|
||||
:profit_and_loss_pdf
|
||||
{:fields {:report_url {:type 'String}}}
|
||||
|
||||
:balance_sheet
|
||||
{:fields {:balance_sheet_accounts {:type '(list :balance_sheet_account)}
|
||||
:comparable_balance_sheet_accounts {:type '(list :balance_sheet_account)}}}
|
||||
|
||||
:profit_and_loss_report_period
|
||||
{:fields {:accounts {:type '(list :balance_sheet_account)}}}
|
||||
|
||||
:profit_and_loss_report
|
||||
{:fields {:periods {:type '(list :profit_and_loss_report_period)}}}
|
||||
|
||||
:journal_entry_line
|
||||
{:fields {:id {:type :id}
|
||||
:account {:type :account}
|
||||
:location {:type 'String}
|
||||
:debit {:type 'String}
|
||||
:credit {:type 'String}
|
||||
:running_balance {:type :money}}}
|
||||
|
||||
:journal_entry
|
||||
{:fields {:id {:type :id}
|
||||
:source {:type 'String}
|
||||
:external_id {:type 'String}
|
||||
:original_entity {:type :id}
|
||||
:amount {:type 'String}
|
||||
:note {:type 'String}
|
||||
:cleared_against {:type 'String}
|
||||
:client {:type :client}
|
||||
:vendor {:type :vendor}
|
||||
:alternate_description {:type 'String}
|
||||
:date {:type 'String}
|
||||
:line_items {:type '(list :journal_entry_line)}}}
|
||||
|
||||
:ledger_page
|
||||
{:fields {:journal_entries {:type '(list :journal_entry)}
|
||||
:count {:type 'Int}
|
||||
:total {:type 'Int}
|
||||
:start {:type 'Int}
|
||||
:end {:type 'Int}}}
|
||||
|
||||
:import_ledger_entry_result
|
||||
{:fields {:external_id {:type 'String}
|
||||
:error {:type 'String}
|
||||
:status {:type 'String}}}
|
||||
|
||||
:import_ledger_result
|
||||
{: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)}}}})
|
||||
|
||||
(def queries
|
||||
{:balance_sheet {:type :balance_sheet
|
||||
:args {:client_id {:type :id}
|
||||
:include_comparison {:type 'Boolean}
|
||||
:date {:type :iso_date}
|
||||
:comparison_date {:type :iso_date}}
|
||||
:resolve :get-balance-sheet}
|
||||
|
||||
:profit_and_loss {:type :profit_and_loss_report
|
||||
:args {:client_id {:type :id}
|
||||
:client_ids {:type '(list :id)}
|
||||
:periods {:type '(list :date_range)}}
|
||||
:resolve :get-profit-and-loss}
|
||||
|
||||
:profit_and_loss_pdf {:type :profit_and_loss_pdf
|
||||
:args {:client_id {:type :id}
|
||||
:client_ids {:type '(list :id)}
|
||||
:periods {:type '(list :date_range)}}
|
||||
:resolve :profit-and-loss-pdf}
|
||||
|
||||
:ledger_page {:type :ledger_page
|
||||
:args {:filters {:type :ledger_filters}}
|
||||
:resolve :get-ledger-page}})
|
||||
|
||||
(def mutations
|
||||
{:import_ledger
|
||||
{:type :import_ledger_result
|
||||
:args {:entries {:type '(list :import_ledger_entry)}}
|
||||
:resolve :mutation/import-ledger}
|
||||
|
||||
:delete_external_ledger
|
||||
{:type :message
|
||||
:args {:filters {:type :ledger_filters}
|
||||
:ids {:type '(list :id)}}
|
||||
:resolve :mutation/delete-external-ledger}})
|
||||
|
||||
(def input-objects
|
||||
{:ledger_filters
|
||||
{:fields {:client_id {:type :id}
|
||||
:vendor_id {:type :id}
|
||||
:account_id {:type :id}
|
||||
:amount_lte {:type :money}
|
||||
:amount_gte {:type :money}
|
||||
:bank_account_id {:type :id}
|
||||
:date_range {:type :date_range}
|
||||
:location {:type 'String}
|
||||
:from_numeric_code {:type 'Int}
|
||||
:to_numeric_code {:type 'Int}
|
||||
:start {:type 'Int}
|
||||
:per_page {:type 'Int}
|
||||
:only_external {:type 'Boolean}
|
||||
:external_id_like {:type 'String}
|
||||
:source {:type 'String}
|
||||
:sort {:type '(list :sort_item)}}}
|
||||
|
||||
:import_ledger_line_item
|
||||
{:fields {:account_identifier {:type 'String}
|
||||
:location {:type 'String}
|
||||
:debit {:type :money}
|
||||
:credit {:type :money}}}
|
||||
:import_ledger_entry
|
||||
{:fields {:source {:type 'String}
|
||||
:external_id {:type 'String}
|
||||
:client_code {:type 'String}
|
||||
:date {:type 'String}
|
||||
:vendor_name {:type 'String}
|
||||
:amount {:type :money}
|
||||
:note {:type 'String}
|
||||
:cleared_against {:type 'String}
|
||||
:line_items {:type '(list :import_ledger_line_item)}}}
|
||||
|
||||
})
|
||||
|
||||
(def enums
|
||||
{:payment_type {:values [{:enum-value :check}
|
||||
{:enum-value :cash}
|
||||
{:enum-value :debit}
|
||||
{:enum-value :credit}]}
|
||||
:payment_status {:values [{:enum-value :voided}
|
||||
{:enum-value :pending}
|
||||
{:enum-value :cleared}]}})
|
||||
|
||||
|
||||
(def resolvers
|
||||
{:get-ledger-page get-ledger-page
|
||||
:get-balance-sheet get-balance-sheet
|
||||
:get-profit-and-loss get-profit-and-loss
|
||||
:profit-and-loss-pdf profit-and-loss-pdf
|
||||
:mutation/delete-external-ledger delete-external-ledger
|
||||
:mutation/import-ledger import-ledger})
|
||||
|
||||
(defn attach [schema]
|
||||
(->
|
||||
(merge-with merge schema
|
||||
{:objects objects
|
||||
:queries queries
|
||||
:mutations mutations
|
||||
:input-objects input-objects
|
||||
:enums enums})
|
||||
(attach-resolvers resolvers)))
|
||||
|
||||
456
src/clj/auto_ap/pdf/ledger.clj
Normal file
456
src/clj/auto_ap/pdf/ledger.clj
Normal file
@@ -0,0 +1,456 @@
|
||||
(ns auto-ap.pdf.ledger
|
||||
(:require
|
||||
[amazonica.aws.s3 :as s3]
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.graphql.utils :refer [<-graphql]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [dollars-0?]]
|
||||
[clj-pdf.core :as pdf]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[clojure.walk :refer [postwalk]]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as d])
|
||||
(:import
|
||||
(java.io ByteArrayOutputStream)
|
||||
(java.util UUID)))
|
||||
|
||||
(defn distribute [nums]
|
||||
(let [sum (reduce + 0 nums)]
|
||||
(map #(* 100 (/ % sum)) nums)))
|
||||
|
||||
(defn date->str [d]
|
||||
(atime/unparse-local d atime/normal-date))
|
||||
|
||||
(def ranges
|
||||
{:sales [40000 49999]
|
||||
:cogs [50000 59999]
|
||||
:payroll [60000 69999]
|
||||
:controllable [70000 79999]
|
||||
:fixed-overhead [80000 89999]
|
||||
:ownership-controllable [90000 99999]})
|
||||
|
||||
(def groupings
|
||||
{:sales [["40000-43999 Food Sales " 40000 43999]
|
||||
["44000-46999 Alcohol Sales" 44000 46999]
|
||||
["47000 Merchandise Sales" 47000 47999]
|
||||
["48000 Other Operating Income" 48000 48999]
|
||||
["49000 Non-Business Income" 49000 49999]]
|
||||
:cogs [
|
||||
["50000-54000 Food Costs" 50000 53999]
|
||||
["54000-56000 Alcohol Costs" 54000 55999]
|
||||
["56000 Merchandise Costs" 56000 56999]
|
||||
["57000-60000 Other Costs of Sales" 57000 59999]]
|
||||
:payroll [["60000 Payroll - General" 60000 60999]
|
||||
["61000 Payroll - Management" 61000 61999]
|
||||
["62000 Payroll - BOH" 62000 62999]
|
||||
["63000-66000 Payroll - FOH" 63000 65999]
|
||||
["66000-70000 Payroll - Other" 66000 69999]]
|
||||
|
||||
:controllable [["70000 72000 GM Controllable Costs - Ops Related" 70000 71999]
|
||||
["72000 GM Controllable Costs - Customer Related" 72000 72999]
|
||||
["73000 GM Controllable Costs - Employee Related" 73000 73999]
|
||||
["74000 GM Controllable Costs - Building & Equipment Related" 74000 74999]
|
||||
["75000 GM Controllable Costs - Office & Management Related" 75000 75999]
|
||||
["76000-80000 GM Controllable Costs - Other" 76000 79999]]
|
||||
|
||||
:fixed-overhead [["80000-82000 Operational Costs" 80000 81999]
|
||||
["82000 Occupancy Costs" 82000 82999]
|
||||
["83000 Utility Costs" 83000 83999]
|
||||
["84000 Equipment Rental" 84000 84999]
|
||||
["85000-87000 Taxes & Insurance" 85000 86999]
|
||||
["87000-90000 Other Non-Controllable Costs" 87000 89999]]
|
||||
:ownership-controllable [["90000-93000 Research & Entertainment" 90000 92999]
|
||||
["93000 Bank Charges & Interest" 93000 93999]
|
||||
["94000-96000 Other Owner Controllable Costs" 94000 95999]
|
||||
["96000 Depreciation" 96000 96999]
|
||||
["97000 Taxes" 97000 97999]
|
||||
["98000 Other Expenses" 98000 98999]]})
|
||||
|
||||
(defn in-range? [code]
|
||||
(reduce
|
||||
(fn [acc [start end]]
|
||||
(if (<= start code end)
|
||||
(reduced true)
|
||||
acc))
|
||||
false
|
||||
(vals ranges)))
|
||||
|
||||
(defn locations [data]
|
||||
(->> data
|
||||
:periods
|
||||
(mapcat :accounts)
|
||||
(filter (comp in-range? :numeric-code))
|
||||
(group-by (juxt :client-id :location))
|
||||
(filter (fn [[k as]]
|
||||
(not (dollars-0? (reduce + 0 (map :amount as))))))
|
||||
(mapcat second)
|
||||
(map (fn [a]
|
||||
(if (or (not (:client-id a))
|
||||
(empty? (:location a)))
|
||||
nil
|
||||
[(:client-id a)
|
||||
(:location a)])))
|
||||
(filter identity)
|
||||
(set)
|
||||
|
||||
(sort-by (fn [x]
|
||||
[(:client-id x)
|
||||
(if (= (:location x) "HQ" )
|
||||
"ZZZZZZ"
|
||||
(:location x))]))))
|
||||
|
||||
(defn expand [data]
|
||||
(postwalk (fn [x]
|
||||
(cond
|
||||
(map-entry? x)
|
||||
x
|
||||
|
||||
(sequential? x)
|
||||
(vec (mapcat (fn [r]
|
||||
(cond (and (sequential? r)
|
||||
(= :<> (first r)))
|
||||
(filter identity (rest r))
|
||||
|
||||
:else
|
||||
[r]))
|
||||
x))
|
||||
|
||||
:else
|
||||
x
|
||||
))
|
||||
data))
|
||||
|
||||
(defn map-periods [for-every between periods include-deltas]
|
||||
(into [:<>]
|
||||
(for [[_ i] (map vector periods (range))]
|
||||
[:<> (for-every i)
|
||||
(if (and include-deltas (not= 0 i))
|
||||
(between i))])))
|
||||
|
||||
(defn period-header [{:keys [include_deltas periods]}]
|
||||
[
|
||||
[[:cell "Period"]
|
||||
[:<>
|
||||
(map-periods
|
||||
(fn [i]
|
||||
[:cell {:colspan 2}
|
||||
(str (date->str (get-in periods [i :start])) " - " (date->str (get-in periods [i :end])))])
|
||||
(fn [i]
|
||||
[:cell ""])
|
||||
periods
|
||||
include_deltas)]]
|
||||
[[:cell ""]
|
||||
[:<> (map-periods
|
||||
(fn [i]
|
||||
[:<>
|
||||
[:cell
|
||||
"Amount"]
|
||||
[:cell
|
||||
"% Sales"]])
|
||||
(fn [i]
|
||||
[:cell "𝝙"])
|
||||
periods
|
||||
include_deltas)]
|
||||
]])
|
||||
|
||||
(defn all-accounts [data]
|
||||
(transduce
|
||||
(comp
|
||||
(map #(->> (:accounts %)
|
||||
(group-by (juxt :numeric-code :client-id :location))
|
||||
(map (fn [[k v]]
|
||||
[k
|
||||
(reduce (fn [a n]
|
||||
(-> a
|
||||
(update :count (fn [z] (+ z (:count n))))
|
||||
(update :amount (fn [z] (+ z (:amount n))))))
|
||||
(first v)
|
||||
(rest v))]))
|
||||
|
||||
(into {}))))
|
||||
|
||||
conj
|
||||
[]
|
||||
(:periods data)))
|
||||
|
||||
|
||||
(defn filter-accounts [accounts period [from to] only-client only-location]
|
||||
(->> (get accounts period)
|
||||
vals
|
||||
|
||||
(filter (fn [{:keys [location client-id numeric-code]}]
|
||||
(and (or (nil? only-location)
|
||||
(= only-location location))
|
||||
(or (nil? only-client)
|
||||
(= only-client client-id))
|
||||
(<= from numeric-code to))))
|
||||
(sort-by :numeric-code)))
|
||||
|
||||
(defn aggregate-accounts [accounts]
|
||||
(reduce (fnil + 0.0) 0.0 (map :amount accounts)))
|
||||
|
||||
(defn used-accounts [accounts [from to] client-id location]
|
||||
(->> accounts
|
||||
(mapcat vals)
|
||||
(filter #(<= from (:numeric-code %) to))
|
||||
(filter #(= client-id (:client-id %)))
|
||||
(filter #(= location (:location %)))
|
||||
(map #(select-keys % [:numeric-code :name]))
|
||||
(set)
|
||||
(sort-by :numeric-code)))
|
||||
|
||||
(defn subtotal-row [args data types negs title client-id location]
|
||||
(let [all-accounts (all-accounts data)
|
||||
raw (map-indexed
|
||||
(fn [i p]
|
||||
(aggregate-accounts (mapcat (fn [t]
|
||||
(cond->> (filter-accounts all-accounts i (ranges t) client-id location)
|
||||
(negs t) (map #(update % :amount -))))
|
||||
types))
|
||||
)
|
||||
|
||||
(:periods args))
|
||||
sales (map-indexed
|
||||
(fn [i _]
|
||||
(aggregate-accounts (filter-accounts all-accounts i (ranges :sales) client-id location)))
|
||||
|
||||
(:periods args))
|
||||
deltas (->> raw
|
||||
(partition-all 2)
|
||||
(map (fn [[a b]]
|
||||
(- b
|
||||
a))))]
|
||||
(into [title]
|
||||
(->> raw
|
||||
(map (fn [s r]
|
||||
[r (if (dollars-0? s)
|
||||
0.0
|
||||
(/ r s))])
|
||||
sales)
|
||||
(partition-all 2)
|
||||
(mapcat (fn [d [[a a-sales] [b b-sales]]]
|
||||
[a a-sales b b-sales d]
|
||||
)
|
||||
deltas)))))
|
||||
|
||||
(defn location-summary-table [args data client-id location]
|
||||
[(subtotal-row args data [:sales] #{} "Sales" client-id location)
|
||||
(subtotal-row args data [:cogs ] #{} "Cogs" client-id location)
|
||||
(subtotal-row args data [:payroll ]#{} "Payroll" client-id location)
|
||||
(subtotal-row args data [:sales :payroll :cogs] #{:payroll :cogs} "Gross Profits" client-id location)
|
||||
(subtotal-row args data [:controllable :fixed-overhead :ownership-controllable] #{} "Overhead" client-id location)
|
||||
(subtotal-row args data [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" client-id location)])
|
||||
|
||||
|
||||
(defn detail-sub-rows [args data grouping client-id location]
|
||||
(let [all-accounts (all-accounts data)]
|
||||
(for [[grouping-name from to] grouping
|
||||
:let [account-codes (used-accounts all-accounts [from to] client-id location)]
|
||||
:when (seq account-codes)]
|
||||
(->
|
||||
[[(str "---" grouping-name "---")]]
|
||||
(into (for [{:keys [numeric-code name]} account-codes]
|
||||
(let [raw (map-indexed
|
||||
(fn [i p]
|
||||
(get-in all-accounts [i [numeric-code client-id location] :amount] 0.0))
|
||||
|
||||
(:periods args))
|
||||
sales (map-indexed
|
||||
(fn [i _]
|
||||
(aggregate-accounts (filter-accounts all-accounts i (ranges :sales) client-id location)))
|
||||
|
||||
(:periods args))
|
||||
deltas (->> raw
|
||||
(partition-all 2)
|
||||
(map (fn [[a b]]
|
||||
(- b
|
||||
a))))]
|
||||
(into [name]
|
||||
(->> raw
|
||||
(map (fn [s r]
|
||||
[r (if (dollars-0? s)
|
||||
0.0
|
||||
(/ r s))])
|
||||
sales)
|
||||
(partition-all 2)
|
||||
(mapcat (fn [d [[a a-sales] [b b-sales]]]
|
||||
[a a-sales b b-sales d]
|
||||
)
|
||||
deltas))))
|
||||
|
||||
#_[:tr
|
||||
[:td name]
|
||||
#_(map-periods
|
||||
(fn [i]
|
||||
(let [amount (get-in all-accounts [i [numeric-code client-id location] :amount] 0.0)]
|
||||
[:<>
|
||||
[:td.has-text-right (if multi-client?
|
||||
[:span (->$ amount)]
|
||||
[:a {:on-click (dispatch-event [::investigate-clicked location numeric-code numeric-code i :current])
|
||||
:disabled (boolean multi-client?)}
|
||||
(->$ amount)])]
|
||||
[:td.has-text-right (->% (percent-of-sales amount all-accounts i client-id location))]]))
|
||||
(fn [i]
|
||||
[:td.has-text-right (->$ (- (get-in all-accounts [i [numeric-code client-id location] :amount] 0.0)
|
||||
(get-in all-accounts [(dec i) [numeric-code client-id location] :amount] 0.0)))])
|
||||
periods
|
||||
include-deltas)])))
|
||||
)))
|
||||
|
||||
(defn detail-rows [args data type title client-id location]
|
||||
(-> [[title]]
|
||||
(into (detail-sub-rows args data (type groupings) client-id location))
|
||||
(conj (subtotal-row args data [type] #{} title client-id location))))
|
||||
|
||||
(defn location-detail-table [args data client-id location]
|
||||
(-> []
|
||||
(into (detail-rows args data :sales (str location " Sales") client-id location))))
|
||||
|
||||
(defn summarize-pnl [args data]
|
||||
{:summaries (for [[client-id location] (locations data)]
|
||||
(location-summary-table args data client-id location))
|
||||
:details (for [[client-id location] (locations data)]
|
||||
(location-detail-table args data client-id location))}
|
||||
)
|
||||
|
||||
(defn make-pnl [args data]
|
||||
|
||||
(let [data (<-graphql data)
|
||||
args (<-graphql args)
|
||||
_ (clojure.pprint/pprint (summarize-pnl (assoc args :deltas true) data))output-stream (ByteArrayOutputStream.)
|
||||
_ (println (:client_ids args))
|
||||
clients (d/pull-many (d/db conn) '[:client/name] (:client-ids args))]
|
||||
(pdf/pdf
|
||||
(expand
|
||||
[{:left-margin 25 :right-margin 0 :top-margin 0 :bottom-margin 0 :size :letter}
|
||||
[:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))]
|
||||
#_(for [[client-id location] (locations data)]
|
||||
^{:key (str client-id "-" location "-summary")}
|
||||
(location-summary args data client-id location (:include-deltas data))
|
||||
)
|
||||
#_(let [{:keys [bank-account paid-to client check date amount memo] {print-as :vendor/print-as vendor-name :vendor/name :as vendor} :vendor} check
|
||||
df (DecimalFormat. "#,###.00")]
|
||||
[:table {:num-cols 6 :border false :leading 11 :widths (distribute [2 2 2 2 2 2])}
|
||||
[(let [{:keys [:client/name] {:keys [:address/street1 :address/city :address/state :address/zip]} :client/address} client]
|
||||
[:cell {:colspan 4 } [:paragraph {:leading 14} name "\n" street1 "\n" (str city ", " state " " zip)] ])
|
||||
(let [{:keys [:bank-account/bank-name :bank-account/bank-code] } bank-account]
|
||||
[:cell {:colspan 6 :align :center} [:paragraph {:style :bold} bank-name] [:paragraph {:size 8 :leading 8} bank-code]])
|
||||
[:cell {:colspan 2 :size 13}
|
||||
check]]
|
||||
|
||||
[[:cell {:colspan 9}]
|
||||
[:cell {:colspan 3 :leading -10} date]]
|
||||
[[:cell {:colspan 12 :size 14}]
|
||||
]
|
||||
|
||||
[[:cell {:size 13 :leading 13} "PAY"]
|
||||
[:cell {:size 8 :leading 8 } "TO THE ORDER OF"]
|
||||
[:cell {:colspan 7} (if (seq print-as)
|
||||
print-as
|
||||
vendor-name)]
|
||||
[:cell {:colspan 3} amount]]
|
||||
|
||||
[[:cell {}]
|
||||
[:cell {:colspan 8} (str " -- " word-amount " " (str/join "" (take (max
|
||||
2
|
||||
(- 95
|
||||
(count word-amount)))
|
||||
(repeat "-"))))
|
||||
[:line {:line-width 0.15 :color [50 50 50]}]]
|
||||
[:cell {:colspan 3}]]
|
||||
|
||||
|
||||
[[:cell {:size 9 :leading 11.5} "\n\n\n\n\nMEMO"]
|
||||
[:cell {:colspan 5 :leading 11.5} (split-memo memo)
|
||||
[:line {:line-width 0.15 :color [50 50 50]}]]
|
||||
[:cell {:colspan 6 } (if (:client/signature-file client)
|
||||
[:image { :top-margin 90 :xscale 0.30 :yscale 0.30 :align :center}
|
||||
|
||||
(:client/signature-file client)]
|
||||
[:spacer])]]
|
||||
|
||||
#_[
|
||||
#_[:cell {:colspan 5} #_memo ]
|
||||
#_[:cell {:colspan 6}]]
|
||||
|
||||
[[:cell {:colspan 2}]
|
||||
[:cell {:colspan 10 :leading 30}
|
||||
[:phrase {:size 18 :ttf-name "public/micrenc.ttf"} (str "c" check "c a" (:bank-account/routing bank-account) "a " (:bank-account/number bank-account) "c")]]]
|
||||
[[:cell {:colspan 12 :leading 18} [:spacer]]]
|
||||
[[:cell]
|
||||
(into
|
||||
[:cell {:colspan 9}]
|
||||
(let [{:keys [:client/name]
|
||||
{:keys [:address/street1 :address/street2 :address/city :address/state :address/zip ]} :client/address} client]
|
||||
(filter identity
|
||||
(list
|
||||
[:paragraph " " name]
|
||||
[:paragraph " " street1]
|
||||
(when (not (str/blank? street2))
|
||||
[:paragraph " " street2])
|
||||
[:paragraph " " city ", " state " " zip]))))
|
||||
[:cell {:colspan 2 :size 13}
|
||||
check]]
|
||||
|
||||
[[:cell {:colspan 12 :leading 74} [:spacer]]]
|
||||
|
||||
[[:cell]
|
||||
[:cell {:colspan 5} [:paragraph
|
||||
" " vendor-name "\n"
|
||||
" " (:address/street1 (:vendor/address vendor)) "\n"
|
||||
(when (not (str/blank? (:address/street2 (:vendor/address vendor))))
|
||||
(str " " (:address/street2 (:vendor/address vendor)) "\n")
|
||||
)
|
||||
" " (:address/city (:vendor/address vendor)) ", " (:address/state (:vendor/address vendor)) " " (:address/zip (:vendor/address vendor))]]
|
||||
[:cell {:align :right}
|
||||
"Paid to:\n"
|
||||
"Amount:\n"
|
||||
"Date:\n"]
|
||||
|
||||
[:cell {:colspan 5}
|
||||
[:paragraph paid-to]
|
||||
[:paragraph amount]
|
||||
[:paragraph date]]]
|
||||
|
||||
[[:cell {:colspan 3} "Memo:"]
|
||||
[:cell {:colspan 9} memo]]
|
||||
|
||||
[[:cell {:colspan 12} [:spacer]]]
|
||||
[[:cell {:colspan 12} [:spacer]]]
|
||||
[[:cell {:colspan 12} [:spacer]]]
|
||||
[[:cell {:colspan 12} [:spacer]]]
|
||||
|
||||
[[:cell {:colspan 5}]
|
||||
[:cell {:align :right :colspan 2}
|
||||
"Check:\n"
|
||||
"Vendor:\n"
|
||||
"Company:\n"
|
||||
"Bank Account:\n"
|
||||
"Paid To:\n"
|
||||
"Amount:\n"
|
||||
"Date:\n"]
|
||||
|
||||
[:cell {:colspan 5}
|
||||
[:paragraph check]
|
||||
[:paragraph vendor-name]
|
||||
[:paragraph (:client/name client)]
|
||||
[:paragraph (:bank-account/bank-name bank-account)]
|
||||
[:paragraph paid-to]
|
||||
[:paragraph amount]
|
||||
[:paragraph date]]]
|
||||
[[:cell {:colspan 3} "Memo:"]
|
||||
[:cell {:colspan 9} memo]]
|
||||
])])
|
||||
output-stream)
|
||||
(.toByteArray output-stream)))
|
||||
|
||||
(defn print-pnl [args data]
|
||||
(let [uuid (str (UUID/randomUUID))
|
||||
pdf-data (make-pnl args data)]
|
||||
(s3/put-object :bucket-name (:data-bucket env)
|
||||
:key (str "reports/pnl/" uuid ".pdf")
|
||||
:input-stream (io/make-input-stream pdf-data {})
|
||||
:metadata {:content-length (count pdf-data)
|
||||
:content-type "application/pdf"})
|
||||
(str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/reports/pnl/" uuid ".pdf")))
|
||||
@@ -212,8 +212,31 @@
|
||||
:clients (mapv #(select-keys % [:name :id]) (:clients (:data db))) }
|
||||
:db (dissoc db :report)})))
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::received-pdf
|
||||
(fn [_ [_ result]]
|
||||
(println result)
|
||||
{:dispatch [::modal/modal-requested {:title "Your report is ready"
|
||||
:body [:div
|
||||
[:div "Click " [:a {:href (-> result :profit-and-loss-pdf :report-url) :target "_new"} "here"] " to view it."]]}]}))
|
||||
(re-frame/reg-event-fx
|
||||
::export-pdf
|
||||
[with-user (forms/in-form ::form)]
|
||||
(fn [{:keys [db user] :as cofx}]
|
||||
(cond-> {:graphql {:token user
|
||||
:owns-state {:single ::page}
|
||||
:query-obj {:venia/queries [[:profit-and-loss-pdf
|
||||
{:client-ids (map :id (:clients (:data db)))
|
||||
:periods (mapv (fn [[start end] ] {:start (date->str start standard) :end (date->str end standard)} )
|
||||
(:periods (:data db)))}
|
||||
[:report_url]]]}
|
||||
:on-success [::received-pdf]}
|
||||
:set-uri-params {:periods (mapv (fn [[start end title]]
|
||||
[(date->str start standard)
|
||||
(date->str end standard)])
|
||||
(:periods (:data db)))
|
||||
:clients (mapv #(select-keys % [:name :id]) (:clients (:data db))) }
|
||||
:db (dissoc db :report)})))
|
||||
|
||||
|
||||
(re-frame/reg-event-db
|
||||
@@ -876,7 +899,11 @@
|
||||
:label "Include deltas"
|
||||
:type "checkbox"}]]]]
|
||||
[:div.level-right
|
||||
[:button.button.is-primary "Run"]]]
|
||||
[:div.buttons
|
||||
[: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
|
||||
(reset! !box el)))}]]))))
|
||||
|
||||
Reference in New Issue
Block a user