makes creation of receivable invoices as possible

This commit is contained in:
2024-04-01 15:14:58 -07:00
parent 2d943fa421
commit 092377a93b
6 changed files with 256 additions and 5 deletions

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,7 @@
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.excel-invoices :as ei-routes]
[auto-ap.routes.admin.import-batch :as ib-routes]
[auto-ap.routes.outgoing-invoice :as oi-routes]
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
[auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.routes.invoice :as invoice-route]
@@ -82,7 +83,7 @@
(defn main-aside-nav- [request]
(let [selected (cond
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page} (:matched-route request))
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new} (:matched-route request))
"invoices"
(#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request))
@@ -129,7 +130,12 @@
{:date-range "month"})
:active? (= ::invoice-route/voided-page (:matched-route request))
:hx-boost "true"}
"Voided"))
"Voided")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::oi-routes/new)
:active? (= ::oi-routes/new (:matched-route request))
:hx-boost "true"}
"Create outgoing"))
(menu-button- {:icon svg/receipt-register-1

View File

@@ -30,6 +30,7 @@
[auto-ap.ssr.pos.sales-orders :as pos-sales]
[auto-ap.ssr.pos.tenders :as pos-tenders]
[auto-ap.ssr.invoices :as invoice]
[auto-ap.ssr.outgoing-invoice.new :as oin]
[auto-ap.ssr.search :as search]
[auto-ap.ssr.transaction.insights :as insights]
[auto-ap.ssr.users :as users]
@@ -100,5 +101,6 @@
(into admin-clients/key->handler)
(into admin-rules/key->handler)
(into indicators/key->handler)
(into payments/key->handler)))
(into payments/key->handler)
(into oin/route->handler)))

View File

@@ -0,0 +1,238 @@
(ns auto-ap.ssr.outgoing-invoice.new
(:require [amazonica.aws.lambda :as lambda]
[auto-ap.datomic :refer [conn pull-attr]]
[auto-ap.routes.invoice :as invoice-route]
[auto-ap.routes.outgoing-invoice :as route]
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated
wrap-secure]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :refer [wrap-trim-client-ids]]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [clj-date-schema modal-response money
percentage strip wrap-schema-decode]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clojure.data.json :as json]
[datomic.api :as dc]
[malli.core :as mc]))
(def form-schema (mc/schema [:map
[:outgoing-invoice/client [:entity-map {:pull '[:client/name {:client/address [*]}]}]]
[:outgoing-invoice/date clj-date-schema]
[:outgoing-invoice/to :string]
[:outgoing-invoice/invoice-number :string]
[:outgoing-invoice/tax percentage]
[:outgoing-invoice/to-address [:map
[:street1 :string]
[:street2 {:optional true} [:maybe [ :string { :decode/string strip}]]]
[:city :string]
[:state :string]
[:zip :string]]]
[:outgoing-invoice/line-items
[:vector {:coerce? true}
[:map
[:outgoing-invoice-line-item/description :string]
[:outgoing-invoice-line-item/unit-price money]
[:outgoing-invoice-line-item/quantity money]]]]]))
(defn form* [{:keys [form-params form-errors]}]
(fc/start-form
form-params
form-errors
[:form {:hx-post (bidi.bidi/path-for ssr-routes/only-routes
::route/new-submit)}
(fc/with-field :outgoing-invoice/invoice-number
(com/validated-field {:errors (fc/field-errors)
:label "Invoice #"}
[:div.w-96
(com/text-input {:name (fc/field-name)
:autofocus true
:error? (fc/error?)
:class "w-96"
:placeholder "10000"
:value (fc/field-value)})]))
(fc/with-field :outgoing-invoice/date
(com/validated-field {:errors (fc/field-errors)
:label "Date"}
[:div.w-96
(com/date-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "10000"
:value (-> (fc/field-value)
(atime/unparse-local atime/normal-date))})]))
[:div.flex.gap-4
(fc/with-field :outgoing-invoice/client
(com/validated-field {:errors (fc/field-errors)
:label "From (client)"}
[:div.w-96
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :company-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
[:div.flex.flex-col.gap-2
(fc/with-field :outgoing-invoice/to
(com/validated-field {:errors (fc/field-errors)
:label "To"}
[:div.w-96
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Hello Company"
:value (fc/field-value)})]))
(fc/with-field-default :outgoing-invoice/to-address {}
(list
(fc/with-field :address/street1
(com/validated-field {:label "Address"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-full"
:placeholder "1200 Pennsylvania Avenue"
:value (fc/field-value)})))
(fc/with-field :address/street2
(com/validated-field {:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-full"
:placeholder "Suite 300"
:value (fc/field-value)})))
[:div.flex.w-full.space-x-4
(fc/with-field :address/city
(com/validated-field {:errors (fc/field-errors)
:class "w-full grow shrink"}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-full"
:placeholder "Cupertino"
:value (fc/field-value)})))
(fc/with-field :address/state
(com/validated-field {:errors (fc/field-errors)
:class "w-16 shrink-0"}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-full"
:placeholder "CA"
:value (fc/field-value)})))
(fc/with-field :address/zip
(com/validated-field {:errors (fc/field-errors)
:class "w-24 shrink-0"}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:placeholder "98101"
:class "w-full"
:value (fc/field-value)})))]))]]
(fc/with-field-default :outgoing-invoice/line-items [{} {} {}]
(com/validated-field {:errors (fc/field-errors)
:label "Line items"}
(com/data-grid {:headers [(com/data-grid-header {} "Description")
(com/data-grid-header {} "Quantity")
(com/data-grid-header {} "Unit Price")]}
(fc/cursor-map
(fn [z]
(com/data-grid-row
{}
(com/data-grid-cell
{}
(fc/with-field :outgoing-invoice-line-item/description
(com/text-input {:name (fc/field-name)
:value (fc/field-value)
:class "w-full"
:placeholder "Catered sandwiches"})))
(com/data-grid-cell
{}
(fc/with-field :outgoing-invoice-line-item/quantity
(com/money-input {:name (fc/field-name)
:value (fc/field-value)
:placeholder "20"})))
(com/data-grid-cell
{}
(fc/with-field :outgoing-invoice-line-item/unit-price
(com/money-input {:name (fc/field-name)
:value (fc/field-value)
:placeholder "23.50"})))))))))
(fc/with-field-default :outgoing-invoice/tax 10.0
(com/validated-field {:errors (fc/field-errors)
:label "Tax %"}
(com/money-input {:name (fc/field-name)
:value (fc/field-value)})))
[:div.flex.flex-row-reverse (com/button {:color :primary :class "w-24"} "Generate")]]))
(defn submit [{:keys [form-params]}]
(let [line-items (->> form-params
:outgoing-invoice/line-items
(filter (fn [li] (not-empty (:outgoing-invoice-line-item/description li))))
(mapv
#(assoc % :outgoing-invoice-line-item/total (* (:outgoing-invoice-line-item/unit-price %)
(:outgoing-invoice-line-item/quantity %)))))
subtotal (reduce + 0.0 (map :outgoing-invoice-line-item/total line-items))
tax (* subtotal (:outgoing-invoice/tax form-params))
total (+ subtotal tax)
result
(-> (lambda/invoke {:function-name "genpdf" :payload
(json/write-str
(-> form-params
(assoc :outgoing-invoice/line-items
line-items
:outgoing-invoice/total total
:outgoing-invoice/tax tax)
(update :outgoing-invoice/date
#(some-> % (atime/unparse-local atime/normal-date)))))})
:payload
slurp
json/read-str)]
(modal-response
(com/modal {}
(com/modal-card {} [:div "Download your invoice"] [:div
"click "
(com/link {:href (str "https://data.prod.app.integreatconsult.com/" result)}
"here")
" to download"] [:div])))))
(def page (-> (fn page [{:keys [identity] :as request}]
(base-page
request
(com/page {:nav com/main-aside-nav
:client-selection (:client-selection (:session request))
:clients (:clients request)
:client (:client request)
:identity (:identity request)
:request request}
(apply com/breadcrumbs {} [[:a {:href (bidi/path-for ssr-routes/only-routes
::invoice-route/all-page)}
"Invoices"]
[:a {:href "#"} ;; TODO
"Outgoing"]
[:a {:href (bidi/path-for ssr-routes/only-routes
::route/new)}
"New"]])
(com/content-card {}
[:div {:class "flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"}
[:div
[:h1.text-2xl.mb-3.font-bold "New outgoing invoice"]]
[:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}]]
[:div.px-4
(form* request)]))
"New outgoing invoice"))
(wrap-trim-client-ids)
(wrap-secure)
(wrap-client-redirect-unauthenticated)))
(def route->handler
{::route/new page
::route/new-submit (-> submit
(wrap-schema-decode :form-schema form-schema)
(wrap-nested-form-params))})

View File

@@ -0,0 +1,3 @@
(ns auto-ap.routes.outgoing-invoice)
(def routes {"/" {"new" {:get ::new
:post ::new-submit} }})

View File

@@ -4,6 +4,7 @@
[auto-ap.routes.admin.import-batch :as ib-routes]
[auto-ap.routes.indicators :as indicator-routes]
[auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.routes.outgoing-invoice :as oi-routes]
[auto-ap.routes.payments :as p-routes]
[auto-ap.routes.invoice :as i-routes]
[auto-ap.routes.admin.clients :as ac-routes]
@@ -14,7 +15,7 @@
"logout" :logout
"search" :search
"indicators" indicator-routes/routes
"account" {"/search" {:get :account-search}}
"admin" {"" :auto-ap.routes.admin/page
"/client" ac-routes/routes
@@ -64,6 +65,7 @@
"/cash-drawer-shifts" {"" {:get :pos-cash-drawer-shifts}
"/table" {:get :pos-cash-drawer-shift-table}}}
"outgoing-invoice" oi-routes/routes
"payment" p-routes/routes
"invoice" i-routes/routes
"vendor" {"/search" :vendor-search}