From 6e0720c3b30fb2caebf7a9c52ef27d8259d60325 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Wed, 19 Jan 2022 08:05:43 -0800 Subject: [PATCH] Less error prone --- src/clj/auto_ap/background/sysco.clj | 26 +- src/clj/auto_ap/datomic/migrate.clj | 6 +- src/clj/auto_ap/graphql.clj | 202 +----------- src/clj/auto_ap/graphql/invoices.clj | 311 +++++++++++++----- src/clj/auto_ap/parse.clj | 4 +- src/clj/auto_ap/routes/invoices.clj | 22 +- .../views/components/invoice_table.cljs | 3 +- .../auto_ap/views/pages/import_invoices.cljs | 18 +- 8 files changed, 284 insertions(+), 308 deletions(-) diff --git a/src/clj/auto_ap/background/sysco.clj b/src/clj/auto_ap/background/sysco.clj index fde8a13c..8d14c363 100644 --- a/src/clj/auto_ap/background/sysco.clj +++ b/src/clj/auto_ap/background/sysco.clj @@ -67,8 +67,8 @@ (header-row "City2")]) account-number (some-> account-number Long/parseLong str) - matching-client (and account-number - (parse/best-match clients account-number 0.0)) + [matching-client similarity] (and account-number + (parse/best-match clients account-number 0.0)) _ (when-not matching-client (throw (ex-info "cannot find matching client" {:account-number account-number @@ -80,16 +80,18 @@ "yyMMdd")] (log/infof "Importing %s for %s" (header-row "InvoiceNumber") (header-row "CustomerName")) - (code-invoice #:invoice {:invoice-number (header-row "InvoiceNumber") - :total (+ total tax) - :outstanding-balance (+ total tax) - :location (parse/best-location-match matching-client location-hint location-hint ) - :date (coerce/to-date date) - :vendor (:db/id sysco-vendor ) - :client (:db/id matching-client) - :import-status :import-status/completed - :status :invoice-status/unpaid - :client-identifier customer-identifier}))) + (cond-> #:invoice {:invoice-number (header-row "InvoiceNumber") + :total (+ total tax) + :outstanding-balance (+ total tax) + :location (parse/best-location-match matching-client location-hint location-hint ) + :date (coerce/to-date date) + :vendor (:db/id sysco-vendor ) + :client (:db/id matching-client) + :import-status :import-status/completed + :status :invoice-status/unpaid + :client-identifier customer-identifier} + similarity (assoc :invoice/similarity (- 1.0 (double similarity))) + true (code-invoice)))) (defn mark-key [k] (s3/copy-object {:source-bucket-name bucket-name diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index cff50d9a..0f87a248 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -479,7 +479,11 @@ {:db/ident :invoice/location :db/doc "The location to code the invoice as" :db/valueType :db.type/string - :db/cardinality :db.cardinality/one}]]}} + :db/cardinality :db.cardinality/one}]]} + :auto-ap/add-invoice-similarity {:txes [[{:db/ident :invoice/similarity + :db/doc "How close an invoice matches its import" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one}]]}} diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index b0d8fb6b..10b2ec42 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -127,7 +127,7 @@ :detailed_status {:type 'String} :last_updated {:type :iso_date} :accounts {:type '(list :yodlee_account)}}} - + :yodlee_account {:fields {:id {:type 'Int} :status {:type 'String} @@ -198,7 +198,6 @@ {:fields {:id {:type :id} :client {:type :client} :terms {:type 'Int}}} - :schedule_payment_dom {:fields {:id {:type :id} @@ -240,7 +239,7 @@ :legal_entity_tin_type {:type :tin_type} :legal_entity_1099_type {:type :type_1099}}} - + :reminder {:fields {:id {:type 'Int} :email {:type 'String} @@ -249,7 +248,6 @@ :scheduled {:type 'String} :sent {:type 'String} :vendor {:type :vendor}}} - :journal_entry_line {:fields {:id {:type :id} @@ -302,8 +300,6 @@ :charges {:type '(list :charge)} :line_items {:type '(list :order_line_item)}}} - - :check {:fields {:id {:type :id} :type {:type 'String} :amount {:type 'String} @@ -315,7 +311,6 @@ :check_number {:type 'Int} :status {:type 'String} :invoices {:type '(list :invoice_payment)}}} - :payment {:fields {:id {:type :id} :type {:type :payment_type} @@ -331,7 +326,6 @@ :status {:type :ident} :transaction {:type :transaction} :invoices {:type '(list :invoice_payment)}}} - :yodlee_merchant {:fields {:id {:type :id} :yodlee_id {:type 'String} @@ -343,7 +337,6 @@ :forecast_match {:fields {:id {:type :id} :identifier {:type 'String}}} - :transaction_rule {:fields {:id {:type :id} :note {:type 'String} @@ -387,60 +380,24 @@ :name {:type 'String} :client_overrides {:type '(list :account_client_override)}}} - :invoices_expense_accounts - {:fields {:id {:type :id} - :invoice_id {:type 'String} - :account {:type :account} - :location {:type 'String} - - :amount {:type 'String}}} - :percentage_account {:fields {:id {:type :id} :account {:type :account} :location {:type 'String} :percentage {:type :percentage}}} - :invoice - {:fields {:id {:type :id} - :original_id {:type 'Int} - :client_identifier {:type 'String} - :total {:type 'String} - :source_url {:type 'String} - :outstanding_balance {:type 'String} - :invoice_number {:type 'String} - :status {:type 'String} - :expense_accounts {:type '(list :invoices_expense_accounts)} - :date {:type :iso_date} - :due {:type :iso_date} - :client_id {:type 'Int} - :payments {:type '(list :invoice_payment)} - :vendor {:type :vendor} - :client {:type :client} - :scheduled_payment {:type :iso_date}}} - - :yodlee_provider_account_page {:fields {:yodlee_provider_accounts {:type '(list :yodlee_provider_account)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} - :invoice_page {:fields {:invoices {:type '(list :invoice)} - :outstanding {:type :money} - :count {:type 'Int} - :total {:type 'Int} - :start {:type 'Int} - :end {:type 'Int}}} - :payment_page {:fields {:payments {:type '(list :payment)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} - - :transaction_rule_page {:fields {:transaction_rules {:type '(list :transaction_rule)} :count {:type 'Int} :total {:type 'Int} @@ -452,7 +409,7 @@ :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} - + :sales_order_page {:fields {:sales_orders {:type '(list :sales_order)} :count {:type 'Int} :total {:type 'Int} @@ -460,10 +417,6 @@ :end {:type 'Int} :sales_order_total {:type :money} :sales_order_tax {:type :money}}} - - - - :reminder_page {:fields {:reminders {:type '(list :reminder)} :count {:type 'Int} @@ -488,7 +441,6 @@ :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} :identifier {:type 'String} @@ -499,9 +451,6 @@ :outstanding_payments {:type :money} :upcoming_credits {:type '(list :upcoming_transaction)} :upcoming_debits {:type '(list :upcoming_transaction)}}}} - - - :queries {:expense_account_stats {:type '(list :expense_account_stat) @@ -542,38 +491,10 @@ :periods {:type '(list :date_range)}} :resolve :get-profit-and-loss} - :yodlee_provider_account_page {:type :yodlee_provider_account_page :args {:client_id {:type :id}} :resolve :get-yodlee-provider-account-page} - :invoice_page {:type '(list :invoice_page) - :args {:import_status {:type 'String} - :exact_match_id {:type :id} - :date_range {:type :date_range} - :due_range {:type :date_range} - :status {:type :invoice_status} - :unresolved {:type 'Boolean} - :scheduled_payments {:type 'Boolean} - :client_id {:type :id} - :vendor_id {:type :id} - :amount_lte {:type :money} - :amount_gte {:type :money} - :invoice_number_like {:type 'String} - :location {:type 'String} - :start {:type 'Int} - :per_page {:type 'Int} - :sort {:type '(list :sort_item)}} - - :resolve :get-invoice-page} - - :all_invoices {:type '(list :invoice) - :args {:client_id {:type :id} - :client_code {:type 'String} - :original_id {:type 'Int} - :statuses {:type '(list String)}} - :resolve :get-all-invoices} - :accounts {:type '(list :account) :args {:account_set {:type 'String}} :resolve :get-accounts} @@ -585,8 +506,6 @@ :statuses {:type '(list String)}} :resolve :get-all-payments} - - :all_sales_orders {:type '(list :sales_order) :args {:client_id {:type :id} :date_range {:type :date_range} @@ -596,15 +515,11 @@ :yodlee_merchants {:type '(list :yodlee_merchant) :args {} :resolve :get-yodlee-merchants} - + :intuit_bank_accounts {:type '(list :intuit_bank_account) :args {} :resolve :get-intuit-bank-accounts} - - - - :transaction_rule_page {:type :transaction_rule_page :args {:client_id {:type :id} :vendor_id {:type :id} @@ -629,11 +544,8 @@ :start {:type 'Int} :per_page {:type 'Int} :sort {:type '(list :sort_item)}} - :resolve :get-sales-order-page} - - :payment_page {:type '(list :payment_page) :args {:client_id {:type :id} :vendor_id {:type :id} @@ -647,9 +559,8 @@ :start {:type 'Int} :per_page {:type 'Int} :sort {:type '(list :sort_item)}} - :resolve :get-payment-page} - + :client {:type '(list :client) :resolve :get-client} :vendor {:type '(list :vendor) @@ -657,8 +568,6 @@ :user {:type '(list :user) :resolve :get-user}} - - :input-objects { :sort_item @@ -666,9 +575,6 @@ :sort_name {:type 'String} :asc {:type 'Boolean}}} - - - :ledger_filters {:fields {:client_id {:type :id} :vendor_id {:type :id} :account_id {:type :id} @@ -728,7 +634,7 @@ :location_matches {:type '(list :edit_location_match)} :bank_accounts {:type '(list :edit_bank_account)} :forecasted_transactions {:type '(list :edit_forecasted_transaction)}}} - + :edit_bank_account {:fields {:id {:type :id} :code {:type 'String} @@ -771,7 +677,6 @@ {:fields {:id {:type :id} :client_id {:type :id} :terms {:type 'Int}}} - :add_account_override {:fields {:id {:type :id} @@ -790,54 +695,21 @@ :terms_overrides {:type '(list :add_terms_override)} :code {:type 'String} :automatically_paid_when_due {:type '(list :id)} - :hidden {:type 'Boolean} :print_as {:type 'String} :primary_contact {:type :add_contact} :secondary_contact {:type :add_contact} :address {:type :add_address} - :default_account_id {:type :id} :account_overrides {:type '(list :add_account_override)} :schedule_payment_dom {:type '(list :add_schedule_payment_dom)} :invoice_reminder_schedule {:type 'String} - :legal_entity_first_name {:type 'String} :legal_entity_middle_name {:type 'String} :legal_entity_last_name {:type 'String} :legal_entity_tin {:type 'String} :legal_entity_tin_type {:type :tin_type} :legal_entity_1099_type {:type :type_1099}}} - - - :edit_expense_account - {:fields {:id {:type :id} - :account_id {:type :id} - :location {:type 'String} - :amount {:type :money}}} - - :add_invoice - {:fields {:id {:type :id} - :invoice_number {:type 'String} - :expense_accounts {:type '(list :edit_expense_account)} - :location {:type 'String} - :scheduled_payment {:type :iso_date} - :date {:type :iso_date} - :due {:type :iso_date} - :client_id {:type :id} - :vendor_id {:type :id} - :vendor_name {:type 'String} - :total {:type :money}}} - - :edit_invoice - {:fields {:id {:type :id} - :invoice_number {:type 'String} - :expense_accounts {:type '(list :edit_expense_account)} - :date {:type :iso_date} - :scheduled_payment {:type :iso_date} - :due {:type :iso_date} - :total {:type :money}}} - :edit_percentage_account {:fields {:id {:type :id} @@ -888,9 +760,6 @@ :type_1099 {:values [{:enum-value :none} {:enum-value :misc} {:enum-value :landlord}]} - :invoice_status {:values [{:enum-value :paid} - {:enum-value :unpaid} - {:enum-value :voided}]} :bank_account_type {:values [{:enum-value :check} {:enum-value :credit} {:enum-value :cash}]} @@ -902,28 +771,17 @@ {:enum-value :asset} {:enum-value :liability} {:enum-value :equity} - {:enum-value :revenue}]} - } + {:enum-value :revenue}]}} :mutations {:request_import {:type 'String :args {:which {:type 'String}} :resolve :mutation/request-import} - :reject_invoices {:type '(list :id) - :args {:invoices {:type '(list :id)}} - :resolve :mutation/reject-invoices} - - :approve_invoices {:type '(list :id) - :args {:invoices {:type '(list :id)}} - :resolve :mutation/approve-invoices} - - - :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} @@ -932,12 +790,6 @@ :to {:type :id}} :resolve :mutation/merge-vendors} - :add_and_print_invoice {:type :check_result - :args {:invoice {:type :add_invoice} - :bank_account_id {:type :id} - :type {:type :payment_type}} - :resolve :mutation/add-and-print-invoice} - :print_checks {:type :check_result :args {:invoice_payments {:type '(list :invoice_payment_amount)} :bank_account_id {:type :id} @@ -966,38 +818,18 @@ :upsert_transaction_rule {:type :transaction_rule :args {:transaction_rule {:type :edit_transaction_rule}} :resolve :mutation/upsert-transaction-rule} - :add_invoice {:type :invoice - :args {:invoice {:type :add_invoice}} - :resolve :mutation/add-invoice} :import_ledger {:type :import_ledger_result :args {:entries {:type '(list :import_ledger_entry)}} :resolve :mutation/import-ledger} - :edit_invoice {:type :invoice - :args {:invoice {:type :edit_invoice}} - :resolve :mutation/edit-invoice} + :upsert_account {:type :account :args {:account {:type :edit_account}} :resolve :mutation/upsert-account} - :void_invoice {:type :invoice - :args {:invoice_id {:type :id}} - :resolve :mutation/void-invoice} - :unvoid_invoice {:type :invoice - :args {:invoice_id {:type :id}} - :resolve :mutation/unvoid-invoice} - - :unautopay_invoice {:type :invoice - :args {:invoice_id {:type :id}} - :resolve :mutation/unautopay-invoice} - :void_payment {:type :payment :args {:payment_id {:type :id}} - :resolve :mutation/void-payment} - :edit_expense_accounts {:type :invoice - :args {:invoice_id {:type :id} - :expense_accounts {:type '(list :edit_expense_account)}} - :resolve :mutation/edit-expense-accounts}}}) + :resolve :mutation/void-payment}}}) (defn snake->kebab [s] @@ -1226,8 +1058,7 @@ (def schema (-> integreat-schema - (attach-resolvers {:get-invoice-page gq-invoices/get-invoice-page - :get-all-invoices gq-invoices/get-all-invoices + (attach-resolvers {:mutation/void-payment gq-checks/void-check :get-yodlee-provider-account-page gq-yodlee2/get-yodlee-provider-account-page :get-all-payments get-all-payments :get-all-sales-orders get-all-sales-orders @@ -1250,12 +1081,7 @@ :mutation/add-handwritten-check gq-checks/add-handwritten-check :mutation/delete-transaction-rule gq-transaction-rules/delete-transaction-rule :mutation/print-checks print-checks - :mutation/reject-invoices gq-invoices/reject-invoices - :mutation/approve-invoices gq-invoices/approve-invoices :mutation/edit-user gq-users/edit-user - :mutation/add-invoice gq-invoices/add-invoice - :mutation/add-and-print-invoice gq-invoices/add-and-print-invoice - :mutation/edit-invoice gq-invoices/edit-invoice :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 @@ -1264,11 +1090,6 @@ :mutation/upsert-vendor gq-vendors/upsert-vendor :mutation/upsert-account gq-accounts/upsert-account :mutation/merge-vendors gq-vendors/merge-vendors - :mutation/void-invoice gq-invoices/void-invoice - :mutation/unvoid-invoice gq-invoices/unvoid-invoice - :mutation/unautopay-invoice gq-invoices/unautopay-invoice - :mutation/void-payment gq-checks/void-check - :mutation/edit-expense-accounts gq-invoices/edit-expense-accounts :mutation/import-ledger gq-ledger/import-ledger :mutation/request-import gq-requests/request-import :get-vendor gq-vendors/get-graphql}) @@ -1276,6 +1097,7 @@ gq-import-batches/attach gq-transactions/attach gq-expected-deposit/attach + gq-invoices/attach schema/compile)) diff --git a/src/clj/auto_ap/graphql/invoices.clj b/src/clj/auto_ap/graphql/invoices.clj index 6c04aaa6..b81b81e5 100644 --- a/src/clj/auto_ap/graphql/invoices.clj +++ b/src/clj/auto_ap/graphql/invoices.clj @@ -1,28 +1,33 @@ (ns auto-ap.graphql.invoices - (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin assert-power-user 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 audit-transact conn]] - [clj-time.coerce :as coerce] - [clj-time.core :as time] - [clojure.set :as set] - [clojure.tools.logging :as log])) + (:require + [auto-ap.datomic :refer [audit-transact conn remove-nils uri]] + [auto-ap.datomic.clients :as d-clients] + [auto-ap.datomic.invoices :as d-invoices] + [auto-ap.datomic.vendors :as d-vendors] + [auto-ap.graphql.checks :as gq-checks] + [auto-ap.graphql.utils + :refer [->graphql + <-graphql + assert-admin + assert-can-see-client + assert-power-user + enum->keyword]] + [auto-ap.utils :refer [dollars=]] + [clj-time.coerce :as coerce] + [clj-time.core :as time] + [clojure.set :as set] + [clojure.tools.logging :as log] + [com.walmartlabs.lacinia.util :refer [attach-resolvers]] + [datomic.api :as d])) + +(defn get-invoice-page [context args _] -(defn get-invoice-page [context args value] - (let [args (assoc args :id (:id context)) [invoices invoice-count outstanding] (-> args (assoc :id (:id context)) - (<-graphql ) + (<-graphql) (update :status enum->keyword "invoice-status") - (d-invoices/get-graphql ))] + (d-invoices/get-graphql))] [{:invoices (map ->graphql invoices) :outstanding outstanding :total invoice-count @@ -30,44 +35,42 @@ :start (:start args 0) :end (+ (:start args 0) (count invoices))}])) -(defn get-all-invoices [context args value] +(defn get-all-invoices [context args _] (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] +(defn reject-invoices [context {:keys [invoices]} _] (assert-power-user (:id context)) (doseq [i invoices] (assert-can-see-client (:id context) (:db/id (:invoice/client (d/entity (d/db conn) i))))) - (let [transactions (map (fn [i] [:db/retractEntity i ]) invoices) - transaction-result (audit-transact transactions (:id context))] + (let [transactions (map (fn [i] [:db/retractEntity i]) invoices)] + (audit-transact transactions (:id context)) invoices)) -(defn approve-invoices [context {:keys [invoices] :as in} value] +(defn approve-invoices [context {:keys [invoices]} _] (assert-power-user (:id context)) (doseq [i invoices] (assert-can-see-client (:id context) (:db/id (:invoice/client (d/entity (d/db conn) i))))) - (let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices) - transaction-result (audit-transact transactions (:id context))] + (let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices)] + (audit-transact transactions (:id context)) invoices)) -(defn assert-no-conflicting [{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in}] +(defn assert-no-conflicting [{:keys [invoice_number client_id vendor_id]}] (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 amount - :db/id id + :db/id id :account account_id :location location})) -(defn add-invoice-transaction [{:keys [total invoice_number location scheduled_payment client_id vendor_id vendor_name date due expense_accounts] :as in}] +(defn add-invoice-transaction [{:keys [total invoice_number scheduled_payment client_id vendor_id date due expense_accounts]}] (let [vendor (d-vendors/get-by-id vendor_id) account (:vendor/default-account vendor) due (or (and (:vendor/terms vendor) @@ -78,24 +81,22 @@ (and (d-vendors/automatically-paid-for-client-id? vendor client_id) due)) _ (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)} + (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)} due (assoc :invoice/due (coerce/to-date due)) - scheduled_payment (assoc :invoice/scheduled-payment (coerce/to-date scheduled_payment))))) - (defn deleted-expense-accounts [invoice expense-accounts] (let [current-expense-accounts (:invoice/expense-accounts invoice) specified-ids (->> expense-accounts @@ -109,16 +110,16 @@ (defn assert-valid-expense-accounts [expense_accounts] (doseq [expense-account expense_accounts :let [account (d/entity (d/db conn) (:account_id expense-account))]] - (log/info "ACCOUNT" (:account/location account) ) + (log/info "ACCOUNT" (:account/location account)) (when (empty? (:location expense-account)) (throw (ex-info "Expense account is missing location" {:validation-error "Expense account is missing location"}))) - (when (and (not (empty? (:account/location account))) + (when (and (seq (:account/location account)) (not= (:location expense-account) (:account/location account))) (let [err (str "Account uses location '" (:location expense-account) "' but expects '" (:account/location account) "'")] (throw (ex-info err - {:validation-error err})))) + {:validation-error err})))) (when (and (empty? (:account/location account)) (= "A" (:location expense-account))) @@ -126,36 +127,33 @@ (throw (ex-info err {:validation-error err})))) - (when (nil? (:account_id expense-account)) (throw (ex-info "Expense account is missing account" {:validation-error "Expense account is missing account"}))) (when (nil? (:amount expense-account)) (throw (ex-info "Expense account does not have an amount specified." {:validation-error "Expense account does not have an amount specified."}))))) - (defn assert-invoice-amounts-add-up [{:keys [expense_accounts total]}] (let [expense-account-total (reduce + 0 (map (fn [x] (:amount x)) expense_accounts))] - (when-not (dollars= total expense-account-total) + (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})))))) - -(defn add-invoice [context {{:keys [total expense_accounts invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value] +(defn add-invoice [context {{:keys [expense_accounts client_id] :as in} :invoice} _] (assert-no-conflicting in) (assert-can-see-client (:id context) client_id) (assert-valid-expense-accounts expense_accounts) (assert-invoice-amounts-add-up in) - + (let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))] (-> (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."} )))) + (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] +(defn add-and-print-invoice [context {{:keys [total client_id] :as in} :invoice bank-account-id :bank_account_id type :type} _] (assert-no-conflicting in) (assert-can-see-client (:id context) client_id) (assert-bank-account-belongs client_id bank-account-id) @@ -170,8 +168,7 @@ (:id context)) ->graphql))) - -(defn edit-invoice [context {{:keys [id due invoice_number total vendor_id date client_id expense_accounts scheduled_payment] :as in} :invoice} value] +(defn edit-invoice [context {{:keys [id due invoice_number total date expense_accounts scheduled_payment] :as in} :invoice} _] (let [invoice (d-invoices/get-by-id id) _ (when (seq (d-invoices/find-conflicting {:db/id id :invoice/invoice-number invoice_number @@ -179,20 +176,17 @@ :invoice/client (:db/id (:invoice/client invoice))})) (throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number}))) - - 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) _ (assert-valid-expense-accounts expense_accounts) _ (assert-invoice-amounts-add-up in) - - + updated-invoice (cond-> {:db/id id :invoice/invoice-number invoice_number :invoice/date (coerce/to-date date) - - :invoice/total total + + :invoice/total total :invoice/outstanding-balance (- total paid-amount) :invoice/expense-accounts (map expense-account->entity expense_accounts)} @@ -204,22 +198,21 @@ (-> (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)))] - (audit-transact [{: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))}] - (:id context)) +(defn void-invoice [context {id :invoice_id} _] + (let [invoice (d-invoices/get-by-id id) + _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))] + (audit-transact [{: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))}] + (:id context)) - (-> (d-invoices/get-by-id id) (->graphql)))) + (-> (d-invoices/get-by-id id) (->graphql)))) - -(defn unvoid-invoice [context {id :invoice_id} value] +(defn unvoid-invoice [context {id :invoice_id} _] (let [invoice (d-invoices/get-by-id id) _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice))) conn (d/connect uri) @@ -236,20 +229,19 @@ (audit-transact [(->> txs (filter (fn [[tx]] (= tx last-transaction))) (reduce (fn [new-transaction [_ entity original-status original-outstanding total expense-account expense-account-amount]] - (-> new-transaction + (-> 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})) - ) {}))] + (update :invoice/expense-accounts conj {:db/id expense-account :invoice-expense-account/amount expense-account-amount}))) {}))] (:id context)) (-> (d-invoices/get-by-id id) (->graphql)))) -(defn unautopay-invoice [context {id :invoice_id} value] +(defn unautopay-invoice [context {id :invoice_id} _] (let [invoice (d/entity (d/db conn) id) _ (assert (:invoice/client invoice)) _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))] @@ -261,7 +253,7 @@ (-> (d-invoices/get-by-id id) (->graphql)))) -(defn edit-expense-accounts [context args value] +(defn edit-expense-accounts [context args _] (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) @@ -273,8 +265,157 @@ (:expense_accounts args))}] (audit-transact (concat [updated] - (map (fn [d] [:db/retract invoice-id :invoice/expense-accounts d])deleted)) + (map (fn [d] [:db/retract invoice-id :invoice/expense-accounts d]) deleted)) (:id context)) (->graphql (d-invoices/get-by-id (:invoice_id args))))) +(def objects + {:invoice + {:fields {:id {:type :id} + :original_id {:type 'Int} + :client_identifier {:type 'String} + :total {:type 'String} + :source_url {:type 'String} + :outstanding_balance {:type 'String} + :invoice_number {:type 'String} + :status {:type 'String} + :expense_accounts {:type '(list :invoices_expense_accounts)} + :similarity {:type 'Float} + :date {:type :iso_date} + :due {:type :iso_date} + :client_id {:type 'Int} + :payments {:type '(list :invoice_payment)} + :vendor {:type :vendor} + :client {:type :client} + :scheduled_payment {:type :iso_date}}} + + :invoices_expense_accounts + {:fields {:id {:type :id} + :invoice_id {:type 'String} + :account {:type :account} + :location {:type 'String} + :amount {:type 'String}}} + + :invoice_page {:fields {:invoices {:type '(list :invoice)} + :outstanding {:type :money} + :count {:type 'Int} + :total {:type 'Int} + :start {:type 'Int} + :end {:type 'Int}}}}) + +(def queries + {:invoice_page {:type '(list :invoice_page) + :args {:import_status {:type 'String} + :exact_match_id {:type :id} + :date_range {:type :date_range} + :due_range {:type :date_range} + :status {:type :invoice_status} + :unresolved {:type 'Boolean} + :scheduled_payments {:type 'Boolean} + :client_id {:type :id} + :vendor_id {:type :id} + :amount_lte {:type :money} + :amount_gte {:type :money} + :invoice_number_like {:type 'String} + :location {:type 'String} + :start {:type 'Int} + :per_page {:type 'Int} + :sort {:type '(list :sort_item)}} + + :resolve :get-invoice-page} + :all_invoices {:type '(list :invoice) + :args {:client_id {:type :id} + :client_code {:type 'String} + :original_id {:type 'Int} + :statuses {:type '(list String)}} + :resolve :get-all-invoices}}) + +(def mutations + {:edit_invoice {:type :invoice + :args {:invoice {:type :edit_invoice}} + :resolve :mutation/edit-invoice} + :add_invoice {:type :invoice + :args {:invoice {:type :add_invoice}} + :resolve :mutation/add-invoice} + :void_invoice {:type :invoice + :args {:invoice_id {:type :id}} + :resolve :mutation/void-invoice} + :unvoid_invoice {:type :invoice + :args {:invoice_id {:type :id}} + :resolve :mutation/unvoid-invoice} + :unautopay_invoice {:type :invoice + :args {:invoice_id {:type :id}} + :resolve :mutation/unautopay-invoice} + :reject_invoices {:type '(list :id) + :args {:invoices {:type '(list :id)}} + :resolve :mutation/reject-invoices} + + :approve_invoices {:type '(list :id) + :args {:invoices {:type '(list :id)}} + :resolve :mutation/approve-invoices} + :add_and_print_invoice {:type :check_result + :args {:invoice {:type :add_invoice} + :bank_account_id {:type :id} + :type {:type :payment_type}} + :resolve :mutation/add-and-print-invoice} + :edit_expense_accounts {:type :invoice + :args {:invoice_id {:type :id} + :expense_accounts {:type '(list :edit_expense_account)}} + :resolve :mutation/edit-expense-accounts}}) + +(def input-objects + {:add_invoice + {:fields {:id {:type :id} + :invoice_number {:type 'String} + :expense_accounts {:type '(list :edit_expense_account)} + :location {:type 'String} + :scheduled_payment {:type :iso_date} + :date {:type :iso_date} + :due {:type :iso_date} + :client_id {:type :id} + :vendor_id {:type :id} + :vendor_name {:type 'String} + :total {:type :money}}} + :edit_invoice + {:fields {:id {:type :id} + :invoice_number {:type 'String} + :expense_accounts {:type '(list :edit_expense_account)} + :date {:type :iso_date} + :scheduled_payment {:type :iso_date} + :due {:type :iso_date} + :total {:type :money}}} + + :edit_expense_account + {:fields {:id {:type :id} + :account_id {:type :id} + :location {:type 'String} + :amount {:type :money}}}}) + +(def enums + {:invoice_status {:values [{:enum-value :paid} + {:enum-value :unpaid} + {:enum-value :voided}]}}) + +(def resolvers + {:get-invoice-page get-invoice-page + :get-all-invoices get-all-invoices + :mutation/reject-invoices reject-invoices + :mutation/approve-invoices approve-invoices + :mutation/add-invoice add-invoice + :mutation/add-and-print-invoice add-and-print-invoice + :mutation/edit-invoice edit-invoice + :mutation/void-invoice void-invoice + :mutation/unvoid-invoice unvoid-invoice + :mutation/unautopay-invoice unautopay-invoice + :mutation/edit-expense-accounts edit-expense-accounts}) + +(defn attach [schema] + (-> + (merge-with merge schema + {:objects objects + :queries queries + :mutations mutations + :input-objects input-objects + :enums enums}) + (attach-resolvers resolvers))) diff --git a/src/clj/auto_ap/parse.clj b/src/clj/auto_ap/parse.clj index db214af8..2821ccbc 100644 --- a/src/clj/auto_ap/parse.clj +++ b/src/clj/auto_ap/parse.clj @@ -91,7 +91,7 @@ (conj matches name)))) (filter #(<= (second %) threshold)) (sort-by second) - ffirst) + first) word-set (set (filter (complement str/blank?) (str/split (.toLowerCase invoice-client-name) #"[\s:\-]" ))) client-word-match (->> clients @@ -108,7 +108,7 @@ (filter (fn [[_ c]] (> c 0))) (sort-by (fn [[_ c]] c)) reverse - ffirst)] + first)] (or fuzzy-match client-word-match)))) (defn best-location-match [client text full-text] diff --git a/src/clj/auto_ap/routes/invoices.clj b/src/clj/auto_ap/routes/invoices.clj index 40a45023..67679fcb 100644 --- a/src/clj/auto_ap/routes/invoices.clj +++ b/src/clj/auto_ap/routes/invoices.clj @@ -126,20 +126,20 @@ (defn import->invoice [{:keys [invoice-number source-url customer-identifier account-number total date vendor-code text full-text client-override vendor-override location-override]} clients] - (let [matching-client (cond - account-number (parse/best-match clients account-number 0.0) - customer-identifier (parse/best-match clients customer-identifier) - client-override (first (filter (fn [c] - (= (:db/id c) (Long/parseLong client-override))) - clients))) - matching-vendor (match-vendor vendor-code vendor-override) - matching-location (or (when-not (str/blank? location-override) - location-override) - - (parse/best-location-match matching-client text full-text))] + (let [[matching-client similarity] (cond + account-number (parse/best-match clients account-number 0.0) + customer-identifier (parse/best-match clients customer-identifier) + client-override (first (filter (fn [c] + (= (:db/id c) (Long/parseLong client-override))) + clients))) + matching-vendor (match-vendor vendor-code vendor-override) + matching-location (or (when-not (str/blank? location-override) + location-override) + (parse/best-location-match matching-client text full-text))] (remove-nils #:invoice {:invoice/client (:db/id matching-client) :invoice/client-identifier (or account-number customer-identifier) :invoice/vendor (:db/id matching-vendor) + :invoice/similarity (some-> similarity double (#(- 1.0 %))) :invoice/source-url source-url :invoice/invoice-number invoice-number :invoice/total (Double/parseDouble total) diff --git a/src/cljs/auto_ap/views/components/invoice_table.cljs b/src/cljs/auto_ap/views/components/invoice_table.cljs index 51c98842..6700295a 100644 --- a/src/cljs/auto_ap/views/components/invoice_table.cljs +++ b/src/cljs/auto_ap/views/components/invoice_table.cljs @@ -45,7 +45,7 @@ :unpaid-invoices :unpaid :paid-invoices :paid :voided-invoices :voided)} - [[:invoices [:id :total :outstanding-balance :invoice-number :date :due :status :client-identifier :scheduled-payment :source-url + [[:invoices [:id :total :outstanding-balance :invoice-number :date :due :status :client-identifier :scheduled-payment :source-url :similarity [:vendor [:name :id]] [:expense_accounts [:amount :id :location [:account [:id ]]]] @@ -202,6 +202,7 @@ "File"] [:td {:colspan 4} [buttons/fa-icon {:icon "fa-external-link" + :target "_new" :href source-url}]]])]]]]] [:span {:style {:margin-right "1em"}}]]) diff --git a/src/cljs/auto_ap/views/pages/import_invoices.cljs b/src/cljs/auto_ap/views/pages/import_invoices.cljs index 94e20691..c67fb76e 100644 --- a/src/cljs/auto_ap/views/pages/import_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/import_invoices.cljs @@ -5,7 +5,7 @@ [auto-ap.subs :as subs] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.components.invoices.side-bar :refer [invoices-side-bar]] - [auto-ap.views.utils :refer [dispatch-event with-user]] + [auto-ap.views.utils :refer [dispatch-event with-user ->%]] [auto-ap.utils :refer [by]] [auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.views.components.invoice-table :refer [invoice-table] :as invoice-table] @@ -16,7 +16,9 @@ [dropzone :as dz] [auto-ap.views.pages.data-page :as data-page] [clojure.set :as set] - [auto-ap.effects.forward :as forward])) + [auto-ap.effects.forward :as forward] + [goog.string :as gstring] + )) (defn dropzone [] @@ -69,9 +71,7 @@ [:span [:span {:class "icon"} [:i {:class "fa fa-cloud-download"}]] - "Drop any invoices you want to process here"]]]])}) - )) - + "Drop any invoices you want to process here"]]]])}))) @@ -207,7 +207,13 @@ :data-page :import-invoices :overrides {:client (fn [row] [:p (:name (:client row)) - [:p [:i.is-size-7 (:client-identifier row)]]])} + [:p [:i.is-size-7 (:client-identifier row)] + " " + [:span + {:style {:background-color (gstring/format "rgba(255, 0,0,%.2f)" (- 1.0 (:similarity row))) + }} + + (->% (:similarity row))]]])} :check-boxes true}] [:span "No pending invoices"])]]])) {:component-did-mount (fn []