209 lines
11 KiB
Clojure
209 lines
11 KiB
Clojure
(ns auto-ap.graphql.invoices
|
|
(:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin enum->keyword]]
|
|
|
|
[auto-ap.datomic.vendors :as d-vendors]
|
|
[auto-ap.datomic.clients :as d-clients]
|
|
[auto-ap.datomic.invoices :as d-invoices]
|
|
[auto-ap.datomic.accounts :as d-accounts]
|
|
[auto-ap.graphql.checks :as gq-checks]
|
|
[auto-ap.time :refer [parse iso-date]]
|
|
[auto-ap.utils :refer [dollars=]]
|
|
[datomic.api :as d]
|
|
[auto-ap.datomic :refer [uri remove-nils]]
|
|
[clj-time.coerce :as coerce]
|
|
[clj-time.core :as time]
|
|
[clojure.set :as set]))
|
|
|
|
(defn get-invoice-page [context args value]
|
|
|
|
(let [args (assoc args :id (:id context))
|
|
[invoices invoice-count outstanding] (d-invoices/get-graphql (update (<-graphql (assoc args :id (:id context))) :status enum->keyword "invoice-status"))]
|
|
[{:invoices (map ->graphql invoices)
|
|
:outstanding outstanding
|
|
:total invoice-count
|
|
:count (count invoices)
|
|
:start (:start args 0)
|
|
:end (+ (:start args 0) (count invoices))}]))
|
|
|
|
(defn get-all-invoices [context args value]
|
|
(assert-admin (:id context))
|
|
(map
|
|
->graphql
|
|
(first (d-invoices/get-graphql (assoc (<-graphql args)
|
|
:count Integer/MAX_VALUE)))))
|
|
|
|
(defn reject-invoices [context {:keys [invoices] :as in} value]
|
|
(assert-admin (:id context))
|
|
|
|
(let [transactions (map (fn [i] [:db/retractEntity i ]) invoices)
|
|
transaction-result @(d/transact (d/connect uri) transactions)]
|
|
invoices))
|
|
|
|
(defn approve-invoices [context {:keys [invoices] :as in} value]
|
|
(assert-admin (:id context))
|
|
(let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices)
|
|
transaction-result @(d/transact (d/connect uri) transactions)]
|
|
invoices))
|
|
|
|
(defn assert-no-conflicting [{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in}]
|
|
(when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice_number
|
|
:invoice/vendor vendor_id
|
|
:invoice/client client_id}))
|
|
(throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number :validation-error (str "Invoice '" invoice_number "' already exists.")}))))
|
|
|
|
(defn expense-account->entity [{:keys [id account_id amount location]}]
|
|
(remove-nils #:invoice-expense-account {:amount (Double/parseDouble amount)
|
|
:db/id id
|
|
:account account_id
|
|
:location location}))
|
|
|
|
(defn add-invoice-transaction [{:keys [total invoice_number location automatically_paid_when_due client_id vendor_id vendor_name date due expense_accounts] :as in}]
|
|
(let [vendor (d-vendors/get-by-id vendor_id)
|
|
account (:vendor/default-account vendor)
|
|
_ (when-not (:db/id account)
|
|
(throw (ex-info (str "Vendor '" (:vendor/name vendor) "' does not have a default expense acount.") {:vendor-id vendor_id} )))]
|
|
(cond->
|
|
{:db/id "invoice"
|
|
:invoice/invoice-number invoice_number
|
|
:invoice/client client_id
|
|
:invoice/vendor vendor_id
|
|
:invoice/import-status :import-status/imported
|
|
:invoice/total total
|
|
:invoice/outstanding-balance total
|
|
:invoice/status :invoice-status/unpaid
|
|
:invoice/date (coerce/to-date date)
|
|
:invoice/expense-accounts (map expense-account->entity
|
|
expense_accounts)}
|
|
(:vendor/terms vendor) (assoc :invoice/due (coerce/to-date
|
|
(time/plus date (time/days (d-vendors/terms-for-client-id vendor client_id)))))
|
|
due (assoc :invoice/due (coerce/to-date due))
|
|
(boolean? automatically_paid_when_due) (assoc :invoice/automatically-paid-when-due automatically_paid_when_due))))
|
|
|
|
|
|
(defn deleted-expense-accounts [invoice expense-accounts]
|
|
(let [current-expense-accounts (:invoice/expense-accounts invoice)
|
|
specified-ids (->> expense-accounts
|
|
(map :id)
|
|
set)
|
|
existing-ids (->> current-expense-accounts
|
|
(map :db/id)
|
|
set)]
|
|
(set/difference existing-ids specified-ids)))
|
|
|
|
|
|
|
|
(defn add-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value]
|
|
(assert-no-conflicting in)
|
|
(assert-can-see-client (:id context) client_id)
|
|
(let [transaction-result @(d/transact (d/connect uri) [(add-invoice-transaction in)])]
|
|
(-> (d-invoices/get-by-id (get-in transaction-result [:tempids "invoice"]))
|
|
(->graphql))))
|
|
|
|
(defn assert-bank-account-belongs [client-id bank-account-id]
|
|
(when-not ((set (map :db/id (:client/bank-accounts (d-clients/get-by-id client-id)))) bank-account-id)
|
|
(throw (ex-info (str "Bank account does not belong to client") {:validation-error "Bank account does not belong to client."} ))))
|
|
|
|
(defn add-and-print-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice bank-account-id :bank_account_id type :type} value]
|
|
(assert-no-conflicting in)
|
|
(assert-can-see-client (:id context) client_id)
|
|
(assert-bank-account-belongs client_id bank-account-id)
|
|
(let [transaction-result @(d/transact (d/connect uri) [(add-invoice-transaction in)])]
|
|
(-> (gq-checks/print-checks [{:invoice-id (get-in transaction-result [:tempids "invoice"])
|
|
:amount total}]
|
|
client_id
|
|
bank-account-id
|
|
type)
|
|
->graphql)))
|
|
|
|
|
|
(defn edit-invoice [context {{:keys [id due invoice_number total vendor_id date client_id expense_accounts automatically_paid_when_due] :as in} :invoice} value]
|
|
(let [invoice (d-invoices/get-by-id id)
|
|
_ (when (seq (d-invoices/find-conflicting {:db/id id
|
|
:invoice/invoice-number invoice_number
|
|
:invoice/vendor (:db/id (:invoice/vendor invoice))
|
|
:invoice/client (:db/id (:invoice/client invoice))}))
|
|
(throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number})))
|
|
|
|
expense-account-total (reduce + 0 (map (fn [x] (Double/parseDouble (:amount x))) expense_accounts))
|
|
_ (when-not (dollars= total expense-account-total)
|
|
(let [error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")")]
|
|
(throw (ex-info error {:validation-error error}))))
|
|
paid-amount (- (:invoice/total invoice) (:invoice/outstanding-balance invoice))
|
|
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
|
|
deleted (deleted-expense-accounts invoice expense_accounts)
|
|
|
|
updated-invoice (cond-> {:db/id id
|
|
:invoice/invoice-number invoice_number
|
|
:invoice/date (coerce/to-date date)
|
|
|
|
:invoice/total total
|
|
:invoice/outstanding-balance (- total paid-amount)
|
|
:invoice/expense-accounts (map expense-account->entity
|
|
expense_accounts)}
|
|
due (assoc :invoice/due (coerce/to-date due))
|
|
(boolean? automatically_paid_when_due) (assoc :invoice/automatically-paid-when-due automatically_paid_when_due))]
|
|
@(d/transact (d/connect uri) (concat [updated-invoice]
|
|
(map (fn [d] [:db/retract id :invoice/expense-accounts d]) deleted)))
|
|
(-> (d-invoices/get-by-id id)
|
|
(->graphql))))
|
|
|
|
(defn void-invoice [context {id :invoice_id} value]
|
|
(let [invoice (d-invoices/get-by-id id)
|
|
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
|
|
updated-invoice (d-invoices/update {:db/id id
|
|
:invoice/total 0.0
|
|
:invoice/outstanding-balance 0.0
|
|
:invoice/status :invoice-status/voided
|
|
:invoice/expense-accounts (map (fn [ea] {:db/id (:db/id ea)
|
|
:invoice-expense-account/amount 0.0})
|
|
(:invoice/expense-accounts invoice))})]
|
|
|
|
(-> updated-invoice
|
|
(->graphql))))
|
|
|
|
|
|
(defn unvoid-invoice [context {id :invoice_id} value]
|
|
(let [invoice (d-invoices/get-by-id id)
|
|
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
|
|
conn (d/connect uri)
|
|
history (d/history (d/db conn))
|
|
txs (d/query {:query {:find ['?tx '?e '?original-status '?original-outstanding '?total '?ea '?ea-amount]
|
|
:where ['[?e :invoice/status :invoice-status/voided ?tx true]
|
|
'[?e :invoice/status ?original-status ?tx false]
|
|
'[?e :invoice/outstanding-balance ?original-outstanding ?tx false]
|
|
'[?e :invoice/total ?total ?tx false]
|
|
'[?ea :invoice-expense-account/amount ?ea-amount ?tx false]]
|
|
:in ['$ '?e]}
|
|
:args [history id]})
|
|
[last-transaction] (->> txs (sort-by first) (last))]
|
|
@(d/transact conn [(->> txs
|
|
(filter (fn [[tx]] (= tx last-transaction)))
|
|
(reduce (fn [new-transaction [_ entity original-status original-outstanding total expense-account expense-account-amount]]
|
|
(-> new-transaction
|
|
(assoc :db/id entity
|
|
:invoice/total total
|
|
:invoice/status original-status
|
|
:invoice/outstanding-balance original-outstanding)
|
|
|
|
(update :invoice/expense-accounts conj {:db/id expense-account :invoice-expense-account/amount expense-account-amount}))
|
|
) {}))])
|
|
|
|
(-> (d-invoices/get-by-id id)
|
|
(->graphql))))
|
|
|
|
(defn edit-expense-accounts [context args value]
|
|
(assert-can-see-client (:id context) (:db/id (:invoice/client (d-invoices/get-by-id (:invoice_id args)))))
|
|
(let [invoice-id (:invoice_id args)
|
|
invoice (d-invoices/get-by-id invoice-id)
|
|
deleted (deleted-expense-accounts invoice (:expense_accounts args))
|
|
updated {:db/id invoice-id
|
|
:invoice/expense-accounts (map
|
|
expense-account->entity
|
|
(:expense_accounts args))}]
|
|
|
|
@(d/transact (d/connect uri) (concat [updated]
|
|
(map (fn [d] [:db/retract invoice-id :invoice/expense-accounts d])deleted)))
|
|
(->graphql
|
|
(d-invoices/get-by-id (:invoice_id args)))))
|
|
|