From 08dbfccef2907b1c12111e75884b2d1fb3fead5d Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 12:18:30 -0500 Subject: [PATCH 01/90] Show product id erorrs --- .../live/sales_order_live/show.ex | 18 ++- lib/therons_erp_web/sales/sales_line.ex | 23 ++- ...210171140_total_prices_on_sales_orders.exs | 25 +++ .../repo/sales_lines/20250210171140.json | 147 ++++++++++++++++++ 4 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 priv/repo/migrations/20250210171140_total_prices_on_sales_orders.exs create mode 100644 priv/resource_snapshots/repo/sales_lines/20250210171140.json diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index ff8194f..274f7db 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -34,14 +34,17 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do + + <.inputs_for :let={sales_line} field={@form[:sales_lines]}> + @@ -59,6 +62,16 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do type="number" /> + <.inputs_for :let={sales_line} field={@form[:sales_lines]}> - + @@ -139,17 +171,31 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end @impl true - def handle_params(%{"id" => id}, _, socket) do + def handle_params(%{"id" => id} = params, _, socket) do + sales_order = + Ash.get!(TheronsErp.Sales.SalesOrder, id, + actor: socket.assigns.current_user, + load: [sales_lines: [:total_price, :product]] + ) + + default_products = + for line_item <- sales_order.sales_lines, into: %{} do + prod = line_item.product + + {prod.id, [%{value: prod.id, label: prod.name, matches: []}]} + end + {:noreply, socket |> assign(:page_title, page_title(socket.assigns.live_action)) |> assign( :sales_order, - Ash.get!(TheronsErp.Sales.SalesOrder, id, - actor: socket.assigns.current_user, - load: [sales_lines: [:total_price]] - ) + sales_order ) + |> assign(:args, params["args"]) + |> assign(:from_args, params["from_args"]) + |> assign(:params, params) + |> assign(:default_products, default_products) |> assign(:drop_sales, 0) |> assign_form()} end @@ -158,14 +204,20 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do defp page_title(:edit), do: "Edit Sales order" @impl true - def handle_event("validate", %{"sales_order" => sales_order_params}, socket) do - form = AshPhoenix.Form.validate(socket.assigns.form, sales_order_params) - drop = length(sales_order_params["_drop_sales_lines"] || []) + def handle_event("validate", %{"sales_order" => sales_order_params} = params, socket) do + if sales_order_params["product_id"] == "create" do + IO.inspect(params) + {:noreply, socket} + else + form = AshPhoenix.Form.validate(socket.assigns.form, sales_order_params) + drop = length(sales_order_params["_drop_sales_lines"] || []) - {:noreply, - assign(socket, form: form) - |> assign(:unsaved_changes, form.source.changed? || drop > 0) - |> assign(:drop_sales, drop)} + {:noreply, + assign(socket, form: form) + |> assign(:unsaved_changes, form.source.changed? || drop > 0) + |> assign(:params, sales_order_params) + |> assign(:drop_sales, drop)} + end end def handle_event("save", %{"sales_order" => sales_order_params}, socket) do @@ -195,7 +247,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end end - defp assign_form(%{assigns: %{sales_order: sales_order}} = socket) do + defp assign_form( + %{assigns: %{sales_order: sales_order, args: args, from_args: from_args}} = socket + ) do form = if sales_order do AshPhoenix.Form.for_update(sales_order, :update, @@ -209,8 +263,84 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do ) end + form = + case {args, from_args} do + {nil, nil} -> form + {_, nil} -> AshPhoenix.Form.validate(form, args) + {nil, _} -> AshPhoenix.Form.validate(form, from_args) + _ -> AshPhoenix.Form.validate(form, Map.merge(args, from_args)) + end + socket |> assign(form: to_form(form)) |> assign(:unsaved_changes, form.changed?) end + + defp parse_select_id!(id) do + [_, number] = + Regex.run(~r/sales_order\[sales_lines\]\[(\d+)\]_product_id_live_select_component/, id) + + number + end + + def handle_event( + "live_select_change", + %{"text" => text, "id" => id}, + socket + ) do + opts = + get_products("") + |> prepare_matches(text) + + send_update(LiveSelect.Component, id: id, options: opts) + {:noreply, socket} + end + + @impl true + def handle_event( + "set-default", + %{"id" => id}, + socket + ) do + number = parse_select_id!(id) + + if pid = + socket.assigns.from_args["product_id"] && socket.assigns.from_args["line_id"] == number do + opts = get_initial_product_options(pid) + + send_update(LiveSelect.Component, + options: opts, + id: id, + value: pid + ) + else + value = + socket.assigns.form + |> Phoenix.HTML.Form.input_value(:sales_lines) + |> Enum.at(String.to_integer(number)) + |> Phoenix.HTML.Form.input_value(:product_id) + + if value not in [nil, ""] do + products = get_products(value) + text = Enum.find(products, &(&1.value == value)).label + opts = prepare_matches(products, text) + + send_update(LiveSelect.Component, + options: opts, + id: id, + value: value + ) + else + products = get_products("") + + send_update(LiveSelect.Component, + options: products, + id: id, + value: nil + ) + end + end + + {:noreply, socket} + end end diff --git a/lib/therons_erp_web/selects.ex b/lib/therons_erp_web/selects.ex index 8368d7b..0f07198 100644 --- a/lib/therons_erp_web/selects.ex +++ b/lib/therons_erp_web/selects.ex @@ -1,24 +1,32 @@ defmodule TheronsErpWeb.Selects do alias TheronsErp.Inventory - def prepare_matches(categories, text) do + def prepare_matches(items, text) do matches = - Seqfuzz.matches(categories, text, & &1.label, filter: true, sort: true) + Seqfuzz.matches(items, text, & &1.label, filter: true, sort: true) (matches - |> Enum.map(fn {categories, c} -> - %{value: categories.value, label: categories.label, matches: c.matches} + |> Enum.map(fn {items, c} -> + %{value: items.value, label: items.label, matches: c.matches} end) |> Enum.take(5)) ++ additional_options() end def get_categories(selected) do + _get_list(Inventory.get_categories!(), selected, & &1.full_name) + end + + def get_products(selected) do + _get_list(Inventory.get_products!(), selected, & &1.name) + end + + defp _get_list(items, selected, mapper) do list = - Inventory.get_categories!() - |> Enum.map(fn cat -> + items + |> Enum.map(fn item -> %{ - value: to_string(cat.id), - label: cat.full_name, + value: to_string(item.id), + label: mapper.(item), matches: [] } end) @@ -43,10 +51,23 @@ defmodule TheronsErpWeb.Selects do ] end + def additional_product_options do + [ + %{ + value: :create, + label: "Create New", + matches: [] + } + ] + end + def get_initial_options(selected) do (get_categories(selected) ++ additional_options()) |> Enum.uniq() |> Enum.take(5) end + def get_initial_product_options(selected) do + (get_products(selected) ++ additional_product_options()) |> Enum.uniq() |> Enum.take(5) + end def get_category_name(categories, id) do found = @@ -55,4 +76,12 @@ defmodule TheronsErpWeb.Selects do found.label end + + def get_product_name(products, id) do + found = + products + |> Enum.find(&(to_string(&1.value) == to_string(id))) + + found.label + end end From 079076dbbafb717e3988ec2257ada591fa0ded18 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 13:32:56 -0500 Subject: [PATCH 03/90] Changing product --- assets/css/app.css | 8 ++++++++ lib/therons_erp_web/components/layouts/app.html.heex | 2 +- lib/therons_erp_web/live/sales_order_live/show.ex | 7 +++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 93764c6..563cc16 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -67,3 +67,11 @@ } } } + +.product-category-table { + td { + input { + width: 100%; + } + } +} diff --git a/lib/therons_erp_web/components/layouts/app.html.heex b/lib/therons_erp_web/components/layouts/app.html.heex index 98e3b24..f3cbbea 100644 --- a/lib/therons_erp_web/components/layouts/app.html.heex +++ b/lib/therons_erp_web/components/layouts/app.html.heex @@ -37,7 +37,7 @@
-
+
<.flash_group flash={@flash} /> {@inner_content}
diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 6b731a9..d4db81e 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -48,7 +48,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do
Product Quantity Sales Price Unit PriceTotal
<.input field={sales_line[:product_id]} /> <.input field={sales_line[:quantity]} type="number" /> + <.input + field={sales_line[:total_price]} + value={ + IO.inspect(do_money(sales_line[:total_price])) || + do_money(sales_line[:calculated_total]) + } + type="number" + /> +
<.input field={sales_line[:product_id]} /> + <.live_select + field={sales_line[:product_id]} + options={@default_products[Phoenix.HTML.Form.input_value(sales_line, :product_id)]} + inline={true} + update_min_len={0} + phx-focus="set-default" + container_class="inline-container" + text_input_class="inline-text-input" + dropdown_class="inline-dropdown" + label="" + > + <:option :let={opt}> + <.highlight matches={opt.matches} string={opt.label} value={opt.value} /> + + <:inject_adjacent> + <%= if Phoenix.HTML.Form.input_value(sales_line, :product_id) do %> + + <.link navigate={ + TheronsErpWeb.Breadcrumbs.navigate_to_url( + @breadcrumbs, + {"products", Phoenix.HTML.Form.input_value(sales_line, :product_id), ""}, + {"sales_orders", @sales_order.id, @params, ""} + ) + }> + <.icon name="hero-arrow-right" /> + + + <% end %> + + + <.input field={sales_line[:quantity]} type="number" /> <.live_select field={sales_line[:product_id]} - options={@default_products[Phoenix.HTML.Form.input_value(sales_line, :product_id)]} + options={ + @default_products[Phoenix.HTML.Form.input_value(sales_line, :product_id)] || [] + } inline={true} update_min_len={0} phx-focus="set-default" @@ -206,6 +208,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do @impl true def handle_event("validate", %{"sales_order" => sales_order_params} = params, socket) do if sales_order_params["product_id"] == "create" do + # TODO implement create IO.inspect(params) {:noreply, socket} else @@ -242,7 +245,6 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket} {:error, form} -> - IO.inspect(form) {:noreply, assign(socket, form: form)} end end @@ -293,6 +295,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> prepare_matches(text) send_update(LiveSelect.Component, id: id, options: opts) + {:noreply, socket} end From 09cfc54f6cbdf5048a5e155d6f7fe56ed460048a Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 13:38:41 -0500 Subject: [PATCH 04/90] Styling --- assets/css/app.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/css/app.css b/assets/css/app.css index 563cc16..3b17377 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -32,6 +32,9 @@ } .link-to-inside-field { + position: absolute; + margin-top: 22px; + span { position: relative; left: -35px; @@ -70,7 +73,7 @@ .product-category-table { td { - input { + div { width: 100%; } } From 67db501aa6234f391c86dd78560ee1d89ff21536 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 13:42:50 -0500 Subject: [PATCH 05/90] Breadcrumbs for sales orders --- assets/css/app.css | 2 + lib/therons_erp_web/breadcrumbs.ex | 13 +++- .../live/sales_order_live/show.ex | 2 +- lib/therons_erp_web/sales/sales_order.ex | 4 ++ ...10184227_add_serial_no_to_sales_orders.exs | 21 +++++++ .../repo/sales_orders/20250210184227.json | 59 +++++++++++++++++++ 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 priv/repo/migrations/20250210184227_add_serial_no_to_sales_orders.exs create mode 100644 priv/resource_snapshots/repo/sales_orders/20250210184227.json diff --git a/assets/css/app.css b/assets/css/app.css index 3b17377..b7f3815 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -72,6 +72,8 @@ } .product-category-table { + @apply text-left; + td { div { width: 100%; diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index 8410e4d..f179c15 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -123,13 +123,18 @@ defmodule TheronsErpWeb.Breadcrumbs do else ~p"/product_categories/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs)]}" end + + {"sales_orders", id, params, _identifier} -> + if from_args do + ~p"/sales_orders/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), from_args: from_args, params: params]}" + else + ~p"/sales_orders/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), params: params]}" + end end {which, breadcrumbs} end - # Todo navigate back to sales_orders - def navigate_to(socket, part, from) do socket |> Phoenix.LiveView.redirect(to: navigate_to_url(socket.assigns.breadcrumbs, part, from)) @@ -172,6 +177,10 @@ defmodule TheronsErpWeb.Breadcrumbs do "#{name}" end + defp name_for_crumb({"sales_orders", _sale_id, _params, serial_no}) do + "S#{serial_no}" + end + def stream_crumbs(list) when is_list(list) do _stream_crumbs(list) end diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index d4db81e..5adca62 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -69,7 +69,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do TheronsErpWeb.Breadcrumbs.navigate_to_url( @breadcrumbs, {"products", Phoenix.HTML.Form.input_value(sales_line, :product_id), ""}, - {"sales_orders", @sales_order.id, @params, ""} + {"sales_orders", @sales_order.id, @params, @sales_order.identifier} ) }> <.icon name="hero-arrow-right" /> diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index 07acc27..839c1a6 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -32,6 +32,10 @@ defmodule TheronsErp.Sales.SalesOrder do attributes do uuid_primary_key :id + attribute :identifier, :integer do + generated? true + end + timestamps() end diff --git a/priv/repo/migrations/20250210184227_add_serial_no_to_sales_orders.exs b/priv/repo/migrations/20250210184227_add_serial_no_to_sales_orders.exs new file mode 100644 index 0000000..c39dd2b --- /dev/null +++ b/priv/repo/migrations/20250210184227_add_serial_no_to_sales_orders.exs @@ -0,0 +1,21 @@ +defmodule TheronsErp.Repo.Migrations.AddSerialNoToSalesOrders do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:sales_orders) do + add :identifier, :bigserial + end + end + + def down do + alter table(:sales_orders) do + remove :identifier + end + end +end diff --git a/priv/resource_snapshots/repo/sales_orders/20250210184227.json b/priv/resource_snapshots/repo/sales_orders/20250210184227.json new file mode 100644 index 0000000..a6a9708 --- /dev/null +++ b/priv/resource_snapshots/repo/sales_orders/20250210184227.json @@ -0,0 +1,59 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": true, + "primary_key?": false, + "references": null, + "size": null, + "source": "identifier", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "2EE39FF720E03A809844B112863EB5FC6E9187FFDBFAA786C6DA2FF7D00750F4", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "sales_orders" +} \ No newline at end of file From 0dd0d9d9986164be69a59a45915dba515a73d15a Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 13:50:49 -0500 Subject: [PATCH 06/90] Styling --- assets/css/app.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/css/app.css b/assets/css/app.css index b7f3815..d713531 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -78,5 +78,7 @@ div { width: 100%; } + + padding-left: 0; } } From f2c940102ba0cd12b4ed232e9155fb356433a9ca Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 14:13:14 -0500 Subject: [PATCH 07/90] Margin --- .../live/sales_order_live/show.ex | 41 ++++++++++++++----- lib/therons_erp_web/sales/sales_line.ex | 3 ++ lib/therons_erp_web/sales/sales_order.ex | 5 +++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 5adca62..d098486 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -7,7 +7,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do ~H""" <.simple_form for={@form} id="sales_order-form" phx-change="validate" phx-submit="save"> <.header> - Sales order {@sales_order.id} + Sales order {@sales_order.identifier} <%= if @unsaved_changes do %> <.button phx-disable-with="Saving..." class="save-button"> <.icon name="hero-check-circle" /> @@ -19,7 +19,26 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <% end %> <% end %> - <:subtitle>This is a sales_order record from your database. + <:subtitle> + + + {@sales_order.total_cost} + {@sales_order.total_price} + + = + + {if @sales_order.total_cost not in [nil, Money.new(0, :USD)], + do: + (Decimal.mult( + Money.div!(@sales_order.total_price, @sales_order.total_cost.amount).amount, + 100 + ) + |> Decimal.to_string()) <> + "%", + else: "undefined"} + + + <:actions> <%!-- <.link patch={~p"/sales_orders/#{@sales_order}/show/edit"} phx-click={JS.push_focus()}> @@ -172,13 +191,16 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:ok, socket} end + defp load_by_id(id, socket) do + Ash.get!(TheronsErp.Sales.SalesOrder, id, + actor: socket.assigns.current_user, + load: [:total_price, :total_cost, sales_lines: [:total_price, :product]] + ) + end + @impl true def handle_params(%{"id" => id} = params, _, socket) do - sales_order = - Ash.get!(TheronsErp.Sales.SalesOrder, id, - actor: socket.assigns.current_user, - load: [sales_lines: [:total_price, :product]] - ) + sales_order = load_by_id(id, socket) default_products = for line_item <- sales_order.sales_lines, into: %{} do @@ -233,10 +255,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> put_flash(:info, "Sales order #{socket.assigns.form.source.type}d successfully") |> assign( :sales_order, - Ash.get!(TheronsErp.Sales.SalesOrder, socket.assigns.sales_order.id, - actor: socket.assigns.current_user, - load: [:sales_lines] - ) + load_by_id(socket.assigns.sales_order.id, socket) ) |> assign_form() diff --git a/lib/therons_erp_web/sales/sales_line.ex b/lib/therons_erp_web/sales/sales_line.ex index d07ba63..ea86ea8 100644 --- a/lib/therons_erp_web/sales/sales_line.ex +++ b/lib/therons_erp_web/sales/sales_line.ex @@ -48,6 +48,9 @@ defmodule TheronsErp.Sales.SalesLine do calculations do calculate :calculated_total_price, :money, expr(sales_price * quantity) + calculate :active_price, :money, expr(total_price || calculated_total_price) + + calculate :total_cost, :money, expr(unit_price * quantity) # calculate :margin do # end end diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index 839c1a6..64f0e84 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -44,4 +44,9 @@ defmodule TheronsErp.Sales.SalesOrder do destination_attribute :sales_order_id end end + + aggregates do + sum :total_price, [:sales_lines], :active_price + sum :total_cost, [:sales_lines], :total_cost + end end From b641a2773307d0c0740999d5a95525adc3be6bf7 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 14:16:38 -0500 Subject: [PATCH 08/90] Fix margin --- lib/therons_erp_web/live/sales_order_live/show.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index d098486..1385a80 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -33,6 +33,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do Money.div!(@sales_order.total_price, @sales_order.total_cost.amount).amount, 100 ) + |> Decimal.sub(100) |> Decimal.to_string()) <> "%", else: "undefined"} From 56d605ef6cb2669f451cbb06d85497ca79b94e44 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 14:25:09 -0500 Subject: [PATCH 09/90] Add states --- .../live/sales_order_live/show.ex | 1 + lib/therons_erp_web/sales/sales_order.ex | 31 ++++++++- ...20250210192335_states_for_sales_orders.exs | 21 ++++++ .../repo/sales_orders/20250210192335.json | 69 +++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 priv/repo/migrations/20250210192335_states_for_sales_orders.exs create mode 100644 priv/resource_snapshots/repo/sales_orders/20250210192335.json diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 1385a80..c2185ca 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -8,6 +8,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.simple_form for={@form} id="sales_order-form" phx-change="validate" phx-submit="save"> <.header> Sales order {@sales_order.identifier} + {@sales_order.state} <%= if @unsaved_changes do %> <.button phx-disable-with="Saving..." class="save-button"> <.icon name="hero-check-circle" /> diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index 64f0e84..0586b76 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -2,14 +2,43 @@ defmodule TheronsErp.Sales.SalesOrder do use Ash.Resource, otp_app: :therons_erp, domain: TheronsErp.Sales, - data_layer: AshPostgres.DataLayer + data_layer: AshPostgres.DataLayer, + extensions: [AshStateMachine] postgres do table "sales_orders" repo TheronsErp.Repo end + state_machine do + initial_states([:draft]) + default_initial_state(:draft) + + transitions do + transition(:ready, from: :draft, to: [:ready, :cancelled]) + transition(:cancel, from: [:draft, :ready], to: :cancelled) + transition(:revive, from: :cancelled, to: [:draft, :ready]) + transition(:complete, from: [:draft, :ready], to: :complete) + end + end + actions do + update :ready do + change transition_state(:ready) + end + + update :cancel do + change transition_state(:cancelled) + end + + update :revive do + change transition_state(:draft) + end + + update :complete do + change transition_state(:complete) + end + defaults [:read] destroy :destroy do diff --git a/priv/repo/migrations/20250210192335_states_for_sales_orders.exs b/priv/repo/migrations/20250210192335_states_for_sales_orders.exs new file mode 100644 index 0000000..c072140 --- /dev/null +++ b/priv/repo/migrations/20250210192335_states_for_sales_orders.exs @@ -0,0 +1,21 @@ +defmodule TheronsErp.Repo.Migrations.StatesForSalesOrders do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:sales_orders) do + add :state, :text, null: false, default: "draft" + end + end + + def down do + alter table(:sales_orders) do + remove :state + end + end +end diff --git a/priv/resource_snapshots/repo/sales_orders/20250210192335.json b/priv/resource_snapshots/repo/sales_orders/20250210192335.json new file mode 100644 index 0000000..e9255fa --- /dev/null +++ b/priv/resource_snapshots/repo/sales_orders/20250210192335.json @@ -0,0 +1,69 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": true, + "primary_key?": false, + "references": null, + "size": null, + "source": "identifier", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "\"draft\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "D3C7713B02CE137604C629C9F4EC1466E6F4396AD90987A9F4DD3573BCC96211", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "sales_orders" +} \ No newline at end of file From 442d4280cdc74496cdcdcdf683371ce2c17a78b9 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 16:43:53 -0500 Subject: [PATCH 10/90] More styling --- assets/js/app.js | 62 +++++ assets/tailwind.config.js | 29 +++ lib/therons_erp_web/breadcrumbs.ex | 1 - lib/therons_erp_web/components/layouts.ex | 24 ++ .../components/layouts/app.html.heex | 234 +++++++++++++++--- lib/therons_erp_web/live/nav.ex | 25 ++ .../live/sales_order_live/index.ex | 5 +- lib/therons_erp_web/router.ex | 6 +- 8 files changed, 353 insertions(+), 33 deletions(-) create mode 100644 lib/therons_erp_web/live/nav.ex diff --git a/assets/js/app.js b/assets/js/app.js index 955a23c..cbd2d13 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -31,6 +31,68 @@ const hooks = { ...live_select, }; +hooks.Sidebar = { + mounted() { + let menuOpen = false; + + let listeners = (a) => + a.addEventListener("click", () => { + if (!menuOpen) { + menuOpen = true; + document + .getElementById("off-canvas-menu") + .classList.add("translate-x-0"); + + document + .getElementById("off-canvas-menu") + .classList.remove("-translate-x-full"); + + document.getElementById("close-sidebar").classList.add("opacity-100"); + + document + .getElementById("close-sidebar") + .classList.remove("opacity-0"); + + document.getElementById("menu-backdrop").classList.add("opacity-100"); + + document + .getElementById("menu-backdrop") + .classList.remove("opacity-0"); + + document.getElementById("off-canvas").classList.add("z-50"); + + document.getElementById("off-canvas").style.display = "initial"; + } else { + menuOpen = false; + document + .getElementById("off-canvas-menu") + .classList.add("-translate-x-full"); + + document + .getElementById("off-canvas-menu") + .classList.remove("translate-x-0"); + + document.getElementById("close-sidebar").classList.add("opacity-0"); + + document + .getElementById("close-sidebar") + .classList.remove("opacity-100"); + + document.getElementById("menu-backdrop").classList.add("opacity-0"); + + document + .getElementById("menu-backdrop") + .classList.remove("opacity-100"); + document.getElementById("off-canvas").classList.remove("z-50"); + document.getElementById("off-canvas").style.display = "none"; + } + }); + + listeners(document.getElementById("open-sidebar")); + listeners(document.getElementById("close-sidebar")); + }, +}; + let liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: { _csrf_token: csrfToken }, diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index b2f3915..7094061 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -28,6 +28,35 @@ module.exports = { // Options: slate, gray, zinc, neutral, stone gray: colors.gray, + + // Logo based reds + mojo: { + 50: "#fdf4f3", + 100: "#fce7e4", + 200: "#fad4ce", + 300: "#f5b7ac", + 400: "#ee8c7b", + 500: "#e26651", + 600: "#bf432e", + 700: "#ad3b28", + 800: "#8f3425", + 900: "#783024", + 950: "#40160f", + }, + // Logo based greens + eagle: { + 50: "#f6f6f4", + 100: "#e5e6df", + 200: "#ccccbb", + 300: "#b7b7a0", + 400: "#9f9d80", + 500: "#918c6f", + 600: "#7f7860", + 700: "#6b6352", + 800: "#595247", + 900: "#4b453c", + 950: "#29251f", + }, }, }, }, diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index f179c15..4efac15 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -4,7 +4,6 @@ defmodule TheronsErpWeb.Breadcrumbs do def on_mount(:default, params, _session, socket) do socket = assign(socket, :breadcrumbs, decode_breadcrumbs(params["breadcrumbs"])) - socket = assign(socket, :wat, 3) {:cont, socket} end diff --git a/lib/therons_erp_web/components/layouts.ex b/lib/therons_erp_web/components/layouts.ex index 0146d3c..3675f0c 100644 --- a/lib/therons_erp_web/components/layouts.ex +++ b/lib/therons_erp_web/components/layouts.ex @@ -11,4 +11,28 @@ defmodule TheronsErpWeb.Layouts do use TheronsErpWeb, :html embed_templates "layouts/*" + + attr :active, :atom, required: true + attr :name, :atom, required: true + attr :path, :string, required: true + attr :text, :string, required: true + attr :icon, :string, default: nil + attr :todo, :string, default: nil + + def nav_link(%{} = assigns) do + ~H""" + <.link + navigate={@path} + class={(if @active == @name, do: "bg-eagle-100 text-mojo-600", else: "text-eagle-700 hover:text-mojo-600 hover:bg-eagle-100") <> " group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"} + > + <%= if @icon do %> + <.icon name={@icon} /> + <% end %> + {@text} + <%= if @todo && @todo > 0 do %> + + <% end %> + + """ + end end diff --git a/lib/therons_erp_web/components/layouts/app.html.heex b/lib/therons_erp_web/components/layouts/app.html.heex index f3cbbea..d73c99f 100644 --- a/lib/therons_erp_web/components/layouts/app.html.heex +++ b/lib/therons_erp_web/components/layouts/app.html.heex @@ -1,44 +1,218 @@
-
- -
- <.flash_group flash={@flash} /> - {@inner_content} + +
+
+
+ +
+ <.flash_group flash={@flash} /> + {@inner_content} +
+
diff --git a/lib/therons_erp_web/live/nav.ex b/lib/therons_erp_web/live/nav.ex new file mode 100644 index 0000000..2d829e1 --- /dev/null +++ b/lib/therons_erp_web/live/nav.ex @@ -0,0 +1,25 @@ +defmodule TheronsErpWeb.Nav do + use TheronsErpWeb, :live_view + + def on_mount(:default, _params, _session, socket) do + {:cont, + socket + |> attach_hook(:active_tab, :handle_params, &set_active_tab/3)} + end + + defp set_active_tab(_params, _url, socket) do + active_tab = + case {socket.view, socket.assigns.live_action} do + {so, _} when so in [TheronsErpWeb.SalesOrderLive.Index, TheronsErpWeb.SalesOrderLive.Show] -> + :sales_orders + {po, _} when po in [TheronsErpWeb.ProductLive.Index, TheronsErpWeb.ProductLive.Show] -> + :products + # {_, _} -> + {a, b} -> + IO.inspect({a,b}) + nil + end + + {:cont, assign(socket, :active_tab, active_tab)} + end +end diff --git a/lib/therons_erp_web/live/sales_order_live/index.ex b/lib/therons_erp_web/live/sales_order_live/index.ex index 81f8968..f98a9a3 100644 --- a/lib/therons_erp_web/live/sales_order_live/index.ex +++ b/lib/therons_erp_web/live/sales_order_live/index.ex @@ -18,7 +18,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Index do rows={@streams.sales_orders} row_click={fn {_id, sales_order} -> JS.navigate(~p"/sales_orders/#{sales_order}") end} > - <:col :let={{_id, sales_order}} label="Id">{sales_order.id} + <:col :let={{_id, sales_order}} label="Id"> + {sales_order.identifier} + {sales_order.state} + <:action :let={{_id, sales_order}}>
diff --git a/lib/therons_erp_web/router.ex b/lib/therons_erp_web/router.ex index 01b6610..857eca1 100644 --- a/lib/therons_erp_web/router.ex +++ b/lib/therons_erp_web/router.ex @@ -47,7 +47,11 @@ defmodule TheronsErpWeb.Router do sign_out_route AuthController ash_authentication_live_session :authentication_optional, - on_mount: [{TheronsErpWeb.LiveUserAuth, :live_user_optional}, TheronsErpWeb.Breadcrumbs] do + on_mount: [ + {TheronsErpWeb.LiveUserAuth, :live_user_optional}, + TheronsErpWeb.Breadcrumbs, + TheronsErpWeb.Nav + ] do live "/product_categories", ProductCategoryLive.Index, :index live "/product_categories/new", ProductCategoryLive.Index, :new live "/product_categories/:id/edit", ProductCategoryLive.Index, :edit From 976c4b29eb9783c43ad5ad776075b18f9fce7532 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 16:49:57 -0500 Subject: [PATCH 11/90] People resource --- config/config.exs | 8 +- lib/therons_erp/people.ex | 9 ++ lib/therons_erp/people/address.ex | 25 ++++ lib/therons_erp/people/entity.ex | 25 ++++ .../components/layouts/app.html.heex | 22 ++++ .../repo/migrations/20250210214928_people.exs | 56 +++++++++ .../repo/addresses/20250210214929.json | 118 ++++++++++++++++++ .../repo/entities/20250210214929.json | 59 +++++++++ 8 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 lib/therons_erp/people.ex create mode 100644 lib/therons_erp/people/address.ex create mode 100644 lib/therons_erp/people/entity.ex create mode 100644 priv/repo/migrations/20250210214928_people.exs create mode 100644 priv/resource_snapshots/repo/addresses/20250210214929.json create mode 100644 priv/resource_snapshots/repo/entities/20250210214929.json diff --git a/config/config.exs b/config/config.exs index 6db21dc..e7505d0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -57,7 +57,13 @@ config :spark, config :therons_erp, ecto_repos: [TheronsErp.Repo], generators: [timestamp_type: :utc_datetime], - ash_domains: [TheronsErp.Sales, TheronsErp.Inventory, TheronsErp.Ledger, TheronsErp.Accounts] + ash_domains: [ + TheronsErp.People, + TheronsErp.Sales, + TheronsErp.Inventory, + TheronsErp.Ledger, + TheronsErp.Accounts + ] # Configures the endpoint config :therons_erp, TheronsErpWeb.Endpoint, diff --git a/lib/therons_erp/people.ex b/lib/therons_erp/people.ex new file mode 100644 index 0000000..baf2cb4 --- /dev/null +++ b/lib/therons_erp/people.ex @@ -0,0 +1,9 @@ +defmodule TheronsErp.People do + use Ash.Domain, + otp_app: :therons_erp + + resources do + resource TheronsErp.People.Entity + resource TheronsErp.People.Address + end +end diff --git a/lib/therons_erp/people/address.ex b/lib/therons_erp/people/address.ex new file mode 100644 index 0000000..31c9355 --- /dev/null +++ b/lib/therons_erp/people/address.ex @@ -0,0 +1,25 @@ +defmodule TheronsErp.People.Address do + use Ash.Resource, + otp_app: :therons_erp, + domain: TheronsErp.People, + data_layer: AshPostgres.DataLayer + + postgres do + table "addresses" + repo TheronsErp.Repo + end + + attributes do + uuid_primary_key :id + + attribute :address, :string + attribute :city, :string + attribute :state, :string + attribute :zip_code, :string + timestamps() + end + + relationships do + belongs_to :entity, TheronsErp.People.Entity + end +end diff --git a/lib/therons_erp/people/entity.ex b/lib/therons_erp/people/entity.ex new file mode 100644 index 0000000..8bcc25e --- /dev/null +++ b/lib/therons_erp/people/entity.ex @@ -0,0 +1,25 @@ +defmodule TheronsErp.People.Entity do + use Ash.Resource, + otp_app: :therons_erp, + domain: TheronsErp.People, + data_layer: AshPostgres.DataLayer + + postgres do + table "entities" + repo TheronsErp.Repo + end + + attributes do + uuid_primary_key :id + + attribute :name, :string do + allow_nil? false + end + + timestamps() + end + + relationships do + has_many :addresses, TheronsErp.People.Address + end +end diff --git a/lib/therons_erp_web/components/layouts/app.html.heex b/lib/therons_erp_web/components/layouts/app.html.heex index d73c99f..34a5651 100644 --- a/lib/therons_erp_web/components/layouts/app.html.heex +++ b/lib/therons_erp_web/components/layouts/app.html.heex @@ -120,6 +120,17 @@ /> +
    +
  • + <.nav_link + active={@active_tab} + name={:people} + path={~p"/people"} + text="People" + icon="hero-user-circle" + /> +
  • +
@@ -176,6 +187,17 @@ /> +
    +
  • + <.nav_link + active={@active_tab} + name={:people} + path={~p"/people"} + text="People" + icon="hero-user-circle" + /> +
  • +
diff --git a/priv/repo/migrations/20250210214928_people.exs b/priv/repo/migrations/20250210214928_people.exs new file mode 100644 index 0000000..96ffbaa --- /dev/null +++ b/priv/repo/migrations/20250210214928_people.exs @@ -0,0 +1,56 @@ +defmodule TheronsErp.Repo.Migrations.People do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + create table(:entities, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :name, :text, null: false + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + end + + create table(:addresses, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :address, :text + add :city, :text + add :state, :text + add :zip_code, :text + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :entity_id, + references(:entities, + column: :id, + name: "addresses_entity_id_fkey", + type: :uuid, + prefix: "public" + ) + end + end + + def down do + drop constraint(:addresses, "addresses_entity_id_fkey") + + drop table(:addresses) + + drop table(:entities) + end +end diff --git a/priv/resource_snapshots/repo/addresses/20250210214929.json b/priv/resource_snapshots/repo/addresses/20250210214929.json new file mode 100644 index 0000000..516036e --- /dev/null +++ b/priv/resource_snapshots/repo/addresses/20250210214929.json @@ -0,0 +1,118 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "address", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "city", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "zip_code", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "addresses_entity_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "entities" + }, + "size": null, + "source": "entity_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": false, + "hash": "3D21B8882ED6A46DDCFC58B31608F4E737986837F4DB62F83FFB9E67BDCDC85A", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "addresses" +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/entities/20250210214929.json b/priv/resource_snapshots/repo/entities/20250210214929.json new file mode 100644 index 0000000..f154636 --- /dev/null +++ b/priv/resource_snapshots/repo/entities/20250210214929.json @@ -0,0 +1,59 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": false, + "hash": "86265667DE367BC57CECDDDF9F246B940D64A8947187DBA67436A6F0E695B98F", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "entities" +} \ No newline at end of file From 82517f94b2979890f8f0ec35918873e924c7c562 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 17:00:06 -0500 Subject: [PATCH 12/90] Add people routes --- lib/therons_erp/people/address.ex | 18 ++++ lib/therons_erp/people/entity.ex | 20 ++++ .../live/entity_live/form_component.ex | 78 +++++++++++++++ lib/therons_erp_web/live/entity_live/index.ex | 89 +++++++++++++++++ lib/therons_erp_web/live/entity_live/show.ex | 61 ++++++++++++ lib/therons_erp_web/live/nav.ex | 7 +- .../live/sales_order_live/index.ex | 16 ++- lib/therons_erp_web/router.ex | 7 ++ lib/therons_erp_web/sales/sales_order.ex | 2 + ...50210215026_add_people_to_sales_orders.exs | 29 ++++++ .../repo/sales_orders/20250210215026.json | 98 +++++++++++++++++++ 11 files changed, 421 insertions(+), 4 deletions(-) create mode 100644 lib/therons_erp_web/live/entity_live/form_component.ex create mode 100644 lib/therons_erp_web/live/entity_live/index.ex create mode 100644 lib/therons_erp_web/live/entity_live/show.ex create mode 100644 priv/repo/migrations/20250210215026_add_people_to_sales_orders.exs create mode 100644 priv/resource_snapshots/repo/sales_orders/20250210215026.json diff --git a/lib/therons_erp/people/address.ex b/lib/therons_erp/people/address.ex index 31c9355..90f0002 100644 --- a/lib/therons_erp/people/address.ex +++ b/lib/therons_erp/people/address.ex @@ -9,6 +9,24 @@ defmodule TheronsErp.People.Address do repo TheronsErp.Repo end + actions do + defaults [:read] + + create :create do + primary? true + accept [:address, :city, :state, :zip_code] + end + + update :update do + primary? true + accept [:address, :city, :state, :zip_code] + end + + destroy :destroy do + primary? true + end + end + attributes do uuid_primary_key :id diff --git a/lib/therons_erp/people/entity.ex b/lib/therons_erp/people/entity.ex index 8bcc25e..3d5c597 100644 --- a/lib/therons_erp/people/entity.ex +++ b/lib/therons_erp/people/entity.ex @@ -9,6 +9,26 @@ defmodule TheronsErp.People.Entity do repo TheronsErp.Repo end + actions do + defaults [:read] + + create :create do + argument :addresses, {:array, :map} + + change manage_relationship(:addresses, type: :create) + end + + update :update do + require_atomic? false + argument :addresses, {:array, :map} + + change manage_relationship(:addresses, type: :direct_control) + end + + destroy :destroy do + end + end + attributes do uuid_primary_key :id diff --git a/lib/therons_erp_web/live/entity_live/form_component.ex b/lib/therons_erp_web/live/entity_live/form_component.ex new file mode 100644 index 0000000..03ff9f0 --- /dev/null +++ b/lib/therons_erp_web/live/entity_live/form_component.ex @@ -0,0 +1,78 @@ +defmodule TheronsErpWeb.EntityLive.FormComponent do + use TheronsErpWeb, :live_component + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + {@title} + <:subtitle>Use this form to manage entity records in your database. + + + <.simple_form + for={@form} + id="entity-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@form[:addresses]} type="select" multiple label="Addresses" options={[]} /> + + <:actions> + <.button phx-disable-with="Saving...">Save Entity + + +
+ """ + end + + @impl true + def update(assigns, socket) do + {:ok, + socket + |> assign(assigns) + |> assign_form()} + end + + @impl true + def handle_event("validate", %{"entity" => entity_params}, socket) do + {:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, entity_params))} + end + + def handle_event("save", %{"entity" => entity_params}, socket) do + case AshPhoenix.Form.submit(socket.assigns.form, params: entity_params) do + {:ok, entity} -> + notify_parent({:saved, entity}) + + socket = + socket + |> put_flash(:info, "Entity #{socket.assigns.form.source.type}d successfully") + |> push_patch(to: socket.assigns.patch) + + {:noreply, socket} + + {:error, form} -> + {:noreply, assign(socket, form: form)} + end + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) + + defp assign_form(%{assigns: %{entity: entity}} = socket) do + form = + if entity do + AshPhoenix.Form.for_update(entity, :update, + as: "entity", + actor: socket.assigns.current_user + ) + else + AshPhoenix.Form.for_create(TheronsErp.People.Entity, :create, + as: "entity", + actor: socket.assigns.current_user + ) + end + + assign(socket, form: to_form(form)) + end +end diff --git a/lib/therons_erp_web/live/entity_live/index.ex b/lib/therons_erp_web/live/entity_live/index.ex new file mode 100644 index 0000000..ce7ec5e --- /dev/null +++ b/lib/therons_erp_web/live/entity_live/index.ex @@ -0,0 +1,89 @@ +defmodule TheronsErpWeb.EntityLive.Index do + use TheronsErpWeb, :live_view + + @impl true + def render(assigns) do + ~H""" + <.header> + Listing Entities + <:actions> + <.link patch={~p"/entities/new"}> + <.button>New Entity + + + + + <.table + id="entities" + rows={@streams.entities} + row_click={fn {_id, entity} -> JS.navigate(~p"/entities/#{entity}") end} + > + <:col :let={{_id, entity}} label="Id">{entity.id} + + <:action :let={{_id, entity}}> +
+ <.link navigate={~p"/entities/#{entity}"}>Show +
+ + <.link patch={~p"/entities/#{entity}/edit"}>Edit + + + + <.modal + :if={@live_action in [:new, :edit]} + id="entity-modal" + show + on_cancel={JS.patch(~p"/entities")} + > + <.live_component + module={TheronsErpWeb.EntityLive.FormComponent} + id={(@entity && @entity.id) || :new} + title={@page_title} + current_user={@current_user} + action={@live_action} + entity={@entity} + patch={~p"/entities"} + /> + + """ + end + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> stream( + :entities, + Ash.read!(TheronsErp.People.Entity, actor: socket.assigns[:current_user]) + ) + |> assign_new(:current_user, fn -> nil end)} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit Entity") + |> assign(:entity, Ash.get!(TheronsErp.People.Entity, id, actor: socket.assigns.current_user)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New Entity") + |> assign(:entity, nil) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing Entities") + |> assign(:entity, nil) + end + + @impl true + def handle_info({TheronsErpWeb.EntityLive.FormComponent, {:saved, entity}}, socket) do + {:noreply, stream_insert(socket, :entities, entity)} + end +end diff --git a/lib/therons_erp_web/live/entity_live/show.ex b/lib/therons_erp_web/live/entity_live/show.ex new file mode 100644 index 0000000..3b4f559 --- /dev/null +++ b/lib/therons_erp_web/live/entity_live/show.ex @@ -0,0 +1,61 @@ +defmodule TheronsErpWeb.EntityLive.Show do + use TheronsErpWeb, :live_view + + @impl true + def render(assigns) do + ~H""" + <.header> + Entity {@entity.id} + <:subtitle>This is a entity record from your database. + + <:actions> + <.link patch={~p"/entities/#{@entity}/show/edit"} phx-click={JS.push_focus()}> + <.button>Edit entity + + + + + <.list> + <:item title="Id">{@entity.id} + + + <.back navigate={~p"/entities"}>Back to entities + + <.modal + :if={@live_action == :edit} + id="entity-modal" + show + on_cancel={JS.patch(~p"/entities/#{@entity}")} + > + <.live_component + module={TheronsErpWeb.EntityLive.FormComponent} + id={@entity.id} + title={@page_title} + action={@live_action} + current_user={@current_user} + entity={@entity} + patch={~p"/entities/#{@entity}"} + /> + + """ + end + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign( + :entity, + Ash.get!(TheronsErp.People.Entity, id, actor: socket.assigns.current_user) + )} + end + + defp page_title(:show), do: "Show Entity" + defp page_title(:edit), do: "Edit Entity" +end diff --git a/lib/therons_erp_web/live/nav.ex b/lib/therons_erp_web/live/nav.ex index 2d829e1..bfd9590 100644 --- a/lib/therons_erp_web/live/nav.ex +++ b/lib/therons_erp_web/live/nav.ex @@ -12,11 +12,14 @@ defmodule TheronsErpWeb.Nav do case {socket.view, socket.assigns.live_action} do {so, _} when so in [TheronsErpWeb.SalesOrderLive.Index, TheronsErpWeb.SalesOrderLive.Show] -> :sales_orders + {po, _} when po in [TheronsErpWeb.ProductLive.Index, TheronsErpWeb.ProductLive.Show] -> :products - # {_, _} -> + + {pp, _} when pp in [TheronsErpWeb.EntityLive.Index, TheronsErpWeb.EntityLive.Show] -> + :people + {a, b} -> - IO.inspect({a,b}) nil end diff --git a/lib/therons_erp_web/live/sales_order_live/index.ex b/lib/therons_erp_web/live/sales_order_live/index.ex index f98a9a3..434e458 100644 --- a/lib/therons_erp_web/live/sales_order_live/index.ex +++ b/lib/therons_erp_web/live/sales_order_live/index.ex @@ -23,6 +23,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Index do {sales_order.state} + <:col :let={{_id, sales_order}} label="Customer"> + {if sales_order.customer, do: sales_order.customer.name, else: ""} + + <:action :let={{_id, sales_order}}>
<.link navigate={~p"/sales_orders/#{sales_order}"}>Show @@ -60,13 +64,18 @@ defmodule TheronsErpWeb.SalesOrderLive.Index do """ end + @ash_loads [:customer] + @impl true def mount(_params, _session, socket) do {:ok, socket |> stream( :sales_orders, - Ash.read!(TheronsErp.Sales.SalesOrder, actor: socket.assigns[:current_user]) + Ash.read!(TheronsErp.Sales.SalesOrder, + actor: socket.assigns[:current_user], + load: @ash_loads + ) ) |> assign_new(:current_user, fn -> nil end)} end @@ -81,7 +90,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Index do |> assign(:page_title, "Edit Sales order") |> assign( :sales_order, - Ash.get!(TheronsErp.Sales.SalesOrder, id, actor: socket.assigns.current_user) + Ash.get!(TheronsErp.Sales.SalesOrder, id, + actor: socket.assigns.current_user, + load: @ash_loads + ) ) end diff --git a/lib/therons_erp_web/router.ex b/lib/therons_erp_web/router.ex index 857eca1..d7842fb 100644 --- a/lib/therons_erp_web/router.ex +++ b/lib/therons_erp_web/router.ex @@ -72,6 +72,13 @@ defmodule TheronsErpWeb.Router do live "/sales_orders/:id", SalesOrderLive.Show, :show live "/sales_orders/:id/show/edit", SalesOrderLive.Show, :edit + + live "/people", EntityLive.Index, :index + live "/people/new", EntityLive.Index, :new + live "/people/:id/edit", EntityLive.Index, :edit + + live "/people/:id", EntityLive.Show, :show + live "/people/:id/show/edit", EntityLive.Show, :edit end # Remove these if you'd like to use your own authentication views diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index 0586b76..4c2a033 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -72,6 +72,8 @@ defmodule TheronsErp.Sales.SalesOrder do has_many :sales_lines, TheronsErp.Sales.SalesLine do destination_attribute :sales_order_id end + + belongs_to :customer, TheronsErp.People.Entity end aggregates do diff --git a/priv/repo/migrations/20250210215026_add_people_to_sales_orders.exs b/priv/repo/migrations/20250210215026_add_people_to_sales_orders.exs new file mode 100644 index 0000000..2a300cf --- /dev/null +++ b/priv/repo/migrations/20250210215026_add_people_to_sales_orders.exs @@ -0,0 +1,29 @@ +defmodule TheronsErp.Repo.Migrations.AddPeopleToSalesOrders do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:sales_orders) do + add :customer_id, + references(:entities, + column: :id, + name: "sales_orders_customer_id_fkey", + type: :uuid, + prefix: "public" + ) + end + end + + def down do + drop constraint(:sales_orders, "sales_orders_customer_id_fkey") + + alter table(:sales_orders) do + remove :customer_id + end + end +end diff --git a/priv/resource_snapshots/repo/sales_orders/20250210215026.json b/priv/resource_snapshots/repo/sales_orders/20250210215026.json new file mode 100644 index 0000000..1d633af --- /dev/null +++ b/priv/resource_snapshots/repo/sales_orders/20250210215026.json @@ -0,0 +1,98 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": true, + "primary_key?": false, + "references": null, + "size": null, + "source": "identifier", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "sales_orders_customer_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "entities" + }, + "size": null, + "source": "customer_id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "\"draft\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "32CA11C077C34B1CB24A55540C1DED3A9BDA876C23004A73315CFACAFAEBDB75", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "sales_orders" +} \ No newline at end of file From c4791bd0a91fdbc85930daeaed3d458590dfbb4f Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 17:12:30 -0500 Subject: [PATCH 13/90] editing entities --- lib/therons_erp/people/address.ex | 5 +- lib/therons_erp/people/entity.ex | 2 + .../live/entity_live/form_component.ex | 86 +++++++++++- lib/therons_erp_web/live/entity_live/index.ex | 14 +- lib/therons_erp_web/live/entity_live/show.ex | 12 +- lib/therons_erp_web/live/nav.ex | 2 +- .../20250210220244_add_address2.exs | 21 +++ .../repo/addresses/20250210220244.json | 128 ++++++++++++++++++ 8 files changed, 253 insertions(+), 17 deletions(-) create mode 100644 priv/repo/migrations/20250210220244_add_address2.exs create mode 100644 priv/resource_snapshots/repo/addresses/20250210220244.json diff --git a/lib/therons_erp/people/address.ex b/lib/therons_erp/people/address.ex index 90f0002..28f1b78 100644 --- a/lib/therons_erp/people/address.ex +++ b/lib/therons_erp/people/address.ex @@ -14,12 +14,12 @@ defmodule TheronsErp.People.Address do create :create do primary? true - accept [:address, :city, :state, :zip_code] + accept [:address, :address2, :city, :state, :zip_code] end update :update do primary? true - accept [:address, :city, :state, :zip_code] + accept [:address, :address2, :city, :state, :zip_code] end destroy :destroy do @@ -31,6 +31,7 @@ defmodule TheronsErp.People.Address do uuid_primary_key :id attribute :address, :string + attribute :address2, :string attribute :city, :string attribute :state, :string attribute :zip_code, :string diff --git a/lib/therons_erp/people/entity.ex b/lib/therons_erp/people/entity.ex index 3d5c597..b9d0327 100644 --- a/lib/therons_erp/people/entity.ex +++ b/lib/therons_erp/people/entity.ex @@ -14,6 +14,7 @@ defmodule TheronsErp.People.Entity do create :create do argument :addresses, {:array, :map} + accept [:name] change manage_relationship(:addresses, type: :create) end @@ -21,6 +22,7 @@ defmodule TheronsErp.People.Entity do update :update do require_atomic? false argument :addresses, {:array, :map} + accept [:name] change manage_relationship(:addresses, type: :direct_control) end diff --git a/lib/therons_erp_web/live/entity_live/form_component.ex b/lib/therons_erp_web/live/entity_live/form_component.ex index 03ff9f0..7451db3 100644 --- a/lib/therons_erp_web/live/entity_live/form_component.ex +++ b/lib/therons_erp_web/live/entity_live/form_component.ex @@ -17,7 +17,90 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do phx-change="validate" phx-submit="save" > - <.input field={@form[:addresses]} type="select" multiple label="Addresses" options={[]} /> + <.input field={@form[:name]} type="text" label="Name" /> + <.inputs_for :let={address} field={@form[:addresses]}> +
+ + Address {to_string(address.index)} + + + <.input field={address[:address]} type="text" label="Address" /> + <.input field={address[:address2]} type="text" label="Address2" /> + <.input field={address[:city]} type="text" label="City" /> + <.input + field={address[:state]} + type="select" + label="State" + options={[ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DE", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WV", + "WI", + "WY" + ]} + /> + <.input field={address[:zip_code]} type="text" label="Zip Code" pattern="[0-9]{5}" /> +
+ + + <:actions> <.button phx-disable-with="Saving...">Save Entity @@ -53,6 +136,7 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do {:noreply, socket} {:error, form} -> + IO.inspect(form) {:noreply, assign(socket, form: form)} end end diff --git a/lib/therons_erp_web/live/entity_live/index.ex b/lib/therons_erp_web/live/entity_live/index.ex index ce7ec5e..761c1bf 100644 --- a/lib/therons_erp_web/live/entity_live/index.ex +++ b/lib/therons_erp_web/live/entity_live/index.ex @@ -7,7 +7,7 @@ defmodule TheronsErpWeb.EntityLive.Index do <.header> Listing Entities <:actions> - <.link patch={~p"/entities/new"}> + <.link patch={~p"/people/new"}> <.button>New Entity @@ -16,16 +16,16 @@ defmodule TheronsErpWeb.EntityLive.Index do <.table id="entities" rows={@streams.entities} - row_click={fn {_id, entity} -> JS.navigate(~p"/entities/#{entity}") end} + row_click={fn {_id, entity} -> JS.navigate(~p"/people/#{entity}") end} > - <:col :let={{_id, entity}} label="Id">{entity.id} + <:col :let={{_id, entity}} label="Name">{entity.name} <:action :let={{_id, entity}}>
- <.link navigate={~p"/entities/#{entity}"}>Show + <.link navigate={~p"/people/#{entity}"}>Show
- <.link patch={~p"/entities/#{entity}/edit"}>Edit + <.link patch={~p"/people/#{entity}/edit"}>Edit @@ -33,7 +33,7 @@ defmodule TheronsErpWeb.EntityLive.Index do :if={@live_action in [:new, :edit]} id="entity-modal" show - on_cancel={JS.patch(~p"/entities")} + on_cancel={JS.patch(~p"/people")} > <.live_component module={TheronsErpWeb.EntityLive.FormComponent} @@ -42,7 +42,7 @@ defmodule TheronsErpWeb.EntityLive.Index do current_user={@current_user} action={@live_action} entity={@entity} - patch={~p"/entities"} + patch={~p"/people"} /> """ diff --git a/lib/therons_erp_web/live/entity_live/show.ex b/lib/therons_erp_web/live/entity_live/show.ex index 3b4f559..fab9ed6 100644 --- a/lib/therons_erp_web/live/entity_live/show.ex +++ b/lib/therons_erp_web/live/entity_live/show.ex @@ -5,11 +5,11 @@ defmodule TheronsErpWeb.EntityLive.Show do def render(assigns) do ~H""" <.header> - Entity {@entity.id} - <:subtitle>This is a entity record from your database. + Entity {@entity.name} + <:subtitle> <:actions> - <.link patch={~p"/entities/#{@entity}/show/edit"} phx-click={JS.push_focus()}> + <.link patch={~p"/people/#{@entity}/show/edit"} phx-click={JS.push_focus()}> <.button>Edit entity @@ -19,13 +19,13 @@ defmodule TheronsErpWeb.EntityLive.Show do <:item title="Id">{@entity.id} - <.back navigate={~p"/entities"}>Back to entities + <.back navigate={~p"/people"}>Back to entities <.modal :if={@live_action == :edit} id="entity-modal" show - on_cancel={JS.patch(~p"/entities/#{@entity}")} + on_cancel={JS.patch(~p"/people/#{@entity}")} > <.live_component module={TheronsErpWeb.EntityLive.FormComponent} @@ -34,7 +34,7 @@ defmodule TheronsErpWeb.EntityLive.Show do action={@live_action} current_user={@current_user} entity={@entity} - patch={~p"/entities/#{@entity}"} + patch={~p"/people/#{@entity}"} /> """ diff --git a/lib/therons_erp_web/live/nav.ex b/lib/therons_erp_web/live/nav.ex index bfd9590..e5f2257 100644 --- a/lib/therons_erp_web/live/nav.ex +++ b/lib/therons_erp_web/live/nav.ex @@ -19,7 +19,7 @@ defmodule TheronsErpWeb.Nav do {pp, _} when pp in [TheronsErpWeb.EntityLive.Index, TheronsErpWeb.EntityLive.Show] -> :people - {a, b} -> + {_, _} -> nil end diff --git a/priv/repo/migrations/20250210220244_add_address2.exs b/priv/repo/migrations/20250210220244_add_address2.exs new file mode 100644 index 0000000..39ae278 --- /dev/null +++ b/priv/repo/migrations/20250210220244_add_address2.exs @@ -0,0 +1,21 @@ +defmodule TheronsErp.Repo.Migrations.AddAddress2 do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:addresses) do + add :address2, :text + end + end + + def down do + alter table(:addresses) do + remove :address2 + end + end +end diff --git a/priv/resource_snapshots/repo/addresses/20250210220244.json b/priv/resource_snapshots/repo/addresses/20250210220244.json new file mode 100644 index 0000000..94ba399 --- /dev/null +++ b/priv/resource_snapshots/repo/addresses/20250210220244.json @@ -0,0 +1,128 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "address", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "address2", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "city", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "zip_code", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "addresses_entity_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "entities" + }, + "size": null, + "source": "entity_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "77B2E8B507DE79AB658047771A614C3BF0B2AE02973F761C0FEA71AD208ECDCC", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "addresses" +} \ No newline at end of file From 5192970168b6a3061525642bc6ff9d207c2360ac Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 17:13:36 -0500 Subject: [PATCH 14/90] Add sales orders to entities --- lib/therons_erp/people/entity.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/therons_erp/people/entity.ex b/lib/therons_erp/people/entity.ex index b9d0327..e33eae7 100644 --- a/lib/therons_erp/people/entity.ex +++ b/lib/therons_erp/people/entity.ex @@ -43,5 +43,9 @@ defmodule TheronsErp.People.Entity do relationships do has_many :addresses, TheronsErp.People.Address + + has_many :sales_orders, TheronsErp.Sales.SalesOrder do + destination_attribute :customer_id + end end end From 6cce69d85039bbc882aeb2571c2e72c337be104c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 17:14:00 -0500 Subject: [PATCH 15/90] remove unused alias --- lib/therons_erp_web/live/product_live/form_component.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/therons_erp_web/live/product_live/form_component.ex b/lib/therons_erp_web/live/product_live/form_component.ex index 4604f2e..4fbea2d 100644 --- a/lib/therons_erp_web/live/product_live/form_component.ex +++ b/lib/therons_erp_web/live/product_live/form_component.ex @@ -2,7 +2,6 @@ defmodule TheronsErpWeb.ProductLive.FormComponent do use TheronsErpWeb, :live_component alias TheronsErpWeb.Breadcrumbs import TheronsErpWeb.Selects - alias TheronsErpWeb.ProductLive.Show @impl true def render(assigns) do From 740bd3085c75f7a59392f5aa45b9457903c620ba Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 17:56:24 -0500 Subject: [PATCH 16/90] Destroying people --- lib/therons_erp/people/entity.ex | 1 + lib/therons_erp_web/breadcrumbs.ex | 3 ++ .../live/entity_live/form_component.ex | 37 ++++++++++++++++++- lib/therons_erp_web/live/entity_live/index.ex | 1 + lib/therons_erp_web/live/entity_live/show.ex | 2 + .../live/product_live/form_component.ex | 1 - 6 files changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/therons_erp/people/entity.ex b/lib/therons_erp/people/entity.ex index e33eae7..9d2a470 100644 --- a/lib/therons_erp/people/entity.ex +++ b/lib/therons_erp/people/entity.ex @@ -28,6 +28,7 @@ defmodule TheronsErp.People.Entity do end destroy :destroy do + primary? true end end diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index 4efac15..ff444f1 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -87,6 +87,9 @@ defmodule TheronsErpWeb.Breadcrumbs do {"products", "edit", product_id} -> ~p"/products/#{product_id}" + + {"people", entity_id} -> + ~p"/people/#{entity_id}" end {which, []} diff --git a/lib/therons_erp_web/live/entity_live/form_component.ex b/lib/therons_erp_web/live/entity_live/form_component.ex index 7451db3..2356181 100644 --- a/lib/therons_erp_web/live/entity_live/form_component.ex +++ b/lib/therons_erp_web/live/entity_live/form_component.ex @@ -1,5 +1,8 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do use TheronsErpWeb, :live_component + alias TheronsErpWeb.Breadcrumbs + + # TODO add delete button @impl true def render(assigns) do @@ -7,9 +10,22 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do
<.header> {@title} - <:subtitle>Use this form to manage entity records in your database. + <:subtitle> + <%!-- Delete button using POST --%> + <%= if @entity do %> + <.button + phx-click="delete" + phx-value-id={@entity.id} + phx-target={@myself} + data-confirm="Are you sure?" + class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded" + > + Delete + + <% end %> + <.simple_form for={@form} id="entity-form" @@ -131,7 +147,7 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do socket = socket |> put_flash(:info, "Entity #{socket.assigns.form.source.type}d successfully") - |> push_patch(to: socket.assigns.patch) + |> Breadcrumbs.navigate_back({"people", entity.id}, %{customer_id: entity.id}) {:noreply, socket} @@ -141,6 +157,23 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do end end + # Delete event + def handle_event("delete", %{"id" => id}, socket) do + # TODO validate ID + entity = Ash.get!(TheronsErp.People.Entity, id, actor: socket.assigns.current_user) + + case Ash.destroy(entity) do + :ok -> + {:noreply, + socket + |> put_flash(:info, "Entity deleted successfully.") + |> push_navigate(to: ~p"/people")} + + {:error, changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) defp assign_form(%{assigns: %{entity: entity}} = socket) do diff --git a/lib/therons_erp_web/live/entity_live/index.ex b/lib/therons_erp_web/live/entity_live/index.ex index 761c1bf..253c192 100644 --- a/lib/therons_erp_web/live/entity_live/index.ex +++ b/lib/therons_erp_web/live/entity_live/index.ex @@ -43,6 +43,7 @@ defmodule TheronsErpWeb.EntityLive.Index do action={@live_action} entity={@entity} patch={~p"/people"} + breadcrumbs={@breadcrumbs} /> """ diff --git a/lib/therons_erp_web/live/entity_live/show.ex b/lib/therons_erp_web/live/entity_live/show.ex index fab9ed6..af75235 100644 --- a/lib/therons_erp_web/live/entity_live/show.ex +++ b/lib/therons_erp_web/live/entity_live/show.ex @@ -1,4 +1,5 @@ defmodule TheronsErpWeb.EntityLive.Show do + alias Bandit.DelegatingHandler use TheronsErpWeb, :live_view @impl true @@ -35,6 +36,7 @@ defmodule TheronsErpWeb.EntityLive.Show do current_user={@current_user} entity={@entity} patch={~p"/people/#{@entity}"} + breadcrumbs={@breadcrumbs} /> """ diff --git a/lib/therons_erp_web/live/product_live/form_component.ex b/lib/therons_erp_web/live/product_live/form_component.ex index 4fbea2d..9a3884b 100644 --- a/lib/therons_erp_web/live/product_live/form_component.ex +++ b/lib/therons_erp_web/live/product_live/form_component.ex @@ -89,7 +89,6 @@ defmodule TheronsErpWeb.ProductLive.FormComponent do @impl true def handle_event("validate", %{"product" => product_params}, socket) do if product_params["category_id"] == "create" do - # TODO add breadcrumbs pid = if socket.assigns.product, do: socket.assigns.product.id, else: nil {:noreply, From 4d6b6df4eaa25cac00415280fd8cc418de8afff4 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 18:14:30 -0500 Subject: [PATCH 17/90] Styling the pages --- assets/css/app.css | 10 ++ lib/therons_erp/inventory.ex | 1 + lib/therons_erp/inventory/product.ex | 8 ++ .../components/core_components.ex | 2 +- .../live/product_live/index.ex | 3 + lib/therons_erp_web/live/product_live/show.ex | 97 +++++++++---------- .../live/sales_order_live/index.ex | 2 +- lib/therons_erp_web/router.ex | 2 +- 8 files changed, 72 insertions(+), 53 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index d713531..0500f05 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -67,6 +67,7 @@ input { @apply border-transparent hover:border-solid hover:border-zinc-300; + @apply border-b-brand; } } } @@ -82,3 +83,12 @@ padding-left: 0; } } + +.product-name-field { + display: inline-block; + input { + field-sizing: content; + @apply border-transparent hover:border-solid hover:border-zinc-300; + @apply border-b-brand; + } +} diff --git a/lib/therons_erp/inventory.ex b/lib/therons_erp/inventory.ex index 34b0324..8e7e623 100644 --- a/lib/therons_erp/inventory.ex +++ b/lib/therons_erp/inventory.ex @@ -12,6 +12,7 @@ defmodule TheronsErp.Inventory do resource TheronsErp.Inventory.Product do define :create_product, args: [:name, :sales_price], action: :create define :update_product, args: [:name, :sales_price], action: :update + define :create_product_stub, action: :create_stub define :get_products, action: :list end end diff --git a/lib/therons_erp/inventory/product.ex b/lib/therons_erp/inventory/product.ex index c0d055a..ddbd56e 100644 --- a/lib/therons_erp/inventory/product.ex +++ b/lib/therons_erp/inventory/product.ex @@ -19,6 +19,14 @@ defmodule TheronsErp.Inventory.Product do accept [:name, :sales_price, :type, :category_id] end + create :create_stub do + accept [] + + change fn changeset, context -> + Ash.Changeset.change_attribute(changeset, :name, "New Product") + end + end + update :update do accept [:name, :sales_price, :type, :category_id] end diff --git a/lib/therons_erp_web/components/core_components.ex b/lib/therons_erp_web/components/core_components.ex index cd9dbea..0ad38a3 100644 --- a/lib/therons_erp_web/components/core_components.ex +++ b/lib/therons_erp_web/components/core_components.ex @@ -696,7 +696,7 @@ defmodule TheronsErpWeb.CoreComponents do "mt-2 block w-full border-transparent rounded-lg py-[7px] px-[11px] hover:border-solid box-border", "text-zinc-900 focus:outline-solid focus:ring-4 sm:text-sm sm:leading-6", "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5", - "hover:border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5", + "hover:border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5 border-b-brand", @errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10" ] else diff --git a/lib/therons_erp_web/live/product_live/index.ex b/lib/therons_erp_web/live/product_live/index.ex index eb53781..3de4845 100644 --- a/lib/therons_erp_web/live/product_live/index.ex +++ b/lib/therons_erp_web/live/product_live/index.ex @@ -93,9 +93,12 @@ defmodule TheronsErpWeb.ProductLive.Index do end defp apply_action(socket, :new, _params) do + product = TheronsErp.Inventory.create_product_stub!() + socket |> assign(:page_title, "New Product") |> assign(:product, nil) + |> push_navigate(to: ~p"/products/#{product}") end defp apply_action(socket, :index, _params) do diff --git a/lib/therons_erp_web/live/product_live/show.ex b/lib/therons_erp_web/live/product_live/show.ex index 0a06b67..101a3de 100644 --- a/lib/therons_erp_web/live/product_live/show.ex +++ b/lib/therons_erp_web/live/product_live/show.ex @@ -9,63 +9,60 @@ defmodule TheronsErpWeb.ProductLive.Show do ~H""" <.simple_form for={@form} id="product-inline-form" phx-change="validate" phx-submit="save"> <.header> - {@product.name} [{@product.identifier}] + + <.input field={@form[:name]} label="" data-1p-ignore /> + <%= if @unsaved_changes do %> <.button phx-disable-with="Saving..." class="save-button"> <.icon name="hero-check-circle" /> <% end %> - <:subtitle> - - <:actions> - <.link patch={~p"/products/#{@product}/show/edit"} phx-click={JS.push_focus()}> - <.button>Edit product - - - - <%= if @live_action != :edit do %> -
- <.live_select - field={@form[:category_id]} - label="Category" - inline={true} - options={@initial_categories} - update_min_len={0} - phx-focus="set-default" - container_class="inline-container" - text_input_class="inline-text-input" - dropdown_class="inline-dropdown" - > - <:option :let={opt}> - <.highlight matches={opt.matches} string={opt.label} value={opt.value} /> - - <:inject_adjacent> - <%= if Phoenix.HTML.Form.input_value(@form, :category_id) do %> - - <.link navigate={ - TheronsErpWeb.Breadcrumbs.navigate_to_url( - @breadcrumbs, - {"product_category", Phoenix.HTML.Form.input_value(@form, :category_id), - get_category_name( - @categories, - Phoenix.HTML.Form.input_value(@form, :category_id) - )}, - {"products", @product.id, @product.name} - ) - }> - <.icon name="hero-arrow-right" /> - - - <% end %> - - -
- <% end %> + <:subtitle> + [{@product.identifier}] + - <.list> + <%!-- <.list> <:item title="Id">{@product.id} - - + --%> + + <%= if @live_action != :edit do %> +
+ <.live_select + field={@form[:category_id]} + label="Category" + inline={true} + options={@initial_categories} + update_min_len={0} + phx-focus="set-default" + container_class="inline-container" + text_input_class="inline-text-input" + dropdown_class="inline-dropdown" + > + <:option :let={opt}> + <.highlight matches={opt.matches} string={opt.label} value={opt.value} /> + + <:inject_adjacent> + <%= if Phoenix.HTML.Form.input_value(@form, :category_id) do %> + + <.link navigate={ + TheronsErpWeb.Breadcrumbs.navigate_to_url( + @breadcrumbs, + {"product_category", Phoenix.HTML.Form.input_value(@form, :category_id), + get_category_name( + @categories, + Phoenix.HTML.Form.input_value(@form, :category_id) + )}, + {"products", @product.id, @product.name} + ) + }> + <.icon name="hero-arrow-right" /> + + + <% end %> + + +
+ <% end %> <.back navigate={~p"/products"}>Back to products <.modal diff --git a/lib/therons_erp_web/live/sales_order_live/index.ex b/lib/therons_erp_web/live/sales_order_live/index.ex index 434e458..ccd3c96 100644 --- a/lib/therons_erp_web/live/sales_order_live/index.ex +++ b/lib/therons_erp_web/live/sales_order_live/index.ex @@ -103,7 +103,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Index do socket |> assign(:page_title, "New Sales order") |> assign(:sales_order, nil) - |> push_redirect(to: ~p"/sales_orders/#{sales_order}") + |> push_navigate(to: ~p"/sales_orders/#{sales_order}") end defp apply_action(socket, :index, _params) do diff --git a/lib/therons_erp_web/router.ex b/lib/therons_erp_web/router.ex index d7842fb..4cc0f90 100644 --- a/lib/therons_erp_web/router.ex +++ b/lib/therons_erp_web/router.ex @@ -67,7 +67,7 @@ defmodule TheronsErpWeb.Router do live "/products/:id/show/edit", ProductLive.Show, :edit live "/sales_orders", SalesOrderLive.Index, :index - live "/sales_orders/new", SalesOrderLive.Index, :new + # live "/sales_orders/new", SalesOrderLive.Index, :new live "/sales_orders/:id/edit", SalesOrderLive.Index, :edit live "/sales_orders/:id", SalesOrderLive.Show, :show From a5775449cfac1b647786f001644bcfacced1ae6f Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 19:31:53 -0500 Subject: [PATCH 18/90] Checkpoint --- lib/therons_erp_web/breadcrumbs.ex | 8 +- .../live/product_live/index.ex | 7 +- lib/therons_erp_web/live/product_live/show.ex | 16 +++- .../live/sales_order_live/show.ex | 88 +++++++++++++------ lib/therons_erp_web/sales/sales_line.ex | 1 + lib/therons_erp_web/selects.ex | 6 +- 6 files changed, 91 insertions(+), 35 deletions(-) diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index ff444f1..6fb10bb 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -128,9 +128,9 @@ defmodule TheronsErpWeb.Breadcrumbs do {"sales_orders", id, params, _identifier} -> if from_args do - ~p"/sales_orders/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), from_args: from_args, params: params]}" + ~p"/sales_orders/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), from_args: from_args, args: params]}" else - ~p"/sales_orders/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), params: params]}" + ~p"/sales_orders/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), args: params]}" end end @@ -158,6 +158,10 @@ defmodule TheronsErpWeb.Breadcrumbs do ~p"/product_categories/#{id}?#{[breadcrumbs: append_and_encode(breadcrumbs, from)]}" end + def navigate_to_url(breadcrumbs, {"products", "new", line_id}, from) do + ~p"/products/new?#{[breadcrumbs: append_and_encode(breadcrumbs, from), line_id: line_id]}" + end + def navigate_to_url(breadcrumbs, {"products", id, _name}, from) do ~p"/products/#{id}?#{[breadcrumbs: append_and_encode(breadcrumbs, from)]}" end diff --git a/lib/therons_erp_web/live/product_live/index.ex b/lib/therons_erp_web/live/product_live/index.ex index 3de4845..31d101a 100644 --- a/lib/therons_erp_web/live/product_live/index.ex +++ b/lib/therons_erp_web/live/product_live/index.ex @@ -92,13 +92,16 @@ defmodule TheronsErpWeb.ProductLive.Index do ) end - defp apply_action(socket, :new, _params) do + defp apply_action(socket, :new, params) do product = TheronsErp.Inventory.create_product_stub!() socket |> assign(:page_title, "New Product") |> assign(:product, nil) - |> push_navigate(to: ~p"/products/#{product}") + |> push_navigate( + to: + ~p"/products/#{product}?#{[breadcrumbs: params["breadcrumbs"], line_id: params["line_id"]]}" + ) end defp apply_action(socket, :index, _params) do diff --git a/lib/therons_erp_web/live/product_live/show.ex b/lib/therons_erp_web/live/product_live/show.ex index 101a3de..a9d29f6 100644 --- a/lib/therons_erp_web/live/product_live/show.ex +++ b/lib/therons_erp_web/live/product_live/show.ex @@ -12,10 +12,16 @@ defmodule TheronsErpWeb.ProductLive.Show do <.input field={@form[:name]} label="" data-1p-ignore /> - <%= if @unsaved_changes do %> + <%= if @line_id do %> <.button phx-disable-with="Saving..." class="save-button"> - <.icon name="hero-check-circle" /> + <.icon name="hero-check-circle" /> Return to Sales Order + <% else %> + <%= if @unsaved_changes do %> + <.button phx-disable-with="Saving..." class="save-button"> + <.icon name="hero-check-circle" /> + + <% end %> <% end %> <:subtitle> [{@product.identifier}] @@ -112,6 +118,7 @@ defmodule TheronsErpWeb.ProductLive.Show do |> assign(:from_args, params["from_args"]) |> assign(:set_category, %{text: nil, value: nil}) |> assign(:unsaved_changes, false) + |> assign(:line_id, params["line_id"]) |> assign_form()} end @@ -191,7 +198,10 @@ defmodule TheronsErpWeb.ProductLive.Show do socket = socket |> put_flash(:info, "Product #{socket.assigns.form.source.type}d successfully") - |> Breadcrumbs.navigate_back({"products", "edit", product.id}) + |> Breadcrumbs.navigate_back({"products", "edit", product.id}, %{ + line_id: socket.assigns.line_id, + product_id: product.id + }) {:noreply, socket} diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index c2185ca..f620862 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -1,4 +1,5 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do + alias TheronsErpWeb.Breadcrumbs use TheronsErpWeb, :live_view import TheronsErpWeb.Selects @@ -42,16 +43,12 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do - <:actions> - <%!-- <.link patch={~p"/sales_orders/#{@sales_order}/show/edit"} phx-click={JS.push_focus()}> - <.button>Edit sales_order - --%> - + <:actions> - <.list> + <%!-- <.list> <:item title="Id">{@sales_order.id} - + --%> @@ -64,7 +61,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do - <.inputs_for :let={sales_line} field={@form[:sales_lines]}> + <.inputs_for :let={sales_line} field={IO.inspect(@form[:sales_lines])}> @@ -196,7 +190,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do defp load_by_id(id, socket) do Ash.get!(TheronsErp.Sales.SalesOrder, id, actor: socket.assigns.current_user, - load: [:total_price, :total_cost, sales_lines: [:total_price, :product]] + load: [ + :total_price, + :total_cost, + sales_lines: [:total_price, :product, :active_price, :calculated_total_price] + ] ) end @@ -231,10 +229,27 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do @impl true def handle_event("validate", %{"sales_order" => sales_order_params} = params, socket) do - if sales_order_params["product_id"] == "create" do - # TODO implement create - IO.inspect(params) - {:noreply, socket} + output = + (sales_order_params["sales_lines"] || []) + |> Enum.map(fn {id, val} -> + {id, val["product_id"]} + end) + |> Enum.find(fn {_id, val} -> val == "create" end) + + has_create = + case output do + {id, "create"} -> id + _ -> nil + end + + if has_create do + {:noreply, + socket + |> Breadcrumbs.navigate_to( + {"products", "new", has_create}, + {"sales_orders", socket.assigns.sales_order.id, params, + socket.assigns.sales_order.identifier} + )} else form = AshPhoenix.Form.validate(socket.assigns.form, sales_order_params) drop = length(sales_order_params["_drop_sales_lines"] || []) @@ -288,12 +303,35 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do form = case {args, from_args} do - {nil, nil} -> form - {_, nil} -> AshPhoenix.Form.validate(form, args) - {nil, _} -> AshPhoenix.Form.validate(form, from_args) - _ -> AshPhoenix.Form.validate(form, Map.merge(args, from_args)) + {nil, nil} -> + form + + {_, nil} -> + AshPhoenix.Form.validate(form, args) + + {nil, _} -> + new_args = + put_in( + from_args, + ["sales_order", "sales_lines", from_args["line_id"], "product_id"], + from_args["product_id"] + ) + + AshPhoenix.Form.validate(form, new_args) + + _ -> + new_args = + put_in( + Map.merge(args, from_args), + ["sales_order", "sales_lines", from_args["line_id"], "product_id"], + from_args["product_id"] + ) + + AshPhoenix.Form.validate(form, new_args) end + # AshPhoenix.Form.params(form) |> IO.inspect() + socket |> assign(form: to_form(form)) |> assign(:unsaved_changes, form.changed?) @@ -345,9 +383,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> Phoenix.HTML.Form.input_value(:product_id) if value not in [nil, ""] do - products = get_products(value) - text = Enum.find(products, &(&1.value == value)).label - opts = prepare_matches(products, text) + opts = get_initial_product_options(value) send_update(LiveSelect.Component, options: opts, @@ -355,10 +391,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do value: value ) else - products = get_products("") + opts = get_initial_product_options(nil) send_update(LiveSelect.Component, - options: products, + options: opts, id: id, value: nil ) diff --git a/lib/therons_erp_web/sales/sales_line.ex b/lib/therons_erp_web/sales/sales_line.ex index ea86ea8..5aea018 100644 --- a/lib/therons_erp_web/sales/sales_line.ex +++ b/lib/therons_erp_web/sales/sales_line.ex @@ -18,6 +18,7 @@ defmodule TheronsErp.Sales.SalesLine do end update :update do + require_atomic? false primary? true accept [:sales_price, :unit_price, :quantity, :product_id] end diff --git a/lib/therons_erp_web/selects.ex b/lib/therons_erp_web/selects.ex index 0f07198..7f8e0fa 100644 --- a/lib/therons_erp_web/selects.ex +++ b/lib/therons_erp_web/selects.ex @@ -62,11 +62,13 @@ defmodule TheronsErpWeb.Selects do end def get_initial_options(selected) do - (get_categories(selected) ++ additional_options()) |> Enum.uniq() |> Enum.take(5) + (get_categories(selected) |> Enum.uniq() |> Enum.take(4)) ++ additional_options() end def get_initial_product_options(selected) do - (get_products(selected) ++ additional_product_options()) |> Enum.uniq() |> Enum.take(5) + (get_products(selected) + |> Enum.uniq() + |> Enum.take(4)) ++ additional_product_options() end def get_category_name(categories, id) do From 69160bdc4e52247bb32eb3bf2709f323cde66bac Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 19:54:48 -0500 Subject: [PATCH 19/90] Show values in ready state --- lib/therons_erp_web/components/layouts.ex | 29 +++ .../live/sales_order_live/index.ex | 3 +- .../live/sales_order_live/show.ex | 189 ++++++++++-------- lib/therons_erp_web/sales/sales_order.ex | 8 +- 4 files changed, 147 insertions(+), 82 deletions(-) diff --git a/lib/therons_erp_web/components/layouts.ex b/lib/therons_erp_web/components/layouts.ex index 3675f0c..d75960a 100644 --- a/lib/therons_erp_web/components/layouts.ex +++ b/lib/therons_erp_web/components/layouts.ex @@ -35,4 +35,33 @@ defmodule TheronsErpWeb.Layouts do """ end + + def status_badge(assigns) do + case assigns.state do + :draft -> + ~H""" + Draft + """ + + :ready -> + ~H""" + Ready + """ + + :sent -> + ~H""" + Sent + """ + + :canceled -> + ~H""" + Canceled + """ + + _ -> + ~H""" + {assigns.state} + """ + end + end end diff --git a/lib/therons_erp_web/live/sales_order_live/index.ex b/lib/therons_erp_web/live/sales_order_live/index.ex index ccd3c96..1056c87 100644 --- a/lib/therons_erp_web/live/sales_order_live/index.ex +++ b/lib/therons_erp_web/live/sales_order_live/index.ex @@ -1,5 +1,6 @@ defmodule TheronsErpWeb.SalesOrderLive.Index do use TheronsErpWeb, :live_view + import TheronsErpWeb.Layouts @impl true def render(assigns) do @@ -20,7 +21,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Index do > <:col :let={{_id, sales_order}} label="Id"> {sales_order.identifier} - {sales_order.state} + <.status_badge state={sales_order.state} /> <:col :let={{_id, sales_order}} label="Customer"> diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index f620862..1acb96f 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -2,6 +2,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do alias TheronsErpWeb.Breadcrumbs use TheronsErpWeb, :live_view import TheronsErpWeb.Selects + import TheronsErpWeb.Layouts @impl true def render(assigns) do @@ -9,7 +10,12 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.simple_form for={@form} id="sales_order-form" phx-change="validate" phx-submit="save"> <.header> Sales order {@sales_order.identifier} - {@sales_order.state} + <.status_badge state={@sales_order.state} /> + <%= if @sales_order.state == :draft and not @unsaved_changes do %> + <.button phx-disable-with="Saving..." phx-click="set-ready"> + Ready + + <% end %> <%= if @unsaved_changes do %> <.button phx-disable-with="Saving..." class="save-button"> <.icon name="hero-check-circle" /> @@ -46,10 +52,6 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <:actions> - <%!-- <.list> - <:item title="Id">{@sales_order.id} - --%> -
<.live_select @@ -120,10 +117,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.input field={sales_line[:total_price]} - value={ - IO.inspect(do_money(sales_line[:total_price])) || - do_money(sales_line[:calculated_total]) - } + value={do_money(sales_line[:active_price])} type="number" />
@@ -61,80 +63,104 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do - <.inputs_for :let={sales_line} field={IO.inspect(@form[:sales_lines])}> - - - - - - - + + + - - + + + + + + + <% else %> + <%= for sales_line <- IO.inspect(@sales_order.sales_lines) do %> + + what + + + + + + + <% end %> + <% end %>
- <.live_select - field={sales_line[:product_id]} - options={ - @default_products[Phoenix.HTML.Form.input_value(sales_line, :product_id)] || [] - } - inline={true} - update_min_len={0} - phx-focus="set-default" - container_class="inline-container" - text_input_class="inline-text-input" - dropdown_class="inline-dropdown" - label="" - > - <:option :let={opt}> - <.highlight matches={opt.matches} string={opt.label} value={opt.value} /> - - <:inject_adjacent> - <%= if Phoenix.HTML.Form.input_value(sales_line, :product_id) do %> - - <.link navigate={ - TheronsErpWeb.Breadcrumbs.navigate_to_url( - @breadcrumbs, - {"products", Phoenix.HTML.Form.input_value(sales_line, :product_id), ""}, - {"sales_orders", @sales_order.id, @params, @sales_order.identifier} - ) - }> - <.icon name="hero-arrow-right" /> - - - <% end %> - - - - <.input field={sales_line[:quantity]} type="number" /> - - <.input - field={sales_line[:sales_price]} - value={do_money(sales_line[:sales_price])} - type="number" - /> - - <.input - field={sales_line[:unit_price]} - value={do_money(sales_line[:unit_price])} - type="number" - /> - - <.input - field={sales_line[:total_price]} - value={do_money(sales_line[:active_price])} - type="number" - /> - -
+ <.live_select + field={sales_line[:product_id]} + options={ + @default_products[Phoenix.HTML.Form.input_value(sales_line, :product_id)] || [] + } + inline={true} + update_min_len={0} + phx-focus="set-default" + container_class="inline-container" + text_input_class="inline-text-input" + dropdown_class="inline-dropdown" + label="" + > + <:option :let={opt}> + <.highlight matches={opt.matches} string={opt.label} value={opt.value} /> + + <:inject_adjacent> + <%= if Phoenix.HTML.Form.input_value(sales_line, :product_id) do %> + + <.link navigate={ + TheronsErpWeb.Breadcrumbs.navigate_to_url( + @breadcrumbs, + {"products", Phoenix.HTML.Form.input_value(sales_line, :product_id), + ""}, + {"sales_orders", @sales_order.id, @params, @sales_order.identifier} + ) + }> + <.icon name="hero-arrow-right" /> + + + <% end %> + + + + <.input field={sales_line[:quantity]} type="number" /> + + <.input + field={sales_line[:sales_price]} + value={do_money(sales_line[:sales_price])} + type="number" /> - - <.icon name="hero-x-mark" /> - -
+ <.input + field={sales_line[:unit_price]} + value={do_money(sales_line[:unit_price])} + type="number" + /> + + <.input + field={sales_line[:total_price]} + value={do_money(sales_line[:active_price])} + type="number" + /> + + +
+ {sales_line.product.name} + + {sales_line.quantity} + + {sales_line.sales_price.amount |> Decimal.to_float()} + + {sales_line.unit_price.amount |> Decimal.to_float()} + + {sales_line.active_price.amount |> Decimal.to_float()} +
@@ -193,7 +219,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do load: [ :total_price, :total_cost, - sales_lines: [:total_price, :product, :active_price, :calculated_total_price] + sales_lines: [:total_price, :product, :active_price, :calculated_total_price, :total_cost] ] ) end @@ -285,6 +311,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end end + def handle_event("set-ready", _, socket) do + Ash.Changeset.for_update(socket.assigns.sales_order, :ready) |> Ash.update!() + {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} + end + defp assign_form( %{assigns: %{sales_order: sales_order, args: args, from_args: from_args}} = socket ) do diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index 4c2a033..ca15129 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -39,7 +39,10 @@ defmodule TheronsErp.Sales.SalesOrder do change transition_state(:complete) end - defaults [:read] + read :read do + primary? true + prepare build(sort: [identifier: :desc]) + end destroy :destroy do end @@ -54,7 +57,8 @@ defmodule TheronsErp.Sales.SalesOrder do require_atomic? false argument :sales_lines, {:array, :map} - change manage_relationship(:sales_lines, type: :direct_control) + change manage_relationship(:sales_lines, type: :direct_control), + where: [attribute_equals(:state, :draft)] end end From 34972e7ff7f670f4c8973a76dc1f81065674d455 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 19:55:31 -0500 Subject: [PATCH 20/90] Remove button on ready stte --- lib/therons_erp_web/live/sales_order_live/show.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 1acb96f..d25d9e4 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -164,10 +164,12 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do
- + <%= if @sales_order.state == :draft do %> + + <% end %> <.back navigate={~p"/sales_orders"}>Back to sales_orders From b9a3188de62183193c1c960d7f3ba6fe0193b232 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 19:59:50 -0500 Subject: [PATCH 21/90] don't allow changes when not draft --- lib/therons_erp_web/sales/sales_order.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index ca15129..63c771f 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -50,7 +50,8 @@ defmodule TheronsErp.Sales.SalesOrder do create :create do argument :sales_lines, {:array, :map} - change manage_relationship(:sales_lines, type: :create) + change manage_relationship(:sales_lines, type: :create), + where: [attribute_equals(:state, :draft)] end update :update do From f1c3cbe6245e08472a5cd689cf172b7e0f70b0f3 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 20:02:16 -0500 Subject: [PATCH 22/90] Return to draft --- .../live/sales_order_live/show.ex | 45 ++++++++++++------- lib/therons_erp_web/sales/sales_order.ex | 2 +- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index d25d9e4..ec91693 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -11,22 +11,30 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.header> Sales order {@sales_order.identifier} <.status_badge state={@sales_order.state} /> - <%= if @sales_order.state == :draft and not @unsaved_changes do %> - <.button phx-disable-with="Saving..." phx-click="set-ready"> - Ready - - <% end %> - <%= if @unsaved_changes do %> - <.button phx-disable-with="Saving..." class="save-button"> - <.icon name="hero-check-circle" /> - - - <%= if @drop_sales > 0 do %> - - Delete {@drop_sales} items. - + + <:actions> + <%= if @sales_order.state == :draft and not @unsaved_changes do %> + <.button phx-disable-with="Saving..." phx-click="set-ready"> + Ready + + <% end %> + <%= if @sales_order.state == :ready do %> + <.button phx-disable-with="Saving..." phx-click="set-draft"> + Return to draft + + <% end %> + <%= if @unsaved_changes do %> + <.button phx-disable-with="Saving..." class="save-button"> + <.icon name="hero-check-circle" /> + + + <%= if @drop_sales > 0 do %> + + Delete {@drop_sales} items. + + <% end %> <% end %> - <% end %> + <:subtitle> @@ -48,8 +56,6 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do - - <:actions> @@ -318,6 +324,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} end + def handle_event("set-draft", _, socket) do + Ash.Changeset.for_update(socket.assigns.sales_order, :revive) |> Ash.update!() + {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} + end + defp assign_form( %{assigns: %{sales_order: sales_order, args: args, from_args: from_args}} = socket ) do diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index 63c771f..c835190 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -17,7 +17,7 @@ defmodule TheronsErp.Sales.SalesOrder do transitions do transition(:ready, from: :draft, to: [:ready, :cancelled]) transition(:cancel, from: [:draft, :ready], to: :cancelled) - transition(:revive, from: :cancelled, to: [:draft, :ready]) + transition(:revive, from: [:cancelled, :ready], to: [:draft, :ready]) transition(:complete, from: [:draft, :ready], to: :complete) end end From fc0a218a7597f746c9bf26f864c2975e26399599 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 20:04:01 -0500 Subject: [PATCH 23/90] Remove inspect --- lib/therons_erp_web/live/sales_order_live/show.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index ec91693..7003be6 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -146,7 +146,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <% else %> - <%= for sales_line <- IO.inspect(@sales_order.sales_lines) do %> + <%= for sales_line <- @sales_order.sales_lines do %> what + @@ -329,6 +336,29 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} end + defp active_price_for_sales_line(sales_line) do + total_price = + Phoenix.HTML.Form.input_value(sales_line, :total_price) + |> case do + "" -> nil + el -> el + end + + sales_price = Phoenix.HTML.Form.input_value(sales_line, :sales_price) + quantity = Phoenix.HTML.Form.input_value(sales_line, :quantity) + + total_price || + Money.mult!(sales_price, quantity) + |> Money.to_decimal() + |> Decimal.to_string() + end + + defp total_cost_for_sales_line(sales_line) do + unit_price = Phoenix.HTML.Form.input_value(sales_line, :unit_price) + quantity = Phoenix.HTML.Form.input_value(sales_line, :quantity) + Money.mult!(unit_price, quantity) + end + defp assign_form( %{assigns: %{sales_order: sales_order, args: args, from_args: from_args}} = socket ) do From addf2b2e56201a91300459c9db542b9f24000d6a Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 20:51:17 -0500 Subject: [PATCH 27/90] Proper passing of new values --- .../live/sales_order_live/show.ex | 63 ++++++++++++++--- .../live/sales_orders_live.exs | 70 ------------------- 2 files changed, 52 insertions(+), 81 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 77d7d03..3c974d9 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -288,7 +288,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do socket |> Breadcrumbs.navigate_to( {"products", "new", has_create}, - {"sales_orders", socket.assigns.sales_order.id, params, + {"sales_orders", socket.assigns.sales_order.id, sales_order_params, socket.assigns.sales_order.identifier} )} else @@ -347,16 +347,41 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do sales_price = Phoenix.HTML.Form.input_value(sales_line, :sales_price) quantity = Phoenix.HTML.Form.input_value(sales_line, :quantity) - total_price || - Money.mult!(sales_price, quantity) - |> Money.to_decimal() - |> Decimal.to_string() + case {sales_price, quantity} do + {nil, nil} -> + "" + + {_, nil} -> + "" + + {nil, _} -> + "" + + {_, _} -> + total_price || + Money.mult!(sales_price, quantity) + |> Money.to_decimal() + |> Decimal.to_string() + end end defp total_cost_for_sales_line(sales_line) do unit_price = Phoenix.HTML.Form.input_value(sales_line, :unit_price) quantity = Phoenix.HTML.Form.input_value(sales_line, :quantity) - Money.mult!(unit_price, quantity) + + case {unit_price, quantity} do + {nil, nil} -> + "" + + {_, nil} -> + "" + + {nil, _} -> + "" + + {_, _} -> + Money.mult!(unit_price, quantity) + end end defp assign_form( @@ -387,7 +412,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do new_args = put_in( from_args, - ["sales_order", "sales_lines", from_args["line_id"], "product_id"], + ["sales_lines", from_args["line_id"], "product_id"], from_args["product_id"] ) @@ -397,14 +422,28 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do new_args = put_in( Map.merge(args, from_args), - ["sales_order", "sales_lines", from_args["line_id"], "product_id"], + ["sales_lines", from_args["line_id"], "product_id"], from_args["product_id"] ) AshPhoenix.Form.validate(form, new_args) end - # AshPhoenix.Form.params(form) |> IO.inspect() + pid = + from_args["product_id"] + + if pid not in [nil, ""] do + opts = get_initial_product_options(pid) + + id = + "sales_order[sales_lines][#{from_args["line_id"]}]_product_id_live_select_component" + + send_update(LiveSelect.Component, + options: opts, + id: id, + value: pid + ) + end socket |> assign(form: to_form(form)) @@ -440,8 +479,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do ) do number = parse_select_id!(id) - if pid = - socket.assigns.from_args["product_id"] && socket.assigns.from_args["line_id"] == number do + pid = socket.assigns.from_args["product_id"] + + if socket.assigns.from_args["product_id"] && + socket.assigns.from_args["line_id"] == to_string(number) do opts = get_initial_product_options(pid) send_update(LiveSelect.Component, diff --git a/test/therons_erp_web/live/sales_orders_live.exs b/test/therons_erp_web/live/sales_orders_live.exs index a17d9f4..b4162ee 100644 --- a/test/therons_erp_web/live/sales_orders_live.exs +++ b/test/therons_erp_web/live/sales_orders_live.exs @@ -1,73 +1,3 @@ defmodule TheronsErpWeb.SalesOrdersLiveTest do use TheronsErpWeb.ConnCase - - @new_args %{ "_target" => ["sales_order", "sales_lines", "2", "product_id"], - "line_id" => "2", - "product_id" => "fa68f48a-a852-49fd-a80e-41592e9922a3", - "sales_order" => %{ - "sales_lines" => %{ - "0" => %{ - "_form_type" => "update", - "_persistent_id" => "0", - "_touched" => "_form_type,_persistent_id,_touched,_unused_product_id_text_input,_unused_quantity,_unused_sales_price,_unused_total_price,_unused_unit_price,id,product_id,product_id_text_input,quantity,sales_price,total_price,unit_price", - "_unused_product_id_text_input" => "", - "_unused_quantity" => "", - "_unused_sales_price" => "", - "_unused_total_price" => "", - "_unused_unit_price" => "", - "id" => "ceb0aedf-6ef4-4497-b820-d43aad073750", - "product_id" => "961be52a-0ad5-4fdb-be76-0c86fdbcd4e4", - "product_id_text_input" => "abc123", - "quantity" => "2", - "sales_price" => "3.0", - "total_price" => "6.0", - "unit_price" => "6.0" - }, - "1" => %{ - "_form_type" => "update", - "_persistent_id" => "1", - "_touched" => "_form_type,_persistent_id,_touched,_unused_product_id_text_input,_unused_quantity,_unused_sales_price,_unused_total_price,_unused_unit_price,id,product_id,product_id_text_input,quantity,sales_price,total_price,unit_price", - "_unused_product_id_text_input" => "", - "_unused_quantity" => "", - "_unused_sales_price" => "", - "_unused_total_price" => "", - "_unused_unit_price" => "", - "id" => "cfdf8f66-0cf5-41cb-97be-ea8a8b308fc1", - "product_id" => "d3d5df6a-1829-40db-a054-64b48b6fc512", - "product_id_text_input" => "Bob The Builder 2", - "quantity" => "2", - "sales_price" => "3.0", - "total_price" => "6.0", - "unit_price" => "4.0" - }, - "2" => %{ - "_form_type" => "create", - "_persistent_id" => "2", - "_touched" => "_form_type,_persistent_id,_touched,_unused_product_id_text_input,_unused_quantity,_unused_sales_price,_unused_total_price,_unused_unit_price,product_id,product_id_text_input,quantity,sales_price,total_price,unit_price", - "_unused_product_id_text_input" => "", - "_unused_quantity" => "", - "_unused_sales_price" => "", - "_unused_total_price" => "", - "_unused_unit_price" => "", - "product_id" => "fa68f48a-a852-49fd-a80e-41592e9922a3", - "product_id_text_input" => "Create New", - "quantity" => "", - "sales_price" => "", - "total_price" => "", - "unit_price" => "" - } - } - } - } - - test "children from froms" do - sales_order = Ash.create!(TheronsErp.Sales.SalesOrder) - - form = AshPhoenix.Form.for_update(sales_order, :update, - as: "sales_order") - - form = AshPhoenix.Form.validate(form, @new_args) - IO.inspect(form) - assert length(form.forms.sales_lines) == 3 - end end From 32f41c4cf2e54c56514c451966356b38a82ea715 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 20:54:08 -0500 Subject: [PATCH 28/90] Load all initial products --- .../live/sales_order_live/show.ex | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 3c974d9..cc6a890 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -416,6 +416,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do from_args["product_id"] ) + update_live_forms(new_args) AshPhoenix.Form.validate(form, new_args) _ -> @@ -426,28 +427,33 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do from_args["product_id"] ) + update_live_forms(new_args) AshPhoenix.Form.validate(form, new_args) end - pid = - from_args["product_id"] + socket + |> assign(form: to_form(form)) + |> assign(:unsaved_changes, form.changed?) + end - if pid not in [nil, ""] do - opts = get_initial_product_options(pid) + defp update_live_forms(new_args) do + for {line_no, line} <- new_args["sales_lines"] do + pid = + line["product_id"] - id = - "sales_order[sales_lines][#{from_args["line_id"]}]_product_id_live_select_component" + if pid not in [nil, ""] do + opts = get_initial_product_options(pid) - send_update(LiveSelect.Component, - options: opts, - id: id, - value: pid - ) - end + id = + "sales_order[sales_lines][#{line_no}]_product_id_live_select_component" - socket - |> assign(form: to_form(form)) - |> assign(:unsaved_changes, form.changed?) + send_update(LiveSelect.Component, + options: opts, + id: id, + value: pid + ) + end + end end defp parse_select_id!(id) do From 0144dea3c0f585c8bb955d996eaa3c2ce9d7b62f Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 20:54:51 -0500 Subject: [PATCH 29/90] Use full width --- lib/therons_erp_web/components/layouts/app.html.heex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/therons_erp_web/components/layouts/app.html.heex b/lib/therons_erp_web/components/layouts/app.html.heex index 34a5651..47fb028 100644 --- a/lib/therons_erp_web/components/layouts/app.html.heex +++ b/lib/therons_erp_web/components/layouts/app.html.heex @@ -228,7 +228,7 @@
-
+
From 0081d99da8047434b9f11a09c2bd31b508d89b9e Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 21:28:46 -0500 Subject: [PATCH 30/90] Format the numbers --- assets/css/app.css | 20 +++ .../live/sales_order_live/show.ex | 142 ++++++++++-------- 2 files changed, 103 insertions(+), 59 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 0500f05..ed92b49 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -92,3 +92,23 @@ @apply border-b-brand; } } + +.input-icon { + position: relative; +} + +.input-icon > i { + position: absolute; + display: block; + transform: translate(0, -40%); + top: 50%; + pointer-events: none; + width: 25px; + text-align: center; + font-style: normal; +} + +.input-icon input { + padding-left: 25px; + padding-right: 0; +} diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index cc6a890..524956a 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -11,6 +11,17 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.header> Sales order {@sales_order.identifier} <.status_badge state={@sales_order.state} /> + <%= if @unsaved_changes do %> + <.button phx-disable-with="Saving..." class="save-button"> + <.icon name="hero-check-circle" /> + + + <%= if @drop_sales > 0 do %> + + Delete {@drop_sales} items. + + <% end %> + <% end %> <:actions> <%= if @sales_order.state == :draft and not @unsaved_changes do %> @@ -23,19 +34,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do Return to draft <% end %> - <%= if @unsaved_changes do %> - <.button phx-disable-with="Saving..." class="save-button"> - <.icon name="hero-check-circle" /> - - - <%= if @drop_sales > 0 do %> - - Delete {@drop_sales} items. - - <% end %> - <% end %> <:subtitle> + Margin: {@sales_order.total_cost} @@ -43,16 +44,17 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do = - {if @sales_order.total_cost not in [nil, Money.new(0, :USD)], - do: - (Decimal.mult( - Money.div!(@sales_order.total_price, @sales_order.total_cost.amount).amount, - 100 - ) - |> Decimal.sub(100) - |> Decimal.to_string()) <> - "%", - else: "undefined"} + {if @sales_order.total_cost != nil and + not Money.equal?(@sales_order.total_cost, Money.new(0, :USD)), + do: + (Decimal.mult( + Money.div!(@sales_order.total_price, @sales_order.total_cost.amount).amount, + 100 + ) + |> Decimal.sub(100) + |> Decimal.to_string()) <> + "%", + else: "undefined"} @@ -65,7 +67,8 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do
- + + @@ -111,32 +114,44 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.input field={sales_line[:quantity]} type="number" /> @@ -159,9 +164,14 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do inline_container={true} /> <%= if @total_cost_changes[to_string(sales_line.index)] || is_total_cost_persisted?(sales_line) do %> - + <.button + phx-disable-with="Saving..." + class="revert-button" + name="revert" + value={"revert-total-cost-#{sales_line.index}"} + > <.icon name="hero-arrow-uturn-left" /> - + <% end %> @@ -327,10 +337,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do socket end - def handle_event("revert-total-price-" <> index, _params, socket) do - # IO.inspect(socket.assigns.form) - params = AshPhoenix.Form.params(socket.assigns.form) - # IO.inspect(params) + def handle_event( + "save", + %{"revert" => "revert-total-price-" <> index, "sales_order" => params}, + socket + ) do new_params = put_in(params, ["sales_lines", index, "total_price"], nil) form = @@ -343,16 +354,21 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> assign(:total_price_changes, Map.delete(socket.assigns.total_price_changes, index))} end - def handle_event("revert-total-cost-" <> index, _params, socket) do - new_params = put_in(socket.assigns.params, ["sales_lines", index, "total_cost"], nil) + def handle_event( + "save", + %{"revert" => "revert-total-cost-" <> index, "sales_order" => params}, + socket + ) do + new_params = put_in(params, ["sales_lines", index, "total_cost"], nil) - form = AshPhoenix.Form.validate(socket.assigns.form, new_params) + form = + AshPhoenix.Form.validate(socket.assigns.form, new_params) {:noreply, socket |> assign(:form, form) |> assign(:params, new_params) - |> assign(:total_cost_changes, Map.delete(socket.assigns.total_price_changes, index))} + |> assign(:total_cost_changes, Map.delete(socket.assigns.total_cost_changes, index))} end @impl true From ef4ab08fedba01e16a01c984aaa6279eadaec73f Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 12:56:45 -0500 Subject: [PATCH 41/90] Operations on costs and prices --- .../live/sales_order_live/show.ex | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index c531c60..470ce28 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -371,8 +371,46 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> assign(:total_cost_changes, Map.delete(socket.assigns.total_cost_changes, index))} end + def erase_total_price_changes(sales_order_params, price_changes) do + new_lines = + for {id, line} <- sales_order_params["sales_lines"], into: %{} do + if price_changes[id] do + {id, line} + else + new_line = put_in(line["total_price"], nil) + {id, new_line} + end + end + + put_in(sales_order_params["sales_lines"], new_lines) + end + + def erase_total_cost_changes(sales_order_params, cost_changes) do + new_lines = + for {id, line} <- sales_order_params["sales_lines"], into: %{} do + if cost_changes[id] do + {id, line} + else + new_line = put_in(line["total_cost"], nil) + {id, new_line} + end + end + + put_in(sales_order_params["sales_lines"], new_lines) + end + + def process_modifications(sales_order_params, socket) do + sales_order_params = + erase_total_price_changes(sales_order_params, socket.assigns.total_price_changes) + + sales_order_params = + erase_total_cost_changes(sales_order_params, socket.assigns.total_cost_changes) + end + @impl true def handle_event("validate", %{"sales_order" => sales_order_params} = params, socket) do + sales_order_params = process_modifications(sales_order_params, socket) + socket = socket |> record_total_price_change(params["_target"]) @@ -411,20 +449,8 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end end - def erase_total_price_changes(sales_order_params, price_changes) do - sales_order_params - end - - def erase_total_cost_changes(sales_order_params, price_changes) do - sales_order_params - end - def handle_event("save", %{"sales_order" => sales_order_params}, socket) do - sales_order_params = - erase_total_price_changes(sales_order_params, socket.assigns.total_price_changes) - - sales_order_params = - erase_total_cost_changes(sales_order_params, socket.assigns.total_cost_changes) + sales_order_params = process_modifications(sales_order_params, socket) case AshPhoenix.Form.submit(socket.assigns.form, params: sales_order_params) do {:ok, sales_order} -> From 0fe35372c0af7c3c3ce0ca6217c99b8b68dbf2de Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 13:30:42 -0500 Subject: [PATCH 42/90] Work on forms --- .../live/sales_order_live/show.ex | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 470ce28..587f49d 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -142,7 +142,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do type="number" inline_container={true} /> - <%= if @total_price_changes[to_string(sales_line.index)] || is_active_price_persisted?(sales_line) do %> + <%= if (@total_price_changes[to_string(sales_line.index)] == true) || is_active_price_persisted?(sales_line, sales_line.index, @total_price_changes) do %> <.button phx-disable-with="Saving..." class="revert-button" @@ -163,7 +163,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do type="number" inline_container={true} /> - <%= if @total_cost_changes[to_string(sales_line.index)] || is_total_cost_persisted?(sales_line) do %> + <%= if (@total_cost_changes[to_string(sales_line.index)] == true) || is_total_cost_persisted?(sales_line, sales_line.index, @total_cost_changes) do %> <.button phx-disable-with="Saving..." class="revert-button" @@ -349,9 +349,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket - |> assign(:form, form) + |> assign(:form, to_form(form)) |> assign(:params, new_params) - |> assign(:total_price_changes, Map.delete(socket.assigns.total_price_changes, index))} + |> assign(:unsaved_changes, form.source.changed?) + |> assign(:total_price_changes, Map.put(socket.assigns.total_price_changes, index, false))} end def handle_event( @@ -366,9 +367,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket - |> assign(:form, form) + |> assign(:form, to_form(form)) |> assign(:params, new_params) - |> assign(:total_cost_changes, Map.delete(socket.assigns.total_cost_changes, index))} + |> assign(:unsaved_changes, form.source.changed?) + |> assign(:total_cost_changes, Map.put(socket.assigns.total_cost_changes, index, false))} end def erase_total_price_changes(sales_order_params, price_changes) do @@ -488,26 +490,35 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} end - defp is_active_price_persisted?(sales_line) do - case sales_line.source.data do - # In case there's no data source (e.g., new line) - nil -> - false + defp is_active_price_persisted?(sales_line, index, total_price_changes) do + if total_price_changes[to_string(index)] == false do + false + else + case sales_line.source.data do + # In case there's no data source (e.g., new line) + nil -> + false - line_data -> - line_data.total_price != nil and - not Money.equal?(line_data.total_cost, Money.new(0, :USD)) + line_data -> + line_data.total_price != nil and + not Money.equal?(line_data.total_price, Money.new(0, :USD)) + end end end - defp is_total_cost_persisted?(sales_line) do - case sales_line.source.data do - # In case there's no data source (e.g., new line) - nil -> - false + defp is_total_cost_persisted?(sales_line, index, total_cost_changes) do + if total_cost_changes[to_string(index)] == false do + false + else + case sales_line.source.data do + # In case there's no data source (e.g., new line) + nil -> + false - line_data -> - line_data.total_cost != nil and not Money.equal?(line_data.total_cost, Money.new(0, :USD)) + line_data -> + line_data.total_cost != nil and + not Money.equal?(line_data.total_cost, Money.new(0, :USD)) + end end end From dd65cce47c41947ee5c409046b8df432ac89e501 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 13:33:26 -0500 Subject: [PATCH 43/90] Fix saving values --- lib/therons_erp_web/live/sales_order_live/show.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 587f49d..340abb6 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -376,7 +376,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do def erase_total_price_changes(sales_order_params, price_changes) do new_lines = for {id, line} <- sales_order_params["sales_lines"], into: %{} do - if price_changes[id] do + if price_changes[id] != false do {id, line} else new_line = put_in(line["total_price"], nil) @@ -390,7 +390,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do def erase_total_cost_changes(sales_order_params, cost_changes) do new_lines = for {id, line} <- sales_order_params["sales_lines"], into: %{} do - if cost_changes[id] do + if cost_changes[id] != false do {id, line} else new_line = put_in(line["total_cost"], nil) From 74bb315f6288ef8f1d88fbe909be998044405029 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 13:34:29 -0500 Subject: [PATCH 44/90] Fix the sort order --- lib/therons_erp_web/sales/sales_order.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index c020ac1..a6b7d54 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -77,6 +77,7 @@ defmodule TheronsErp.Sales.SalesOrder do relationships do has_many :sales_lines, TheronsErp.Sales.SalesLine do destination_attribute :sales_order_id + sort id: :desc end belongs_to :customer, TheronsErp.People.Entity From c9c5e437107bab2b0f6a402272d1930407a19a27 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 13:50:47 -0500 Subject: [PATCH 45/90] Add total cost and dollar signs --- lib/therons_erp_web/live/sales_order_live/show.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 340abb6..7a9555f 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -192,7 +192,6 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <% else %> <%= for sales_line <- @sales_order.sales_lines do %> - what @@ -200,13 +199,16 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {sales_line.quantity} + <% end %> From a1a6fd105741a4870ec27de5fc0c2af43d9cb0ed Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 13:57:59 -0500 Subject: [PATCH 46/90] add price summary --- assets/css/app.css | 13 +++++++++++++ lib/therons_erp_web/live/sales_order_live/show.ex | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/assets/css/app.css b/assets/css/app.css index e154b6f..75d5c33 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -125,3 +125,16 @@ padding-left: 25px; padding-right: 0; } + +.cost-summary { + width: 200px; + float: right; + + div { + text-align: right; + + span { + float: left; + } + } +} diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 7a9555f..925e0ae 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -223,6 +223,20 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <% end %> +
+
+ (Subtotal) $ {@sales_order.total_price.amount |> Decimal.to_float()} +
+
+ (Cost) $ {@sales_order.total_cost.amount |> Decimal.to_float()} +
+
+
+ (Net) $ {Money.sub!(@sales_order.total_price, @sales_order.total_cost).amount + |> Decimal.to_float()} +
+
+ <.back navigate={~p"/sales_orders"}>Back to sales_orders From 88cd7b3cdc25bf898e7bc77f41549fd380b3e2df Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 13:59:55 -0500 Subject: [PATCH 47/90] Add links to products --- lib/therons_erp_web/live/sales_order_live/show.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 925e0ae..b603231 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -193,7 +193,18 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <%= for sales_line <- @sales_order.sales_lines do %>
From 82edde5e6f20811e4d38a1c5dc3edb2752ddf605 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 20:20:51 -0500 Subject: [PATCH 24/90] Test with the problem --- lib/therons_erp_web/sales/sales_order.ex | 1 + .../live/sales_orders_live.exs | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/therons_erp_web/live/sales_orders_live.exs diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex index c835190..c020ac1 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp_web/sales/sales_order.ex @@ -48,6 +48,7 @@ defmodule TheronsErp.Sales.SalesOrder do end create :create do + primary? true argument :sales_lines, {:array, :map} change manage_relationship(:sales_lines, type: :create), diff --git a/test/therons_erp_web/live/sales_orders_live.exs b/test/therons_erp_web/live/sales_orders_live.exs new file mode 100644 index 0000000..a17d9f4 --- /dev/null +++ b/test/therons_erp_web/live/sales_orders_live.exs @@ -0,0 +1,73 @@ +defmodule TheronsErpWeb.SalesOrdersLiveTest do + use TheronsErpWeb.ConnCase + + @new_args %{ "_target" => ["sales_order", "sales_lines", "2", "product_id"], + "line_id" => "2", + "product_id" => "fa68f48a-a852-49fd-a80e-41592e9922a3", + "sales_order" => %{ + "sales_lines" => %{ + "0" => %{ + "_form_type" => "update", + "_persistent_id" => "0", + "_touched" => "_form_type,_persistent_id,_touched,_unused_product_id_text_input,_unused_quantity,_unused_sales_price,_unused_total_price,_unused_unit_price,id,product_id,product_id_text_input,quantity,sales_price,total_price,unit_price", + "_unused_product_id_text_input" => "", + "_unused_quantity" => "", + "_unused_sales_price" => "", + "_unused_total_price" => "", + "_unused_unit_price" => "", + "id" => "ceb0aedf-6ef4-4497-b820-d43aad073750", + "product_id" => "961be52a-0ad5-4fdb-be76-0c86fdbcd4e4", + "product_id_text_input" => "abc123", + "quantity" => "2", + "sales_price" => "3.0", + "total_price" => "6.0", + "unit_price" => "6.0" + }, + "1" => %{ + "_form_type" => "update", + "_persistent_id" => "1", + "_touched" => "_form_type,_persistent_id,_touched,_unused_product_id_text_input,_unused_quantity,_unused_sales_price,_unused_total_price,_unused_unit_price,id,product_id,product_id_text_input,quantity,sales_price,total_price,unit_price", + "_unused_product_id_text_input" => "", + "_unused_quantity" => "", + "_unused_sales_price" => "", + "_unused_total_price" => "", + "_unused_unit_price" => "", + "id" => "cfdf8f66-0cf5-41cb-97be-ea8a8b308fc1", + "product_id" => "d3d5df6a-1829-40db-a054-64b48b6fc512", + "product_id_text_input" => "Bob The Builder 2", + "quantity" => "2", + "sales_price" => "3.0", + "total_price" => "6.0", + "unit_price" => "4.0" + }, + "2" => %{ + "_form_type" => "create", + "_persistent_id" => "2", + "_touched" => "_form_type,_persistent_id,_touched,_unused_product_id_text_input,_unused_quantity,_unused_sales_price,_unused_total_price,_unused_unit_price,product_id,product_id_text_input,quantity,sales_price,total_price,unit_price", + "_unused_product_id_text_input" => "", + "_unused_quantity" => "", + "_unused_sales_price" => "", + "_unused_total_price" => "", + "_unused_unit_price" => "", + "product_id" => "fa68f48a-a852-49fd-a80e-41592e9922a3", + "product_id_text_input" => "Create New", + "quantity" => "", + "sales_price" => "", + "total_price" => "", + "unit_price" => "" + } + } + } + } + + test "children from froms" do + sales_order = Ash.create!(TheronsErp.Sales.SalesOrder) + + form = AshPhoenix.Form.for_update(sales_order, :update, + as: "sales_order") + + form = AshPhoenix.Form.validate(form, @new_args) + IO.inspect(form) + assert length(form.forms.sales_lines) == 3 + end +end From d37f8441a26b638e5864e9bae588e2f0c1c886cb Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 20:42:43 -0500 Subject: [PATCH 25/90] fix/removed tests --- test/breadcrumb_test.exs | 2 +- test/therons_erp/inventory_test.exs | 5 +- .../controllers/page_controller_test.exs | 2 +- .../live/product_category_live_test.exs | 118 ------------------ .../live/product_live_test.exs | 110 ---------------- 5 files changed, 4 insertions(+), 233 deletions(-) diff --git a/test/breadcrumb_test.exs b/test/breadcrumb_test.exs index 83ca9c1..0a4cefe 100644 --- a/test/breadcrumb_test.exs +++ b/test/breadcrumb_test.exs @@ -1,4 +1,4 @@ -defmodule TheronsErpWeb.ProductCategoryLiveTest do +defmodule TheronsErpWeb.BreadcrumbTest do use TheronsErpWeb.ConnCase import Phoenix.LiveViewTest diff --git a/test/therons_erp/inventory_test.exs b/test/therons_erp/inventory_test.exs index 14a747a..d303dd5 100644 --- a/test/therons_erp/inventory_test.exs +++ b/test/therons_erp/inventory_test.exs @@ -28,9 +28,8 @@ defmodule TheronsErp.InventoryTest do assert Ash.get!(ProductCategory, cat2.id).full_name == "Category 3 / Category 2" assert Ash.get!(ProductCategory, cat3.id).full_name == "Category 3" - assert_raise Ash.Error.Invalid, fn error -> - IO.inspect(error) - Inventory.change_parent!(cat3.id, cat2.id) + assert_raise Ash.Error.Invalid, fn -> + Inventory.change_parent!(cat3, cat2.id) end end end diff --git a/test/therons_erp_web/controllers/page_controller_test.exs b/test/therons_erp_web/controllers/page_controller_test.exs index 332cfc9..e31b984 100644 --- a/test/therons_erp_web/controllers/page_controller_test.exs +++ b/test/therons_erp_web/controllers/page_controller_test.exs @@ -3,6 +3,6 @@ defmodule TheronsErpWeb.PageControllerTest do test "GET /", %{conn: conn} do conn = get(conn, ~p"/") - assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + assert html_response(conn, 200) =~ "TheronsErp" end end diff --git a/test/therons_erp_web/live/product_category_live_test.exs b/test/therons_erp_web/live/product_category_live_test.exs index 8dff03c..af930bd 100644 --- a/test/therons_erp_web/live/product_category_live_test.exs +++ b/test/therons_erp_web/live/product_category_live_test.exs @@ -1,121 +1,3 @@ defmodule TheronsErpWeb.ProductCategoryLiveTest do use TheronsErpWeb.ConnCase - - import Phoenix.LiveViewTest - import TheronsErp.InventoryFixtures - - @create_attrs %{name: "some name"} - @update_attrs %{name: "some updated name"} - @invalid_attrs %{name: nil} - - defp create_product_category(_) do - product_category = product_category_fixture() - %{product_category: product_category} - end - - describe "Index" do - setup [:create_product_category] - - test "lists all product_categories", %{conn: conn, product_category: product_category} do - {:ok, _index_live, html} = live(conn, ~p"/product_categories") - - assert html =~ "Listing Product categories" - assert html =~ product_category.name - end - - test "saves new product_category", %{conn: conn} do - {:ok, index_live, _html} = live(conn, ~p"/product_categories") - - assert index_live |> element("a", "New Product category") |> render_click() =~ - "New Product category" - - assert_patch(index_live, ~p"/product_categories/new") - - assert index_live - |> form("#product_category-form", product_category: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert index_live - |> form("#product_category-form", product_category: @create_attrs) - |> render_submit() - - assert_patch(index_live, ~p"/product_categories") - - html = render(index_live) - assert html =~ "Product category created successfully" - assert html =~ "some name" - end - - test "updates product_category in listing", %{conn: conn, product_category: product_category} do - {:ok, index_live, _html} = live(conn, ~p"/product_categories") - - assert index_live - |> element("#product_categories-#{product_category.id} a", "Edit") - |> render_click() =~ - "Edit Product category" - - assert_patch(index_live, ~p"/product_categories/#{product_category}/edit") - - assert index_live - |> form("#product_category-form", product_category: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert index_live - |> form("#product_category-form", product_category: @update_attrs) - |> render_submit() - - assert_patch(index_live, ~p"/product_categories") - - html = render(index_live) - assert html =~ "Product category updated successfully" - assert html =~ "some updated name" - end - - test "deletes product_category in listing", %{conn: conn, product_category: product_category} do - {:ok, index_live, _html} = live(conn, ~p"/product_categories") - - assert index_live - |> element("#product_categories-#{product_category.id} a", "Delete") - |> render_click() - - refute has_element?(index_live, "#product_categories-#{product_category.id}") - end - end - - describe "Show" do - setup [:create_product_category] - - test "displays product_category", %{conn: conn, product_category: product_category} do - {:ok, _show_live, html} = live(conn, ~p"/product_categories/#{product_category}") - - assert html =~ "Show Product category" - assert html =~ product_category.name - end - - test "updates product_category within modal", %{ - conn: conn, - product_category: product_category - } do - {:ok, show_live, _html} = live(conn, ~p"/product_categories/#{product_category}") - - assert show_live |> element("a", "Edit") |> render_click() =~ - "Edit Product category" - - assert_patch(show_live, ~p"/product_categories/#{product_category}/show/edit") - - assert show_live - |> form("#product_category-form", product_category: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert show_live - |> form("#product_category-form", product_category: @update_attrs) - |> render_submit() - - assert_patch(show_live, ~p"/product_categories/#{product_category}") - - html = render(show_live) - assert html =~ "Product category updated successfully" - assert html =~ "some updated name" - end - end end diff --git a/test/therons_erp_web/live/product_live_test.exs b/test/therons_erp_web/live/product_live_test.exs index 3d1313c..42fce11 100644 --- a/test/therons_erp_web/live/product_live_test.exs +++ b/test/therons_erp_web/live/product_live_test.exs @@ -1,113 +1,3 @@ defmodule TheronsErpWeb.ProductLiveTest do use TheronsErpWeb.ConnCase - - import Phoenix.LiveViewTest - import TheronsErp.InventoryFixtures - - @create_attrs %{name: "some name", tags: ["option1", "option2"]} - @update_attrs %{name: "some updated name", tags: ["option1"]} - @invalid_attrs %{name: nil, tags: []} - - defp create_product(_) do - product = product_fixture() - %{product: product} - end - - describe "Index" do - setup [:create_product] - - test "lists all products", %{conn: conn, product: product} do - {:ok, _index_live, html} = live(conn, ~p"/products") - - assert html =~ "Listing Products" - assert html =~ product.name - end - - test "saves new product", %{conn: conn} do - {:ok, index_live, _html} = live(conn, ~p"/products") - - assert index_live |> element("a", "New Product") |> render_click() =~ - "New Product" - - assert_patch(index_live, ~p"/products/new") - - assert index_live - |> form("#product-form", product: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert index_live - |> form("#product-form", product: @create_attrs) - |> render_submit() - - assert_patch(index_live, ~p"/products") - - html = render(index_live) - assert html =~ "Product created successfully" - assert html =~ "some name" - end - - test "updates product in listing", %{conn: conn, product: product} do - {:ok, index_live, _html} = live(conn, ~p"/products") - - assert index_live |> element("#products-#{product.id} a", "Edit") |> render_click() =~ - "Edit Product" - - assert_patch(index_live, ~p"/products/#{product}/edit") - - assert index_live - |> form("#product-form", product: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert index_live - |> form("#product-form", product: @update_attrs) - |> render_submit() - - assert_patch(index_live, ~p"/products") - - html = render(index_live) - assert html =~ "Product updated successfully" - assert html =~ "some updated name" - end - - test "deletes product in listing", %{conn: conn, product: product} do - {:ok, index_live, _html} = live(conn, ~p"/products") - - assert index_live |> element("#products-#{product.id} a", "Delete") |> render_click() - refute has_element?(index_live, "#products-#{product.id}") - end - end - - describe "Show" do - setup [:create_product] - - test "displays product", %{conn: conn, product: product} do - {:ok, _show_live, html} = live(conn, ~p"/products/#{product}") - - assert html =~ "Show Product" - assert html =~ product.name - end - - test "updates product within modal", %{conn: conn, product: product} do - {:ok, show_live, _html} = live(conn, ~p"/products/#{product}") - - assert show_live |> element("a", "Edit") |> render_click() =~ - "Edit Product" - - assert_patch(show_live, ~p"/products/#{product}/show/edit") - - assert show_live - |> form("#product-form", product: @invalid_attrs) - |> render_change() =~ "can't be blank" - - assert show_live - |> form("#product-form", product: @update_attrs) - |> render_submit() - - assert_patch(show_live, ~p"/products/#{product}") - - html = render(show_live) - assert html =~ "Product updated successfully" - assert html =~ "some updated name" - end - end end From e22ca4a33857d496330fc6db1db2ef27ed4db500 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Mon, 10 Feb 2025 20:42:50 -0500 Subject: [PATCH 26/90] showing active prie --- .../live/sales_order_live/show.ex | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 7003be6..77d7d03 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -127,7 +127,14 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.input field={sales_line[:total_price]} - value={do_money(sales_line[:active_price])} + value={active_price_for_sales_line(sales_line)} + type="number" + /> + + <.input + field={sales_line[:total_cost]} + value={total_cost_for_sales_line(sales_line)} type="number" /> Quantity Sales Price Unit PriceTotalTotal PriceTotal Cost
- <.input - field={sales_line[:sales_price]} - value={do_money(sales_line[:sales_price])} - type="number" - /> + + $ + <.input + field={sales_line[:sales_price]} + value={do_money(sales_line[:sales_price])} + type="number" + /> + - <.input - field={sales_line[:unit_price]} - value={do_money(sales_line[:unit_price])} - type="number" - /> + + $ + <.input + field={sales_line[:unit_price]} + value={do_money(sales_line[:unit_price])} + type="number" + /> + - <.input - field={sales_line[:total_price]} - value={active_price_for_sales_line(sales_line)} - type="number" - /> + + $ + <.input + field={sales_line[:total_price]} + value={active_price_for_sales_line(sales_line)} + type="number" + /> + - <.input - field={sales_line[:total_cost]} - value={total_cost_for_sales_line(sales_line)} - type="number" - /> + + $ + <.input + field={sales_line[:total_cost]} + value={total_cost_for_sales_line(sales_line)} + type="number" + /> + - $ + $ <.input field={sales_line[:sales_price]} value={do_money(sales_line[:sales_price])} @@ -125,7 +125,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do - $ + $ <.input field={sales_line[:unit_price]} value={do_money(sales_line[:unit_price])} @@ -135,17 +135,21 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do - $ + $ <.input field={sales_line[:total_price]} value={active_price_for_sales_line(sales_line)} type="number" + inline_container={true} /> + + <.icon name="hero-arrow-uturn-left" /> + - $ + $ <.input field={sales_line[:total_cost]} value={total_cost_for_sales_line(sales_line)} diff --git a/lib/therons_erp_web/sales/sales_line.ex b/lib/therons_erp_web/sales/sales_line.ex index 5aea018..e8968c8 100644 --- a/lib/therons_erp_web/sales/sales_line.ex +++ b/lib/therons_erp_web/sales/sales_line.ex @@ -20,7 +20,7 @@ defmodule TheronsErp.Sales.SalesLine do update :update do require_atomic? false primary? true - accept [:sales_price, :unit_price, :quantity, :product_id] + accept [:sales_price, :unit_price, :quantity, :product_id, :total_price] end end From 23b538bae616ad501ef80a2ccce3b5ac327b48a3 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Tue, 11 Feb 2025 11:51:56 -0500 Subject: [PATCH 35/90] Change reversions --- .../live/sales_order_live/show.ex | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 7e8ddc9..b3785e7 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -142,9 +142,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do type="number" inline_container={true} /> - - <.icon name="hero-arrow-uturn-left" /> - + <%= if @total_price_changes[to_string(sales_line.index)] do %> + + <.icon name="hero-arrow-uturn-left" /> + + <% end %> @@ -281,14 +283,51 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> assign(:params, params) |> assign(:default_products, default_products) |> assign(:drop_sales, 0) + |> assign(:total_price_changes, %{}) + |> assign(:total_cost_changes, %{}) |> assign_form()} end defp page_title(:show), do: "Show Sales order" defp page_title(:edit), do: "Edit Sales order" + defp record_total_price_change( + socket, + ["sales_order", "sales_lines", index, "total_price"] + ) do + changes = socket.assigns.total_price_changes + changes = put_in(changes[index], true) + + socket + |> assign(:total_price_changes, changes) + end + + defp record_total_price_change(socket, _) do + socket + end + + defp record_total_cost_change( + socket, + ["sales_order", "sales_lines", index, "total_cost"] + ) do + changes = socket.assigns.total_cost_changes + changes = put_in(changes[index], true) + + socket + |> assign(:total_cost_changes, changes) + end + + defp record_total_cost_change(socket, _) do + socket + end + @impl true def handle_event("validate", %{"sales_order" => sales_order_params} = params, socket) do + socket = + socket + |> record_total_price_change(params["_target"]) + |> record_total_cost_change(params["_target"]) + output = (sales_order_params["sales_lines"] || []) |> Enum.map(fn {id, val} -> From 412fca6fd49329140507bd8641a7915bf383ecc5 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Tue, 11 Feb 2025 12:24:32 -0500 Subject: [PATCH 36/90] Reverting --- .../live/sales_order_live/show.ex | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index b3785e7..09f93bd 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -143,7 +143,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do inline_container={true} /> <%= if @total_price_changes[to_string(sales_line.index)] do %> - + <.icon name="hero-arrow-uturn-left" /> <% end %> @@ -157,6 +157,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do value={total_cost_for_sales_line(sales_line)} type="number" /> + <%= if @total_cost_changes[to_string(sales_line.index)] do %> + + <.icon name="hero-arrow-uturn-left" /> + + <% end %> @@ -321,6 +326,18 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do socket end + def handle_event("revert-total-price-" <> index, params, socket) do + {:noreply, + socket + |> assign(:total_price_changes, Map.delete(socket.assigns.total_price_changes, index))} + end + + def handle_event("revert-total-cost-" <> index, params, socket) do + {:noreply, + socket + |> assign(:total_cost_changes, Map.delete(socket.assigns.total_cost_changes, index))} + end + @impl true def handle_event("validate", %{"sales_order" => sales_order_params} = params, socket) do socket = @@ -398,6 +415,22 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} end + defp is_active_price_persisted?(sales_line) do + case sales_line.source.data do + # In case there's no data source (e.g., new line) + nil -> false + line_data -> line_data.total_price != nil + end + end + + defp is_total_cost_persisted?(sales_line) do + case sales_line.source.data do + # In case there's no data source (e.g., new line) + nil -> false + line_data -> line_data.total_cost != nil + end + end + defp active_price_for_sales_line(sales_line) do data_total_price = case sales_line.source.data do @@ -434,7 +467,6 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do defp total_cost_for_sales_line(sales_line) do unit_price = Phoenix.HTML.Form.input_value(sales_line, :unit_price) quantity = Phoenix.HTML.Form.input_value(sales_line, :quantity) - IO.inspect(unit_price: unit_price, quantity: quantity) case {unit_price, quantity} do {nil, nil} -> From c2dfe30f4290714492882ab3f926a97d241f2f3c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Tue, 11 Feb 2025 12:24:49 -0500 Subject: [PATCH 37/90] Unused args --- lib/therons_erp_web/live/sales_order_live/show.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 09f93bd..7c52f31 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -326,13 +326,13 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do socket end - def handle_event("revert-total-price-" <> index, params, socket) do + def handle_event("revert-total-price-" <> index, _params, socket) do {:noreply, socket |> assign(:total_price_changes, Map.delete(socket.assigns.total_price_changes, index))} end - def handle_event("revert-total-cost-" <> index, params, socket) do + def handle_event("revert-total-cost-" <> index, _params, socket) do {:noreply, socket |> assign(:total_cost_changes, Map.delete(socket.assigns.total_cost_changes, index))} From 378e8eed1b121b41aef8ca22ff36b2c146101cfd Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 12:06:56 -0500 Subject: [PATCH 38/90] Fix some bugs --- .../live/sales_order_live/show.ex | 104 ++++++++++++++---- 1 file changed, 82 insertions(+), 22 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 7c52f31..8b142ec 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -142,7 +142,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do type="number" inline_container={true} /> - <%= if @total_price_changes[to_string(sales_line.index)] do %> + <%= if @total_price_changes[to_string(sales_line.index)] || is_active_price_persisted?(sales_line) do %> <.icon name="hero-arrow-uturn-left" /> @@ -156,8 +156,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do field={sales_line[:total_cost]} value={total_cost_for_sales_line(sales_line)} type="number" + inline_container={true} /> - <%= if @total_cost_changes[to_string(sales_line.index)] do %> + <%= if @total_cost_changes[to_string(sales_line.index)] || is_total_cost_persisted?(sales_line) do %> <.icon name="hero-arrow-uturn-left" /> @@ -327,15 +328,31 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end def handle_event("revert-total-price-" <> index, _params, socket) do + # IO.inspect(socket.assigns.form) + params = AshPhoenix.Form.params(socket.assigns.form) + # IO.inspect(params) + new_params = put_in(params, ["sales_lines", index, "total_price"], nil) + + form = + AshPhoenix.Form.validate(socket.assigns.form, new_params) + {:noreply, socket + |> assign(:form, form) + |> assign(:params, new_params) |> assign(:total_price_changes, Map.delete(socket.assigns.total_price_changes, index))} end def handle_event("revert-total-cost-" <> index, _params, socket) do + new_params = put_in(socket.assigns.params, ["sales_lines", index, "total_cost"], nil) + + form = AshPhoenix.Form.validate(socket.assigns.form, new_params) + {:noreply, socket - |> assign(:total_cost_changes, Map.delete(socket.assigns.total_cost_changes, index))} + |> assign(:form, form) + |> assign(:params, new_params) + |> assign(:total_cost_changes, Map.delete(socket.assigns.total_price_changes, index))} end @impl true @@ -378,7 +395,21 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end end + def erase_total_price_changes(sales_order_params, price_changes) do + sales_order_params + end + + def erase_total_cost_changes(sales_order_params, price_changes) do + sales_order_params + end + def handle_event("save", %{"sales_order" => sales_order_params}, socket) do + sales_order_params = + erase_total_price_changes(sales_order_params, socket.assigns.total_price_changes) + + sales_order_params = + erase_total_cost_changes(sales_order_params, socket.assigns.total_cost_changes) + case AshPhoenix.Form.submit(socket.assigns.form, params: sales_order_params) do {:ok, sales_order} -> # notify_parent({:saved, sales_order}) @@ -418,30 +449,42 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do defp is_active_price_persisted?(sales_line) do case sales_line.source.data do # In case there's no data source (e.g., new line) - nil -> false - line_data -> line_data.total_price != nil + nil -> + false + + line_data -> + line_data.total_price != nil and + not Money.equal?(line_data.total_cost, Money.new(0, :USD)) end end defp is_total_cost_persisted?(sales_line) do case sales_line.source.data do # In case there's no data source (e.g., new line) - nil -> false - line_data -> line_data.total_cost != nil + nil -> + false + + line_data -> + line_data.total_cost != nil and not Money.equal?(line_data.total_cost, Money.new(0, :USD)) end end defp active_price_for_sales_line(sales_line) do data_total_price = - case sales_line.source.data do + case Phoenix.HTML.Form.input_value(sales_line, :total_price) do # In case there's no data source (e.g., new line) nil -> nil - line_data -> line_data.total_price + total_price -> total_price end if data_total_price do - data_total_price.amount - |> Decimal.to_string() + case data_total_price do + %Money{} -> + data_total_price.amount |> Decimal.to_string() + + _ -> + data_total_price + end else sales_price = Phoenix.HTML.Form.input_value(sales_line, :sales_price) quantity = Phoenix.HTML.Form.input_value(sales_line, :quantity) @@ -465,21 +508,38 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end defp total_cost_for_sales_line(sales_line) do - unit_price = Phoenix.HTML.Form.input_value(sales_line, :unit_price) - quantity = Phoenix.HTML.Form.input_value(sales_line, :quantity) + data_total_cost = + case Phoenix.HTML.Form.input_value(sales_line, :total_cost) do + # In case there's no data source (e.g., new line) + nil -> nil + total_cost -> total_cost + end - case {unit_price, quantity} do - {nil, nil} -> - "" + if data_total_cost do + case data_total_cost do + %Money{} -> + data_total_cost.amount |> Decimal.to_string() - {_, nil} -> - "" + _ -> + data_total_cost + end + else + unit_price = Phoenix.HTML.Form.input_value(sales_line, :unit_price) + quantity = Phoenix.HTML.Form.input_value(sales_line, :quantity) - {nil, _} -> - "" + case {unit_price, quantity} do + {nil, nil} -> + "" - {_, _} -> - Money.mult!(unit_price, quantity) |> Money.to_decimal() |> Decimal.to_string() + {_, nil} -> + "" + + {nil, _} -> + "" + + {_, _} -> + Money.mult!(unit_price, quantity) |> Money.to_decimal() |> Decimal.to_string() + end end end From c0d85e3f2384d3fe12af389868ac00c8cc81cba5 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 12:08:16 -0500 Subject: [PATCH 39/90] Update show.ex Fix reload --- lib/therons_erp_web/live/sales_order_live/show.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 8b142ec..ae84beb 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -422,7 +422,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do load_by_id(socket.assigns.sales_order.id, socket) ) |> assign_form() - |> push_patch( + |> push_navigate( to: ~p"/sales_orders/#{sales_order.id}?#{[breadcrumbs: Breadcrumbs.encode_breadcrumbs(socket.assigns.breadcrumbs)]}" ) From 6254f46a4e93145378e9a5ff57d2bf9725d06d2c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 12:49:55 -0500 Subject: [PATCH 40/90] Fixing reset --- assets/css/app.css | 5 ++- .../live/sales_order_live/show.ex | 40 +++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 51a3006..e154b6f 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -48,7 +48,10 @@ .revert-button { position: absolute; - margin-top: 22px; + /* margin-top: 22px; */ + color: black; + @apply border-transparent bg-transparent; + @apply hover:bg-transparent !important; span { position: relative; diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index ae84beb..c531c60 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -143,9 +143,14 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do inline_container={true} /> <%= if @total_price_changes[to_string(sales_line.index)] || is_active_price_persisted?(sales_line) do %> - + <.button + phx-disable-with="Saving..." + class="revert-button" + name="revert" + value={"revert-total-price-#{sales_line.index}"} + > <.icon name="hero-arrow-uturn-left" /> - + <% end %>
{sales_line.product.name} - {sales_line.sales_price.amount |> Decimal.to_float()} + $ {sales_line.sales_price.amount |> Decimal.to_float()} - {sales_line.unit_price.amount |> Decimal.to_float()} + $ {sales_line.unit_price.amount |> Decimal.to_float()} - {sales_line.active_price.amount |> Decimal.to_float()} + $ {sales_line.active_price.amount |> Decimal.to_float()} + + $ {sales_line.total_cost.amount |> Decimal.to_float()}
- {sales_line.product.name} + <.link + navigate={ + TheronsErpWeb.Breadcrumbs.navigate_to_url( + @breadcrumbs, + {"products", sales_line.product.id, ""}, + {"sales_orders", @sales_order.id, @params, @sales_order.identifier} + ) + } + class="text-blue-600" + > + {sales_line.product.name} + {sales_line.quantity} From 4dc0e4fafd460e0ff4519de6a4928c13257d36c3 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 23:09:00 -0500 Subject: [PATCH 48/90] Sales was in wrong folder --- lib/{therons_erp_web => therons_erp}/sales.ex | 0 .../sales/sales_line.ex | 0 .../sales/sales_order.ex | 15 +++++++++++++++ 3 files changed, 15 insertions(+) rename lib/{therons_erp_web => therons_erp}/sales.ex (100%) rename lib/{therons_erp_web => therons_erp}/sales/sales_line.ex (100%) rename lib/{therons_erp_web => therons_erp}/sales/sales_order.ex (81%) diff --git a/lib/therons_erp_web/sales.ex b/lib/therons_erp/sales.ex similarity index 100% rename from lib/therons_erp_web/sales.ex rename to lib/therons_erp/sales.ex diff --git a/lib/therons_erp_web/sales/sales_line.ex b/lib/therons_erp/sales/sales_line.ex similarity index 100% rename from lib/therons_erp_web/sales/sales_line.ex rename to lib/therons_erp/sales/sales_line.ex diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp/sales/sales_order.ex similarity index 81% rename from lib/therons_erp_web/sales/sales_order.ex rename to lib/therons_erp/sales/sales_order.ex index a6b7d54..0ac3d7c 100644 --- a/lib/therons_erp_web/sales/sales_order.ex +++ b/lib/therons_erp/sales/sales_order.ex @@ -53,6 +53,11 @@ defmodule TheronsErp.Sales.SalesOrder do change manage_relationship(:sales_lines, type: :create), where: [attribute_equals(:state, :draft)] + + argument :customer_id, :uuid + argument :address, {:array, :string} + + change &update_customer/2, where: [attribute_equals(:state, :draft)] end update :update do @@ -61,9 +66,18 @@ defmodule TheronsErp.Sales.SalesOrder do change manage_relationship(:sales_lines, type: :direct_control), where: [attribute_equals(:state, :draft)] + + argument :customer_id, :uuid + argument :address, {:array, :string} + + change &update_customer/2, where: [attribute_equals(:state, :draft)] end end + def update_customer(changeset, context) do + IO.inspect({changeset, context}) + end + attributes do uuid_primary_key :id @@ -81,6 +95,7 @@ defmodule TheronsErp.Sales.SalesOrder do end belongs_to :customer, TheronsErp.People.Entity + belongs_to :address, TheronsErp.People.Address end aggregates do From 5978c96227fc2e6e0070ebe25721fdd57a75b274 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Wed, 12 Feb 2025 23:18:22 -0500 Subject: [PATCH 49/90] Fix some bugs and add address --- lib/therons_erp/people.ex | 8 +- lib/therons_erp/sales/sales_order.ex | 1 + .../live/sales_order_live/show.ex | 31 ++++- ...13040919_add_addresses_to_sales_orders.exs | 29 ++++ .../repo/sales_orders/20250213040919.json | 127 ++++++++++++++++++ 5 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 priv/repo/migrations/20250213040919_add_addresses_to_sales_orders.exs create mode 100644 priv/resource_snapshots/repo/sales_orders/20250213040919.json diff --git a/lib/therons_erp/people.ex b/lib/therons_erp/people.ex index baf2cb4..27d5066 100644 --- a/lib/therons_erp/people.ex +++ b/lib/therons_erp/people.ex @@ -3,7 +3,11 @@ defmodule TheronsErp.People do otp_app: :therons_erp resources do - resource TheronsErp.People.Entity - resource TheronsErp.People.Address + resource TheronsErp.People.Entity do + define :list_people, action: :read + end + + resource TheronsErp.People.Address do + end end end diff --git a/lib/therons_erp/sales/sales_order.ex b/lib/therons_erp/sales/sales_order.ex index 0ac3d7c..3a92194 100644 --- a/lib/therons_erp/sales/sales_order.ex +++ b/lib/therons_erp/sales/sales_order.ex @@ -76,6 +76,7 @@ defmodule TheronsErp.Sales.SalesOrder do def update_customer(changeset, context) do IO.inspect({changeset, context}) + changeset end attributes do diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index b603231..910192d 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -60,6 +60,20 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do +
+

Customer

+ + <%!-- <.input + field={@form[:customer_id]} + type="select" + options={ + for customer <- @customers do + {customer.name, customer.id} + end + } + /> --%> +
+ @@ -236,14 +250,21 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do
- (Subtotal) $ {@sales_order.total_price.amount |> Decimal.to_float()} + (Subtotal) $ {if @sales_order.total_price, + do: @sales_order.total_price.amount |> Decimal.to_float(), + else: 0}
- (Cost) $ {@sales_order.total_cost.amount |> Decimal.to_float()} + (Cost) $ {if @sales_order.total_cost, + do: @sales_order.total_cost.amount |> Decimal.to_float(), + else: 0}

- (Net) $ {Money.sub!(@sales_order.total_price, @sales_order.total_cost).amount + (Net) $ {Money.sub!( + @sales_order.total_price || Money.new(0, :USD), + @sales_order.total_cost || Money.new(0, :USD) + ).amount |> Decimal.to_float()}
@@ -298,7 +319,8 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do load: [ :total_price, :total_cost, - sales_lines: [:total_price, :product, :active_price, :calculated_total_price, :total_cost] + sales_lines: [:total_price, :product, :active_price, :calculated_total_price, :total_cost], + customer: [:addresses] ] ) end @@ -328,6 +350,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> assign(:drop_sales, 0) |> assign(:total_price_changes, %{}) |> assign(:total_cost_changes, %{}) + |> assign(:customers, TheronsErp.People.list_people!()) |> assign_form()} end diff --git a/priv/repo/migrations/20250213040919_add_addresses_to_sales_orders.exs b/priv/repo/migrations/20250213040919_add_addresses_to_sales_orders.exs new file mode 100644 index 0000000..6e51981 --- /dev/null +++ b/priv/repo/migrations/20250213040919_add_addresses_to_sales_orders.exs @@ -0,0 +1,29 @@ +defmodule TheronsErp.Repo.Migrations.AddAddressesToSalesOrders do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:sales_orders) do + add :address_id, + references(:addresses, + column: :id, + name: "sales_orders_address_id_fkey", + type: :uuid, + prefix: "public" + ) + end + end + + def down do + drop constraint(:sales_orders, "sales_orders_address_id_fkey") + + alter table(:sales_orders) do + remove :address_id + end + end +end diff --git a/priv/resource_snapshots/repo/sales_orders/20250213040919.json b/priv/resource_snapshots/repo/sales_orders/20250213040919.json new file mode 100644 index 0000000..fe257c9 --- /dev/null +++ b/priv/resource_snapshots/repo/sales_orders/20250213040919.json @@ -0,0 +1,127 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": true, + "primary_key?": false, + "references": null, + "size": null, + "source": "identifier", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "sales_orders_customer_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "entities" + }, + "size": null, + "source": "customer_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "sales_orders_address_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "addresses" + }, + "size": null, + "source": "address_id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "\"draft\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "D3B5681CCC25F82D153DB35482A9F5B261886B3A66CD7E20A3338E9C0C89399B", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "sales_orders" +} \ No newline at end of file From 626bf2484c9f5f3b54ab38277005604a1d30bbc8 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Thu, 13 Feb 2025 11:10:12 -0500 Subject: [PATCH 50/90] Progress --- lib/therons_erp/sales/sales_order.ex | 17 +-- lib/therons_erp_web/breadcrumbs.ex | 4 + .../live/sales_order_live/show.ex | 107 +++++++++++++++--- lib/therons_erp_web/selects.ex | 5 + 4 files changed, 103 insertions(+), 30 deletions(-) diff --git a/lib/therons_erp/sales/sales_order.ex b/lib/therons_erp/sales/sales_order.ex index 3a92194..b395a7f 100644 --- a/lib/therons_erp/sales/sales_order.ex +++ b/lib/therons_erp/sales/sales_order.ex @@ -48,37 +48,28 @@ defmodule TheronsErp.Sales.SalesOrder do end create :create do + accept [:customer_id, :address_id] primary? true argument :sales_lines, {:array, :map} change manage_relationship(:sales_lines, type: :create), where: [attribute_equals(:state, :draft)] - argument :customer_id, :uuid - argument :address, {:array, :string} - - change &update_customer/2, where: [attribute_equals(:state, :draft)] + # TODO validate address belongs to customer end update :update do + accept [:customer_id, :address_id] require_atomic? false argument :sales_lines, {:array, :map} change manage_relationship(:sales_lines, type: :direct_control), where: [attribute_equals(:state, :draft)] - argument :customer_id, :uuid - argument :address, {:array, :string} - - change &update_customer/2, where: [attribute_equals(:state, :draft)] + # TODO validate address belongs to customer end end - def update_customer(changeset, context) do - IO.inspect({changeset, context}) - changeset - end - attributes do uuid_primary_key :id diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index faef13f..f982c7c 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -166,6 +166,10 @@ defmodule TheronsErpWeb.Breadcrumbs do ~p"/products/#{id}?#{[breadcrumbs: append_and_encode(breadcrumbs, from)]}" end + def navigate_to_url(breadcrumbs, {"entities", id, _name}, from) do + ~p"/people/#{id}?#{[breadcrumbs: append_and_encode(breadcrumbs, from)]}" + end + defp append_and_encode(breadcrumbs, breadcrumb) do [breadcrumb | breadcrumbs] |> encode_breadcrumbs() diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 910192d..42c69d1 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -63,15 +63,36 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do

Customer

- <%!-- <.input + <.live_select field={@form[:customer_id]} - type="select" - options={ - for customer <- @customers do - {customer.name, customer.id} - end - } - /> --%> + options={@default_customers} + inline={true} + update_min_len={0} + phx-focus="set-default-customers" + container_class="inline-container" + text_input_class="inline-text-input" + dropdown_class="inline-dropdown" + label="" + > + <:option :let={opt}> + <.highlight matches={opt.matches} string={opt.label} value={opt.value} /> + + <:inject_adjacent> + <%= if Phoenix.HTML.Form.input_value(@form, :customer_id) do %> + + <.link navigate={ + TheronsErpWeb.Breadcrumbs.navigate_to_url( + @breadcrumbs, + {"entities", Phoenix.HTML.Form.input_value(@form, :customer_id), ""}, + {"sales_orders", @sales_order.id, @params, @sales_order.identifier} + ) + }> + <.icon name="hero-arrow-right" /> + + + <% end %> + +
@@ -325,6 +346,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do ) end + defp get_initial_customer_options(selected) do + get_customers(selected) + end + @impl true def handle_params(%{"id" => id} = params, _, socket) do sales_order = load_by_id(id, socket) @@ -350,7 +375,8 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> assign(:drop_sales, 0) |> assign(:total_price_changes, %{}) |> assign(:total_cost_changes, %{}) - |> assign(:customers, TheronsErp.People.list_people!()) + |> assign(:set_customer, %{text: nil, value: nil}) + |> assign(:default_customers, get_initial_customer_options(sales_order.customer_id)) |> assign_form()} end @@ -490,14 +516,35 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do socket.assigns.sales_order.identifier} )} else - form = AshPhoenix.Form.validate(socket.assigns.form, sales_order_params) - drop = length(sales_order_params["_drop_sales_lines"] || []) - - {:noreply, - assign(socket, form: form) - |> assign(:unsaved_changes, form.source.changed? || drop > 0) - |> assign(:params, sales_order_params) - |> assign(:drop_sales, drop)} + if sales_order_params["customer_id"] == "create" do + sid = socket.assigns.sales_order.id + + {:noreply, + socket + |> Breadcrumbs.navigate_to( + {"entities", "new", sid}, + {"sales_orders", socket.assigns.sales_order.id, sales_order_params, + socket.assigns.sales_order.identifier} + )} + else + set_customer = + socket.assigns.default_customers + |> Enum.find(fn c -> c.value == sales_order_params["customer_id"] end) + |> case do + nil -> %{text: nil, value: nil} + c -> %{text: c.label, value: c.value} + end + + form = AshPhoenix.Form.validate(socket.assigns.form, sales_order_params) + drop = length(sales_order_params["_drop_sales_lines"] || []) + + {:noreply, + assign(socket, form: form) + |> assign(:unsaved_changes, form.source.changed? || drop > 0) + |> assign(:set_customer, set_customer) + |> assign(:params, sales_order_params) + |> assign(:drop_sales, drop)} + end end end @@ -739,6 +786,32 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket} end + @impl true + def handle_event( + "set-default-customers", + %{ + "id" => id + }, + socket + ) do + if cid = socket.assigns.from_args["customer_id"] do + opts = get_initial_customer_options(cid) + send_update(LiveSelect.Component, options: opts, id: id, value: cid) + else + text = + socket.assigns.set_customer.text || + (socket.assigns.sales_order.customer && socket.assigns.sales_order.customer.name) || "" + + value = socket.assigns.set_customer.value || socket.assigns.sales_order.customer_id + + opts = prepare_matches(socket.assigns.default_customers, text) + + send_update(LiveSelect.Component, options: opts, id: id, value: value) + end + + {:noreply, socket} + end + @impl true def handle_event( "set-default", diff --git a/lib/therons_erp_web/selects.ex b/lib/therons_erp_web/selects.ex index 4db03c7..c06fd65 100644 --- a/lib/therons_erp_web/selects.ex +++ b/lib/therons_erp_web/selects.ex @@ -1,4 +1,5 @@ defmodule TheronsErpWeb.Selects do + alias TheronsErp.People alias TheronsErp.Inventory def prepare_matches(items, text) do @@ -20,6 +21,10 @@ defmodule TheronsErpWeb.Selects do _get_list(Inventory.get_products!(), selected, & &1.name) end + def get_customers(selected) do + _get_list(People.list_people!(), selected, & &1.name) + end + defp _get_list(items, selected, mapper) do list = items From e97d02ac7c44122134640a84153d80c31704a218 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Thu, 13 Feb 2025 11:20:29 -0500 Subject: [PATCH 51/90] Customer create --- .../live/sales_order_live/show.ex | 52 ++++++++++++++----- lib/therons_erp_web/selects.ex | 14 +++++ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 42c69d1..1aa61ac 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -346,10 +346,6 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do ) end - defp get_initial_customer_options(selected) do - get_customers(selected) - end - @impl true def handle_params(%{"id" => id} = params, _, socket) do sales_order = load_by_id(id, socket) @@ -729,15 +725,29 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do AshPhoenix.Form.validate(form, new_args) _ -> - new_args = - put_in( - Map.merge(args, from_args), - ["sales_lines", from_args["line_id"], "product_id"], - from_args["product_id"] - ) - - update_live_forms(new_args) - AshPhoenix.Form.validate(form, new_args) + cond do + from_args["line_id"] -> + new_args = + put_in( + Map.merge(args, from_args), + ["sales_lines", from_args["line_id"], "product_id"], + from_args["product_id"] + ) + + update_live_forms(new_args) + AshPhoenix.Form.validate(form, new_args) + + from_args["customer_id"] -> + new_args = + put_in( + Map.merge(args, from_args), + ["customer_id"], + from_args["customer_id"] + ) + + update_live_forms(new_args) + AshPhoenix.Form.validate(form, new_args) + end end socket @@ -774,7 +784,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do def handle_event( "live_select_change", - %{"text" => text, "id" => id}, + %{"text" => text, "id" => "sales_order[sales_lines]" <> _ = id}, socket ) do opts = @@ -786,6 +796,20 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket} end + def handle_event( + "live_select_change", + %{"text" => text, "id" => "sales_order_customer_id" <> _ = id}, + socket + ) do + opts = + get_customers("") + |> prepare_matches(text) + + send_update(LiveSelect.Component, id: id, options: opts) + + {:noreply, socket} + end + @impl true def handle_event( "set-default-customers", diff --git a/lib/therons_erp_web/selects.ex b/lib/therons_erp_web/selects.ex index c06fd65..5941044 100644 --- a/lib/therons_erp_web/selects.ex +++ b/lib/therons_erp_web/selects.ex @@ -66,10 +66,24 @@ defmodule TheronsErpWeb.Selects do ] end + def additional_customer_options do + [ + %{ + value: :create, + label: "Create New", + matches: [] + } + ] + end + def get_initial_options(selected) do (get_categories(selected) |> Enum.uniq() |> Enum.take(4)) ++ additional_options() end + def get_initial_customer_options(selected) do + (get_customers(selected) |> Enum.uniq() |> Enum.take(4)) ++ additional_customer_options() + end + def get_initial_product_options(selected) do (get_products(selected) |> Enum.uniq() From 44e64cf6e1296a7aa85621806f31ec5a13d24c50 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Thu, 13 Feb 2025 22:48:21 -0500 Subject: [PATCH 52/90] Work on address selection --- lib/therons_erp_web/live/sales_order_live/show.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 1aa61ac..3c8d739 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -4,6 +4,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do import TheronsErpWeb.Selects import TheronsErpWeb.Layouts + # TODO address should be selectable when customer is changed but not persisted + # TODO address should be reset when customer changed + @impl true def render(assigns) do ~H""" @@ -93,6 +96,14 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <% end %> + + <%= if Phoenix.HTML.Form.input_value(@form, :customer_id) do %> + <.input + field={@form[:address_id]} + type="select" + options={Enum.map(@sales_order.customer.addresses, &{&1.address, &1.id})} + /> + <% end %>
@@ -340,6 +351,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do load: [ :total_price, :total_cost, + :address, sales_lines: [:total_price, :product, :active_price, :calculated_total_price, :total_cost], customer: [:addresses] ] From f0fb44ea018ad9b4abdf87ca41f818a1146e6ce7 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Thu, 13 Feb 2025 22:48:51 -0500 Subject: [PATCH 53/90] Add credo --- mix.exs | 3 ++- mix.lock | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 23e1a38..21a02aa 100644 --- a/mix.exs +++ b/mix.exs @@ -76,7 +76,8 @@ defmodule TheronsErp.MixProject do {:live_select, "~> 1.0"}, {:seqfuzz, "~> 0.2.0"}, {:igniter, "~> 0.5", only: [:dev, :test]}, - {:petal_components, "~> 2.8"} + {:petal_components, "~> 2.8"}, + {:credo, "~> 1.7"} ] end diff --git a/mix.lock b/mix.lock index fa80c5b..3f52e1b 100644 --- a/mix.lock +++ b/mix.lock @@ -14,9 +14,11 @@ "assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"}, "bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, "cldr_utils": {:hex, :cldr_utils, "2.28.2", "f500667164a9043369071e4f9dcef31f88b8589b2e2c07a1eb9f9fa53cb1dce9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c506eb1a170ba7cdca59b304ba02a56795ed119856662f6b1a420af80ec42551"}, "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"}, + "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, From 0c2f01b16069ad10e0e4b37e90e3f6859a06c1b2 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Thu, 13 Feb 2025 22:51:45 -0500 Subject: [PATCH 54/90] use reraise instead of raise --- lib/therons_erp_web/live/product_category_live/show.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/therons_erp_web/live/product_category_live/show.ex b/lib/therons_erp_web/live/product_category_live/show.ex index 905a177..6b76c89 100644 --- a/lib/therons_erp_web/live/product_category_live/show.ex +++ b/lib/therons_erp_web/live/product_category_live/show.ex @@ -295,11 +295,11 @@ defmodule TheronsErpWeb.ProductCategoryLive.Show do {:noreply, socket |> put_flash(:error, "Cannot create a cycle in the product category tree")} else - raise e + reraise(e) end _ -> - raise e + reraise(e) end end end From 280579f3946fff68e79d1d461ef4fa88c6aca3e0 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Thu, 13 Feb 2025 22:51:49 -0500 Subject: [PATCH 55/90] remove io inspect --- lib/therons_erp_web/live/entity_live/form_component.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/therons_erp_web/live/entity_live/form_component.ex b/lib/therons_erp_web/live/entity_live/form_component.ex index 2356181..dff334a 100644 --- a/lib/therons_erp_web/live/entity_live/form_component.ex +++ b/lib/therons_erp_web/live/entity_live/form_component.ex @@ -152,7 +152,6 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do {:noreply, socket} {:error, form} -> - IO.inspect(form) {:noreply, assign(socket, form: form)} end end From 72ef6c18931fd14807f44ca19629e1b2487cdf7c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Thu, 13 Feb 2025 23:39:50 -0500 Subject: [PATCH 56/90] Addresses --- lib/therons_erp/people/address.ex | 61 +++++++- lib/therons_erp_web/breadcrumbs.ex | 11 ++ .../live/entity_live/form_component.ex | 58 +------- .../entity_live/new_address_form_component.ex | 91 ++++++++++++ lib/therons_erp_web/live/entity_live/show.ex | 35 ++++- .../live/product_category_live/show.ex | 4 +- .../live/sales_order_live/show.ex | 75 ++++++---- lib/therons_erp_web/router.ex | 1 + ...0214041703_add_phone_number_to_address.exs | 21 +++ .../repo/addresses/20250214041703.json | 138 ++++++++++++++++++ 10 files changed, 406 insertions(+), 89 deletions(-) create mode 100644 lib/therons_erp_web/live/entity_live/new_address_form_component.ex create mode 100644 priv/repo/migrations/20250214041703_add_phone_number_to_address.exs create mode 100644 priv/resource_snapshots/repo/addresses/20250214041703.json diff --git a/lib/therons_erp/people/address.ex b/lib/therons_erp/people/address.ex index 28f1b78..932d111 100644 --- a/lib/therons_erp/people/address.ex +++ b/lib/therons_erp/people/address.ex @@ -1,4 +1,59 @@ defmodule TheronsErp.People.Address do + def state_options do + [ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DE", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WV", + "WI", + "WY" + ] + end + use Ash.Resource, otp_app: :therons_erp, domain: TheronsErp.People, @@ -14,12 +69,12 @@ defmodule TheronsErp.People.Address do create :create do primary? true - accept [:address, :address2, :city, :state, :zip_code] + accept [:address, :address2, :city, :state, :zip_code, :phone, :entity_id] end update :update do primary? true - accept [:address, :address2, :city, :state, :zip_code] + accept [:address, :address2, :city, :state, :zip_code, :phone, :entity_id] end destroy :destroy do @@ -35,6 +90,8 @@ defmodule TheronsErp.People.Address do attribute :city, :string attribute :state, :string attribute :zip_code, :string + attribute :phone, :string + timestamps() end diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index f982c7c..135ab6a 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -98,6 +98,13 @@ defmodule TheronsErpWeb.Breadcrumbs do defp _navigate_back([breadcrumb | breadcrumbs], _from, from_args) do which = case breadcrumb do + {"people", args, id} -> + if from_args do + ~p"/people/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), args: args, from_args: from_args]}" + else + ~p"/people/#{id}?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), args: args]}" + end + {"products", "new", args} -> if from_args do ~p"/products/new?#{[breadcrumbs: encode_breadcrumbs(breadcrumbs), args: args, from_args: from_args]}" @@ -170,6 +177,10 @@ defmodule TheronsErpWeb.Breadcrumbs do ~p"/people/#{id}?#{[breadcrumbs: append_and_encode(breadcrumbs, from)]}" end + def navigate_to_url(breadcrumbs, {"addresses", "new", customer_id}, from) do + ~p"/people/#{customer_id}/new_address?#{[breadcrumbs: append_and_encode(breadcrumbs, from)]}" + end + defp append_and_encode(breadcrumbs, breadcrumb) do [breadcrumb | breadcrumbs] |> encode_breadcrumbs() diff --git a/lib/therons_erp_web/live/entity_live/form_component.ex b/lib/therons_erp_web/live/entity_live/form_component.ex index dff334a..b9d5908 100644 --- a/lib/therons_erp_web/live/entity_live/form_component.ex +++ b/lib/therons_erp_web/live/entity_live/form_component.ex @@ -1,9 +1,8 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do + # I don't think this is used because the new action creates use TheronsErpWeb, :live_component alias TheronsErpWeb.Breadcrumbs - # TODO add delete button - @impl true def render(assigns) do ~H""" @@ -13,7 +12,6 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do <:subtitle> - <%!-- Delete button using POST --%> <%= if @entity do %> <.button phx-click="delete" @@ -56,60 +54,10 @@ defmodule TheronsErpWeb.EntityLive.FormComponent do field={address[:state]} type="select" label="State" - options={[ - "AL", - "AK", - "AZ", - "AR", - "CA", - "CO", - "CT", - "DE", - "FL", - "GA", - "HI", - "ID", - "IL", - "IN", - "IA", - "KS", - "KY", - "LA", - "ME", - "MD", - "MA", - "MI", - "MN", - "MS", - "MO", - "MT", - "NE", - "NV", - "NH", - "NJ", - "NM", - "NY", - "NC", - "ND", - "OH", - "OK", - "OR", - "PA", - "RI", - "SC", - "SD", - "TN", - "TX", - "UT", - "VT", - "VA", - "WA", - "WV", - "WI", - "WY" - ]} + options={TheronsErp.People.Address.state_options()} /> <.input field={address[:zip_code]} type="text" label="Zip Code" pattern="[0-9]{5}" /> + <.input field={address[:phone]} type="text" label="Phone" /> diff --git a/lib/therons_erp_web/live/entity_live/new_address_form_component.ex b/lib/therons_erp_web/live/entity_live/new_address_form_component.ex new file mode 100644 index 0000000..4192a1f --- /dev/null +++ b/lib/therons_erp_web/live/entity_live/new_address_form_component.ex @@ -0,0 +1,91 @@ +defmodule TheronsErpWeb.EntityLive.NewAddressFormComponent do + use TheronsErpWeb, :live_component + alias TheronsErpWeb.Breadcrumbs + + @impl true + def render(assigns) do + ~H""" +
+ <.header> + New address for {@entity.name} + + <.simple_form + for={@form} + id="address-form" + phx-target={@myself} + phx-change="validate" + phx-submit="save" + > + <.input field={@form[:address]} label="Address" required /> + <.input field={@form[:address2]} label="Address2" /> + <.input field={@form[:city]} label="City" required /> + <.input + field={@form[:state]} + type="select" + label="State" + options={TheronsErp.People.Address.state_options()} + /> + <.input field={@form[:zip_code]} label="Zip code" required /> + <.input field={@form[:phone]} label="Phone" required /> + + <:actions> + <.button phx-disable-with="Saving...">Save Address + + +
+ """ + end + + @impl true + def update(assigns, socket) do + {:ok, socket |> assign(assigns) |> assign(:address, nil) |> assign_form()} + end + + @impl true + def handle_event("validate", %{"address" => address_params}, socket) do + {:noreply, + assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, address_params))} + end + + def handle_event("save", %{"address" => address_params}, socket) do + case AshPhoenix.Form.submit(socket.assigns.form, params: address_params) do + {:ok, address} -> + notify_parent({:saved, address}) + + socket = + socket + |> put_flash(:info, "Address #{socket.assigns.form.source.type}d successfully") + |> Breadcrumbs.navigate_back({"people", address.entity_id}, %{ + address_id: address.id + }) + + {:noreply, socket} + + {:error, form} -> + {:noreply, assign(socket, form: form)} + end + end + + defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) + + defp assign_form(%{assigns: %{address: address, entity: entity}} = socket) do + form = + if address do + AshPhoenix.Form.for_update(address, :update, + as: "address", + actor: socket.assigns.current_user + ) + else + AshPhoenix.Form.for_create(TheronsErp.People.Address, :create, + as: "address", + actor: socket.assigns.current_user, + prepare_source: fn source -> + source + |> Ash.Changeset.change_attribute(:entity_id, entity.id) + end + ) + end + + assign(socket, form: to_form(form)) + end +end diff --git a/lib/therons_erp_web/live/entity_live/show.ex b/lib/therons_erp_web/live/entity_live/show.ex index af75235..d63e828 100644 --- a/lib/therons_erp_web/live/entity_live/show.ex +++ b/lib/therons_erp_web/live/entity_live/show.ex @@ -26,7 +26,14 @@ defmodule TheronsErpWeb.EntityLive.Show do :if={@live_action == :edit} id="entity-modal" show - on_cancel={JS.patch(~p"/people/#{@entity}")} + on_cancel={ + JS.navigate( + TheronsErpWeb.Breadcrumbs.get_previous_path( + @breadcrumbs, + {"people", @entity.id} + ) + ) + } > <.live_component module={TheronsErpWeb.EntityLive.FormComponent} @@ -39,6 +46,31 @@ defmodule TheronsErpWeb.EntityLive.Show do breadcrumbs={@breadcrumbs} /> + + <.modal + :if={@live_action == :new_address} + id="entity-modal" + show + on_cancel={ + JS.navigate( + TheronsErpWeb.Breadcrumbs.get_previous_path( + @breadcrumbs, + {"people", @entity.id} + ) + ) + } + > + <.live_component + module={TheronsErpWeb.EntityLive.NewAddressFormComponent} + id={@entity.id} + title={@page_title} + action={@live_action} + current_user={@current_user} + entity={@entity} + patch={~p"/people/#{@entity}"} + breadcrumbs={@breadcrumbs} + /> + """ end @@ -60,4 +92,5 @@ defmodule TheronsErpWeb.EntityLive.Show do defp page_title(:show), do: "Show Entity" defp page_title(:edit), do: "Edit Entity" + defp page_title(:new_address), do: "New Address" end diff --git a/lib/therons_erp_web/live/product_category_live/show.ex b/lib/therons_erp_web/live/product_category_live/show.ex index 6b76c89..9487b15 100644 --- a/lib/therons_erp_web/live/product_category_live/show.ex +++ b/lib/therons_erp_web/live/product_category_live/show.ex @@ -295,11 +295,11 @@ defmodule TheronsErpWeb.ProductCategoryLive.Show do {:noreply, socket |> put_flash(:error, "Cannot create a cycle in the product category tree")} else - reraise(e) + reraise e, __STACKTRACE__ end _ -> - reraise(e) + reraise e, __STACKTRACE__ end end end diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 3c8d739..fcaf13f 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -101,7 +101,10 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.input field={@form[:address_id]} type="select" - options={Enum.map(@sales_order.customer.addresses, &{&1.address, &1.id})} + options={ + Enum.map(@sales_order.customer.addresses, &{&1.address, &1.id}) ++ + [{"Create new", "create"}] + } /> <% end %> @@ -515,43 +518,54 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do _ -> nil end - if has_create do + if sales_order_params["address_id"] == "create" and + sales_order_params["customer_id"] not in [nil, "create"] do {:noreply, socket |> Breadcrumbs.navigate_to( - {"products", "new", has_create}, + {"addresses", "new", sales_order_params["customer_id"]}, {"sales_orders", socket.assigns.sales_order.id, sales_order_params, socket.assigns.sales_order.identifier} )} else - if sales_order_params["customer_id"] == "create" do - sid = socket.assigns.sales_order.id - + if has_create do {:noreply, socket |> Breadcrumbs.navigate_to( - {"entities", "new", sid}, + {"products", "new", has_create}, {"sales_orders", socket.assigns.sales_order.id, sales_order_params, socket.assigns.sales_order.identifier} )} else - set_customer = - socket.assigns.default_customers - |> Enum.find(fn c -> c.value == sales_order_params["customer_id"] end) - |> case do - nil -> %{text: nil, value: nil} - c -> %{text: c.label, value: c.value} - end - - form = AshPhoenix.Form.validate(socket.assigns.form, sales_order_params) - drop = length(sales_order_params["_drop_sales_lines"] || []) - - {:noreply, - assign(socket, form: form) - |> assign(:unsaved_changes, form.source.changed? || drop > 0) - |> assign(:set_customer, set_customer) - |> assign(:params, sales_order_params) - |> assign(:drop_sales, drop)} + if sales_order_params["customer_id"] == "create" do + sid = socket.assigns.sales_order.id + + {:noreply, + socket + |> Breadcrumbs.navigate_to( + {"entities", "new", sid}, + {"sales_orders", socket.assigns.sales_order.id, sales_order_params, + socket.assigns.sales_order.identifier} + )} + else + set_customer = + socket.assigns.default_customers + |> Enum.find(fn c -> c.value == sales_order_params["customer_id"] end) + |> case do + nil -> %{text: nil, value: nil} + c -> %{text: c.label, value: c.value} + end + + form = AshPhoenix.Form.validate(socket.assigns.form, sales_order_params) + drop = length(sales_order_params["_drop_sales_lines"] || []) + + {:noreply, + assign(socket, form: form) + |> assign(:unsaved_changes, form.source.changed? || drop > 0) + |> assign(:set_customer, set_customer) + |> assign(:params, sales_order_params) + |> assign(:drop_sales, drop)} + end end end end @@ -751,11 +765,14 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do from_args["customer_id"] -> new_args = - put_in( - Map.merge(args, from_args), - ["customer_id"], - from_args["customer_id"] - ) + Map.merge(args, from_args) + + update_live_forms(new_args) + AshPhoenix.Form.validate(form, new_args) + + from_args["address_id"] -> + new_args = + Map.merge(args, from_args) update_live_forms(new_args) AshPhoenix.Form.validate(form, new_args) diff --git a/lib/therons_erp_web/router.ex b/lib/therons_erp_web/router.ex index 4cc0f90..f5cdf12 100644 --- a/lib/therons_erp_web/router.ex +++ b/lib/therons_erp_web/router.ex @@ -78,6 +78,7 @@ defmodule TheronsErpWeb.Router do live "/people/:id/edit", EntityLive.Index, :edit live "/people/:id", EntityLive.Show, :show + live "/people/:id/new_address", EntityLive.Show, :new_address live "/people/:id/show/edit", EntityLive.Show, :edit end diff --git a/priv/repo/migrations/20250214041703_add_phone_number_to_address.exs b/priv/repo/migrations/20250214041703_add_phone_number_to_address.exs new file mode 100644 index 0000000..f2149ab --- /dev/null +++ b/priv/repo/migrations/20250214041703_add_phone_number_to_address.exs @@ -0,0 +1,21 @@ +defmodule TheronsErp.Repo.Migrations.AddPhoneNumberToAddress do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:addresses) do + add :phone, :text + end + end + + def down do + alter table(:addresses) do + remove :phone + end + end +end diff --git a/priv/resource_snapshots/repo/addresses/20250214041703.json b/priv/resource_snapshots/repo/addresses/20250214041703.json new file mode 100644 index 0000000..c16c664 --- /dev/null +++ b/priv/resource_snapshots/repo/addresses/20250214041703.json @@ -0,0 +1,138 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "address", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "address2", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "city", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "zip_code", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "phone", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "addresses_entity_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "entities" + }, + "size": null, + "source": "entity_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "1B145B96FFFBA0EFA920F95A79FC43C100F3F2FE5B1B7CFC27BF2F6FC641F249", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "addresses" +} \ No newline at end of file From 96dd1aebe74928b284e977b82f849f210b9f78c8 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Fri, 14 Feb 2025 11:46:46 -0500 Subject: [PATCH 57/90] Style address --- assets/css/app.css | 10 ++++++ .../live/sales_order_live/show.ex | 33 ++++++++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 75d5c33..aad21ea 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -138,3 +138,13 @@ } } } + +.address-container { + display: inline-block; + width: 400px; +} + +.address-container div select { + display: inline-block; + width: 200px; +} diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index fcaf13f..f9cf752 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -75,7 +75,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do container_class="inline-container" text_input_class="inline-text-input" dropdown_class="inline-dropdown" - label="" + label="Customer" > <:option :let={opt}> <.highlight matches={opt.matches} string={opt.label} value={opt.value} /> @@ -98,14 +98,29 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <%= if Phoenix.HTML.Form.input_value(@form, :customer_id) do %> - <.input - field={@form[:address_id]} - type="select" - options={ - Enum.map(@sales_order.customer.addresses, &{&1.address, &1.id}) ++ - [{"Create new", "create"}] - } - /> +
+ <.input + field={@form[:address_id]} + type="select" + label="Address" + class="address-select" + options={ + Enum.map(@sales_order.customer.addresses, &{&1.address, &1.id}) ++ + [{"Create new", "create"}] + } + /> +
+ + <.link navigate={ + TheronsErpWeb.Breadcrumbs.navigate_to_url( + @breadcrumbs, + {"entities", Phoenix.HTML.Form.input_value(@form, :customer_id), ""}, + {"sales_orders", @sales_order.id, @params, @sales_order.identifier} + ) + }> + <.icon name="hero-arrow-right" /> + + <% end %> From dfc1bc25ec7581794c710595cc3fdf2cc98f0b3a Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Fri, 14 Feb 2025 11:59:42 -0500 Subject: [PATCH 58/90] fix address when customer changes --- .../live/sales_order_live/show.ex | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index f9cf752..4a9f2d5 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -103,9 +103,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do field={@form[:address_id]} type="select" label="Address" - class="address-select" options={ - Enum.map(@sales_order.customer.addresses, &{&1.address, &1.id}) ++ + [{"Unselected", nil}] ++ + Enum.map(@addresses, &{&1.address, &1.id}) ++ [{"Create new", "create"}] } /> @@ -402,6 +402,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> assign(:total_price_changes, %{}) |> assign(:total_cost_changes, %{}) |> assign(:set_customer, %{text: nil, value: nil}) + |> assign(:addresses, sales_order.customer.addresses) |> assign(:default_customers, get_initial_customer_options(sales_order.customer_id)) |> assign_form()} end @@ -512,7 +513,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end @impl true - def handle_event("validate", %{"sales_order" => sales_order_params} = params, socket) do + def handle_event( + "validate", + %{"sales_order" => sales_order_params, "_target" => target} = params, + socket + ) do sales_order_params = process_modifications(sales_order_params, socket) socket = @@ -534,7 +539,8 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end if sales_order_params["address_id"] == "create" and - sales_order_params["customer_id"] not in [nil, "create"] do + sales_order_params["customer_id"] not in [nil, "create"] and + target != ["sales_order", "customer_id"] do {:noreply, socket |> Breadcrumbs.navigate_to( @@ -571,6 +577,13 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do c -> %{text: c.label, value: c.value} end + # If address_id not in customer addresses we want to reset the sales_order_params["address_id"] to nil + # Load the customer by sales_order_params["customer_id"] + customer = + Ash.get!(TheronsErp.People.Entity, sales_order_params["customer_id"], + load: [:addresses] + ) + form = AshPhoenix.Form.validate(socket.assigns.form, sales_order_params) drop = length(sales_order_params["_drop_sales_lines"] || []) @@ -578,6 +591,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do assign(socket, form: form) |> assign(:unsaved_changes, form.source.changed? || drop > 0) |> assign(:set_customer, set_customer) + |> assign(:addresses, customer.addresses) |> assign(:params, sales_order_params) |> assign(:drop_sales, drop)} end From 49a4da58c630ac10dcb92d77a34715afb3a833ed Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Fri, 14 Feb 2025 12:15:21 -0500 Subject: [PATCH 59/90] Format address --- assets/css/app.css | 31 +++++++++++ .../live/sales_order_live/show.ex | 55 ++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/assets/css/app.css b/assets/css/app.css index aad21ea..a35d336 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -46,6 +46,12 @@ } } +.link-to-inside-field.address-link { + span { + left: -135px; + } +} + .revert-button { position: absolute; /* margin-top: 22px; */ @@ -144,7 +150,32 @@ width: 400px; } +.address div { + height: 1.5em; +} + .address-container div select { display: inline-block; width: 200px; } + +.fieldsetlike { + width: 500px; + overflow-x: scroll; + padding: 20px; + border: 1px solid lightgray; +} + +.fieldsetlikelabel { + position: relative; + top: 14px; /* change this how you need */ + left: 20px; /* change this how you need */ + background-color: white; + display: inline-block; +} + +.address-city-state-zip { + div { + display: inline-block; + } +} diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 4a9f2d5..c388291 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -110,7 +110,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do } /> - + <.link navigate={ TheronsErpWeb.Breadcrumbs.navigate_to_url( @breadcrumbs, @@ -122,6 +122,54 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <% end %> + + <%!-- Render address --%> + <%= if get_address(@form, @addresses) do %> +
+ +
+
+ <%!-- Address 1 --%> +
+ + <%!-- Address label --%> + Address + {get_address(@form, @addresses).address} + +
+ <%!-- Address 2 --%> +
+ + Address 2 + {get_address(@form, @addresses).address2} + +
+
+ <%!-- City --%> +
+ City + {get_address(@form, @addresses).city} +
+ <%!-- State --%> +
+ State + {get_address(@form, @addresses).state} +
+ <%!-- Zip Code --%> +
+ Zip Code + {get_address(@form, @addresses).zip_code} +
+
+ <%!-- Phone number --%> +
+ Phone Number + {get_address(@form, @addresses).phone} +
+
+
+
+ <% end %>
@@ -941,4 +989,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket} end + + defp get_address(form, addresses) do + id = Phoenix.HTML.Form.input_value(form, :address_id) + Enum.find(addresses, &(&1.id == id)) + end end From 6c895048188161733b18671795a158a3a1358d2d Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Fri, 14 Feb 2025 12:34:00 -0500 Subject: [PATCH 60/90] Show addresses on entity page --- lib/therons_erp_web/live/entity_live/show.ex | 57 ++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/lib/therons_erp_web/live/entity_live/show.ex b/lib/therons_erp_web/live/entity_live/show.ex index d63e828..de9b4fa 100644 --- a/lib/therons_erp_web/live/entity_live/show.ex +++ b/lib/therons_erp_web/live/entity_live/show.ex @@ -16,9 +16,55 @@ defmodule TheronsErpWeb.EntityLive.Show do - <.list> - <:item title="Id">{@entity.id} - +
+

Addresses

+
+ <%= for address <- @entity.addresses do %> +
+ +
+
+ <%!-- Address 1 --%> +
+ + <%!-- Address label --%> + Address + {address.address} + +
+ <%!-- Address 2 --%> +
+ + Address 2 + {address.address2} + +
+
+ <%!-- City --%> +
+ City + {address.city} +
+ <%!-- State --%> +
+ State + {address.state} +
+ <%!-- Zip Code --%> +
+ Zip Code + {address.zip_code} +
+
+ <%!-- Phone number --%> +
+ Phone Number + {address.phone} +
+
+
+
+ <% end %> <.back navigate={~p"/people"}>Back to entities @@ -86,7 +132,10 @@ defmodule TheronsErpWeb.EntityLive.Show do |> assign(:page_title, page_title(socket.assigns.live_action)) |> assign( :entity, - Ash.get!(TheronsErp.People.Entity, id, actor: socket.assigns.current_user) + Ash.get!(TheronsErp.People.Entity, id, + actor: socket.assigns.current_user, + load: [:addresses] + ) )} end From de0170ee89298fc84023bd47a202c689aa3f0932 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Fri, 14 Feb 2025 12:44:44 -0500 Subject: [PATCH 61/90] Add saleable and purchaseable --- lib/therons_erp/inventory.ex | 1 + lib/therons_erp/inventory/product.ex | 16 +- .../live/product_live/form_component.ex | 5 - lib/therons_erp_web/live/product_live/show.ex | 2 + lib/therons_erp_web/selects.ex | 2 +- ..._saleable_and_purchaseable_to_products.exs | 23 +++ .../repo/products/20250214173939.json | 138 ++++++++++++++++++ 7 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 priv/repo/migrations/20250214173939_add_saleable_and_purchaseable_to_products.exs create mode 100644 priv/resource_snapshots/repo/products/20250214173939.json diff --git a/lib/therons_erp/inventory.ex b/lib/therons_erp/inventory.ex index 8e7e623..fecbd85 100644 --- a/lib/therons_erp/inventory.ex +++ b/lib/therons_erp/inventory.ex @@ -13,6 +13,7 @@ defmodule TheronsErp.Inventory do define :create_product, args: [:name, :sales_price], action: :create define :update_product, args: [:name, :sales_price], action: :update define :create_product_stub, action: :create_stub + define :get_saleable_products, action: :list_saleable define :get_products, action: :list end end diff --git a/lib/therons_erp/inventory/product.ex b/lib/therons_erp/inventory/product.ex index ddbd56e..9f68271 100644 --- a/lib/therons_erp/inventory/product.ex +++ b/lib/therons_erp/inventory/product.ex @@ -15,8 +15,12 @@ defmodule TheronsErp.Inventory.Product do read :list do end + read :list_saleable do + filter expr(saleable == true) + end + create :create do - accept [:name, :sales_price, :type, :category_id] + accept [:name, :sales_price, :type, :category_id, :saleable, :purchaseable] end create :create_stub do @@ -28,7 +32,7 @@ defmodule TheronsErp.Inventory.Product do end update :update do - accept [:name, :sales_price, :type, :category_id] + accept [:name, :sales_price, :type, :category_id, :saleable, :purchaseable] end destroy :destroy do @@ -52,6 +56,14 @@ defmodule TheronsErp.Inventory.Product do default :goods end + attribute :saleable, :boolean do + default true + end + + attribute :purchaseable, :boolean do + default false + end + timestamps() end diff --git a/lib/therons_erp_web/live/product_live/form_component.ex b/lib/therons_erp_web/live/product_live/form_component.ex index 9a3884b..0d61735 100644 --- a/lib/therons_erp_web/live/product_live/form_component.ex +++ b/lib/therons_erp_web/live/product_live/form_component.ex @@ -25,11 +25,6 @@ defmodule TheronsErpWeb.ProductLive.FormComponent do type="text" label="Type" /> - <%!-- <.input - field={@form[:category_id]} - type="text" - label="Category" - /> --%> <.live_select field={@form[:category_id]} diff --git a/lib/therons_erp_web/live/product_live/show.ex b/lib/therons_erp_web/live/product_live/show.ex index b689b28..0992589 100644 --- a/lib/therons_erp_web/live/product_live/show.ex +++ b/lib/therons_erp_web/live/product_live/show.ex @@ -33,6 +33,8 @@ defmodule TheronsErpWeb.ProductLive.Show do <%= if @live_action != :edit do %>
+ <.input field={@form[:saleable]} type="checkbox" label="Saleable" /> + <.input field={@form[:purchaseable]} type="checkbox" label="Purchaseable" /> <.live_select field={@form[:category_id]} label="Category" diff --git a/lib/therons_erp_web/selects.ex b/lib/therons_erp_web/selects.ex index 5941044..11ca157 100644 --- a/lib/therons_erp_web/selects.ex +++ b/lib/therons_erp_web/selects.ex @@ -18,7 +18,7 @@ defmodule TheronsErpWeb.Selects do end def get_products(selected) do - _get_list(Inventory.get_products!(), selected, & &1.name) + _get_list(Inventory.get_saleable_products!(), selected, & &1.name) end def get_customers(selected) do diff --git a/priv/repo/migrations/20250214173939_add_saleable_and_purchaseable_to_products.exs b/priv/repo/migrations/20250214173939_add_saleable_and_purchaseable_to_products.exs new file mode 100644 index 0000000..d0d4e01 --- /dev/null +++ b/priv/repo/migrations/20250214173939_add_saleable_and_purchaseable_to_products.exs @@ -0,0 +1,23 @@ +defmodule TheronsErp.Repo.Migrations.AddSaleableAndPurchaseableToProducts do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:products) do + add :saleable, :boolean, default: true + add :purchaseable, :boolean, default: false + end + end + + def down do + alter table(:products) do + remove :purchaseable + remove :saleable + end + end +end diff --git a/priv/resource_snapshots/repo/products/20250214173939.json b/priv/resource_snapshots/repo/products/20250214173939.json new file mode 100644 index 0000000..62667e7 --- /dev/null +++ b/priv/resource_snapshots/repo/products/20250214173939.json @@ -0,0 +1,138 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": true, + "primary_key?": false, + "references": null, + "size": null, + "source": "identifier", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "sales_price", + "type": "money_with_currency" + }, + { + "allow_nil?": true, + "default": "\"goods\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "type", + "type": "text" + }, + { + "allow_nil?": true, + "default": "true", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "saleable", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "false", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "purchaseable", + "type": "boolean" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "products_category_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "product_categories" + }, + "size": null, + "source": "category_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "925D844AB184B6483A53E8552043B47C861D9CE1D45E879E8F9738EFEE6CB889", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "products" +} \ No newline at end of file From 3d7711c8ed30922b90790805e64cc5011f3844f2 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Fri, 14 Feb 2025 12:45:49 -0500 Subject: [PATCH 62/90] Don't use modal form for editing --- .../live/product_live/form_component.ex | 144 ------------------ .../live/product_live/index.ex | 22 --- lib/therons_erp_web/live/product_live/show.ex | 19 --- lib/therons_erp_web/router.ex | 3 - 4 files changed, 188 deletions(-) delete mode 100644 lib/therons_erp_web/live/product_live/form_component.ex diff --git a/lib/therons_erp_web/live/product_live/form_component.ex b/lib/therons_erp_web/live/product_live/form_component.ex deleted file mode 100644 index 0d61735..0000000 --- a/lib/therons_erp_web/live/product_live/form_component.ex +++ /dev/null @@ -1,144 +0,0 @@ -defmodule TheronsErpWeb.ProductLive.FormComponent do - use TheronsErpWeb, :live_component - alias TheronsErpWeb.Breadcrumbs - import TheronsErpWeb.Selects - - @impl true - def render(assigns) do - ~H""" -
- <.header> - {@title} - <:subtitle>Use this form to manage product records in your database. - - - <.simple_form - for={@form} - id="product-form" - phx-target={@myself} - phx-change="validate" - phx-submit="save" - > - <.input field={@form[:name]} type="text" label="Name" /> - <.input field={@form[:sales_price]} type="text" label="Sales price" /><.input - field={@form[:type]} - type="text" - label="Type" - /> - - <.live_select - field={@form[:category_id]} - label="Category" - phx-target={@myself} - options={@initial_options} - update_min_len={0} - phx-focus="set-default" - > - <:option :let={opt}> - <.highlight matches={opt.matches} string={opt.label} value={opt.value} /> - - - - <:actions> - <.button phx-disable-with="Saving...">Save Product - - -
- """ - end - - @impl true - def handle_event("set-default", %{"id" => id}, socket) do - send_update(LiveSelect.Component, options: socket.assigns.categories, id: id) - - {:noreply, socket} - end - - def handle_event("live_select_change", %{"text" => text, "id" => live_select_id}, socket) do - matches = - Seqfuzz.matches(socket.assigns.categories, text, & &1.label, filter: true, sort: true) - - opts = - (matches - |> Enum.map(fn {categories, c} -> - %{value: categories.value, label: categories.label, matches: c.matches} - end) - |> Enum.take(5)) ++ additional_options() - - send_update(LiveSelect.Component, id: live_select_id, options: opts) - {:noreply, socket} - end - - @impl true - def update(assigns, socket) do - current_category_id = (assigns.product && assigns.product.category_id) || nil - - {:ok, - socket - |> assign(assigns) - |> assign(:categories, get_categories(current_category_id)) - |> assign(:initial_options, get_initial_options(current_category_id)) - |> assign_form()} - end - - @impl true - def handle_event("validate", %{"product" => product_params}, socket) do - if product_params["category_id"] == "create" do - pid = if socket.assigns.product, do: socket.assigns.product.id, else: nil - - {:noreply, - socket - |> Breadcrumbs.navigate_to( - {"product_category", "new", pid}, - {"product", "new", product_params} - )} - else - {:noreply, - assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, product_params))} - end - end - - def handle_event("save", %{"product" => product_params}, socket) do - case AshPhoenix.Form.submit(socket.assigns.form, params: product_params) do - {:ok, product} -> - notify_parent({:saved, product |> Ash.load!(:category)}) - - socket = - socket - |> put_flash(:info, "Product #{socket.assigns.form.source.type}d successfully") - |> push_patch(to: socket.assigns.patch) - - {:noreply, socket} - - {:error, form} -> - {:noreply, assign(socket, form: form)} - end - end - - defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) - - defp assign_form(%{assigns: %{product: product, args: args, from_args: from_args}} = socket) do - form = - if product do - AshPhoenix.Form.for_update(product, :update, - as: "product", - actor: socket.assigns.current_user - ) - else - AshPhoenix.Form.for_create(TheronsErp.Inventory.Product, :create, - as: "product", - actor: socket.assigns.current_user - ) - end - - form = - case {args, from_args} do - {nil, nil} -> form - {_, nil} -> AshPhoenix.Form.validate(form, args) - {nil, _} -> AshPhoenix.Form.validate(form, from_args) - _ -> AshPhoenix.Form.validate(form, Map.merge(args, from_args)) - end - - assign(socket, form: to_form(form)) - end -end diff --git a/lib/therons_erp_web/live/product_live/index.ex b/lib/therons_erp_web/live/product_live/index.ex index 31d101a..69d208b 100644 --- a/lib/therons_erp_web/live/product_live/index.ex +++ b/lib/therons_erp_web/live/product_live/index.ex @@ -27,8 +27,6 @@ defmodule TheronsErpWeb.ProductLive.Index do
<.link navigate={~p"/products/#{product}"}>Show
- - <.link patch={~p"/products/#{product}/edit"}>Edit <:action :let={{id, product}}> @@ -40,26 +38,6 @@ defmodule TheronsErpWeb.ProductLive.Index do - - <.modal - :if={@live_action in [:new, :edit]} - id="product-modal" - show - on_cancel={JS.navigate(~p"/products")} - > - <.live_component - module={TheronsErpWeb.ProductLive.FormComponent} - id={(@product && @product.id) || :new} - title={@page_title} - current_user={@current_user} - action={@live_action} - breadcrumbs={@breadcrumbs} - product={@product} - args={@args} - from_args={@from_args} - patch={~p"/products"} - /> - """ end diff --git a/lib/therons_erp_web/live/product_live/show.ex b/lib/therons_erp_web/live/product_live/show.ex index 0992589..4535778 100644 --- a/lib/therons_erp_web/live/product_live/show.ex +++ b/lib/therons_erp_web/live/product_live/show.ex @@ -73,25 +73,6 @@ defmodule TheronsErpWeb.ProductLive.Show do <% end %> <.back navigate={~p"/products"}>Back to products - <.modal - :if={@live_action == :edit} - id="product-modal" - show - on_cancel={JS.patch(~p"/products/#{@product}")} - > - <.live_component - module={TheronsErpWeb.ProductLive.FormComponent} - id={@product.id} - title={@page_title} - action={@live_action} - current_user={@current_user} - breadcrumbs={@breadcrumbs} - product={@product} - args={@args} - from_args={@from_args} - patch={~p"/products/#{@product}"} - /> - """ end diff --git a/lib/therons_erp_web/router.ex b/lib/therons_erp_web/router.ex index f5cdf12..a6a770a 100644 --- a/lib/therons_erp_web/router.ex +++ b/lib/therons_erp_web/router.ex @@ -61,13 +61,10 @@ defmodule TheronsErpWeb.Router do live "/products", ProductLive.Index, :index live "/products/new", ProductLive.Index, :new - live "/products/:id/edit", ProductLive.Index, :edit live "/products/:id", ProductLive.Show, :show - live "/products/:id/show/edit", ProductLive.Show, :edit live "/sales_orders", SalesOrderLive.Index, :index - # live "/sales_orders/new", SalesOrderLive.Index, :new live "/sales_orders/:id/edit", SalesOrderLive.Index, :edit live "/sales_orders/:id", SalesOrderLive.Show, :show From b4a64bb3a54e51b8cfc969abb3f56ce8a9922609 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Fri, 14 Feb 2025 12:47:47 -0500 Subject: [PATCH 63/90] Render saleable and purchasable on product index page --- lib/therons_erp_web/live/product_live/index.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/therons_erp_web/live/product_live/index.ex b/lib/therons_erp_web/live/product_live/index.ex index 69d208b..1ba0472 100644 --- a/lib/therons_erp_web/live/product_live/index.ex +++ b/lib/therons_erp_web/live/product_live/index.ex @@ -23,6 +23,14 @@ defmodule TheronsErpWeb.ProductLive.Index do {(product.category && product.category.full_name) || ""} + <:col :let={{_id, product}} label="Saleable"> + + + + <:col :let={{_id, product}} label="Purchaseable"> + + + <:action :let={{_id, product}}>
<.link navigate={~p"/products/#{product}"}>Show From e8efddf0d3767c6cff5c03e02d668b3c96227d5c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Fri, 14 Feb 2025 23:37:28 -0500 Subject: [PATCH 64/90] Add comment --- lib/therons_erp_web/live/sales_order_live/show.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index c388291..ab7c82f 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -756,6 +756,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end end + # TODO consider using Ash.calculate!/2 defp total_cost_for_sales_line(sales_line) do data_total_cost = case Phoenix.HTML.Form.input_value(sales_line, :total_cost) do From f997a5278387f15ac4735cdcfdfa2b2c1938b35c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 11:23:33 -0500 Subject: [PATCH 65/90] remove the zero check --- lib/therons_erp_web/live/sales_order_live/show.ex | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index ab7c82f..da2a70a 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -696,8 +696,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do false line_data -> - line_data.total_price != nil and - not Money.equal?(line_data.total_price, Money.new(0, :USD)) + line_data.total_price != nil end end end @@ -712,8 +711,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do false line_data -> - line_data.total_cost != nil and - not Money.equal?(line_data.total_cost, Money.new(0, :USD)) + line_data.total_cost != nil end end end From 50d826dda1907f4aa551aa9258444c4e6c6b00dd Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 11:23:41 -0500 Subject: [PATCH 66/90] use ash calculations --- lib/therons_erp_web/live/sales_order_live/show.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index da2a70a..42068f3 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -747,7 +747,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do "" {_, _} -> - Money.mult!(sales_price, quantity) + Ash.calculate!(TheronsErp.Sales.SalesLine, :calculated_total_price, + refs: %{sales_price: sales_price, quantity: quantity} + ) |> Money.to_decimal() |> Decimal.to_string() end @@ -786,7 +788,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do "" {_, _} -> - Money.mult!(unit_price, quantity) |> Money.to_decimal() |> Decimal.to_string() + Ash.calculate!(TheronsErp.Sales.SalesLine, :total_cost, + refs: %{unit_price: unit_price, quantity: quantity} + ) + |> Money.to_decimal() + |> Decimal.to_string() end end end From 2b986fde5ed7ff3ac7bdbcdf1722952fa00e523b Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 11:33:35 -0500 Subject: [PATCH 67/90] Fix price changes --- .../live/sales_order_live/show.ex | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 42068f3..c106abd 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -250,7 +250,13 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do $ <.input field={sales_line[:total_price]} - value={active_price_for_sales_line(sales_line)} + value={ + active_price_for_sales_line( + sales_line, + sales_line.index, + @total_price_changes + ) + } type="number" inline_container={true} /> @@ -271,7 +277,9 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do $ <.input field={sales_line[:total_cost]} - value={total_cost_for_sales_line(sales_line)} + value={ + total_cost_for_sales_line(sales_line, sales_line.index, @total_cost_changes) + } type="number" inline_container={true} /> @@ -716,7 +724,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end end - defp active_price_for_sales_line(sales_line) do + defp active_price_for_sales_line(sales_line, index, total_price_changes) do data_total_price = case Phoenix.HTML.Form.input_value(sales_line, :total_price) do # In case there's no data source (e.g., new line) @@ -724,7 +732,8 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do total_price -> total_price end - if data_total_price do + if (data_total_price && is_active_price_persisted?(sales_line, index, total_price_changes)) || + total_price_changes[to_string(index)] do case data_total_price do %Money{} -> data_total_price.amount |> Decimal.to_string() @@ -757,7 +766,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do end # TODO consider using Ash.calculate!/2 - defp total_cost_for_sales_line(sales_line) do + defp total_cost_for_sales_line(sales_line, index, total_cost_changes) do data_total_cost = case Phoenix.HTML.Form.input_value(sales_line, :total_cost) do # In case there's no data source (e.g., new line) @@ -765,7 +774,8 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do total_cost -> total_cost end - if data_total_cost do + if (data_total_cost && is_total_cost_persisted?(sales_line, index, total_cost_changes)) || + total_cost_changes[to_string(index)] do case data_total_cost do %Money{} -> data_total_cost.amount |> Decimal.to_string() From 64806e399d21b900d4f272e7e3cf4346dd8fbdbc Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 11:55:40 -0500 Subject: [PATCH 68/90] add product categories link --- .../components/layouts/app.html.heex | 26 +++++++++++++++++++ lib/therons_erp_web/live/nav.ex | 10 ++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/therons_erp_web/components/layouts/app.html.heex b/lib/therons_erp_web/components/layouts/app.html.heex index 47fb028..6b82344 100644 --- a/lib/therons_erp_web/components/layouts/app.html.heex +++ b/lib/therons_erp_web/components/layouts/app.html.heex @@ -118,6 +118,19 @@ text="Products" icon="hero-document" /> +
+
    +
  • + <.nav_link + active={@active_tab} + name={:product_categories} + path={~p"/product_categories"} + text="Categories" + icon="hero-rectangle-group" + /> +
  • +
+
    @@ -185,6 +198,19 @@ text="Products" icon="hero-document" /> +
    +
      +
    • + <.nav_link + active={@active_tab} + name={:product_categories} + path={~p"/product_categories"} + text="Categories" + icon="hero-rectangle-group" + /> +
    • +
    +
    diff --git a/lib/therons_erp_web/live/nav.ex b/lib/therons_erp_web/live/nav.ex index e5f2257..89d4b12 100644 --- a/lib/therons_erp_web/live/nav.ex +++ b/lib/therons_erp_web/live/nav.ex @@ -10,7 +10,8 @@ defmodule TheronsErpWeb.Nav do defp set_active_tab(_params, _url, socket) do active_tab = case {socket.view, socket.assigns.live_action} do - {so, _} when so in [TheronsErpWeb.SalesOrderLive.Index, TheronsErpWeb.SalesOrderLive.Show] -> + {so, _} + when so in [TheronsErpWeb.SalesOrderLive.Index, TheronsErpWeb.SalesOrderLive.Show] -> :sales_orders {po, _} when po in [TheronsErpWeb.ProductLive.Index, TheronsErpWeb.ProductLive.Show] -> @@ -19,6 +20,13 @@ defmodule TheronsErpWeb.Nav do {pp, _} when pp in [TheronsErpWeb.EntityLive.Index, TheronsErpWeb.EntityLive.Show] -> :people + {pc, _} + when pc in [ + TheronsErpWeb.ProductCategoryLive.Index, + TheronsErpWeb.ProductCategoryLive.Show + ] -> + :product_categories + {_, _} -> nil end From 05487e01b5bbcd2215abfbaed81e9adbf68f634c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 12:10:48 -0500 Subject: [PATCH 69/90] adding cost to products --- lib/therons_erp/inventory/product.ex | 5 +- lib/therons_erp_web/live/product_live/show.ex | 28 ++++ .../20250215165722_add_cost_to_products.exs | 21 +++ .../repo/products/20250215165722.json | 148 ++++++++++++++++++ 4 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 priv/repo/migrations/20250215165722_add_cost_to_products.exs create mode 100644 priv/resource_snapshots/repo/products/20250215165722.json diff --git a/lib/therons_erp/inventory/product.ex b/lib/therons_erp/inventory/product.ex index 9f68271..f0ef497 100644 --- a/lib/therons_erp/inventory/product.ex +++ b/lib/therons_erp/inventory/product.ex @@ -20,7 +20,7 @@ defmodule TheronsErp.Inventory.Product do end create :create do - accept [:name, :sales_price, :type, :category_id, :saleable, :purchaseable] + accept [:name, :sales_price, :type, :category_id, :saleable, :purchaseable, :cost] end create :create_stub do @@ -32,7 +32,7 @@ defmodule TheronsErp.Inventory.Product do end update :update do - accept [:name, :sales_price, :type, :category_id, :saleable, :purchaseable] + accept [:name, :sales_price, :type, :category_id, :saleable, :purchaseable, :cost] end destroy :destroy do @@ -51,6 +51,7 @@ defmodule TheronsErp.Inventory.Product do end attribute :sales_price, :money + attribute :cost, :money attribute :type, TheronsErp.Inventory.Product.Types do default :goods diff --git a/lib/therons_erp_web/live/product_live/show.ex b/lib/therons_erp_web/live/product_live/show.ex index 4535778..1d41cc6 100644 --- a/lib/therons_erp_web/live/product_live/show.ex +++ b/lib/therons_erp_web/live/product_live/show.ex @@ -34,7 +34,18 @@ defmodule TheronsErpWeb.ProductLive.Show do <%= if @live_action != :edit do %>
    <.input field={@form[:saleable]} type="checkbox" label="Saleable" /> + <.input field={@form[:purchaseable]} type="checkbox" label="Purchaseable" /> + + <.input field={@form[:cost]} value={do_money(@form[:cost])} type="number" label="Cost" /> + + <.input + field={@form[:sales_price]} + value={do_money(@form[:sales_price])} + type="number" + label="Sales Price" + /> + <.live_select field={@form[:category_id]} label="Category" @@ -235,4 +246,21 @@ defmodule TheronsErpWeb.ProductLive.Show do {:noreply, socket} end + + defp do_money(field) do + case field.value do + nil -> + "" + + "" -> + "" + + %Money{} = money -> + money.amount + |> Decimal.to_float() + + el -> + el + end + end end diff --git a/priv/repo/migrations/20250215165722_add_cost_to_products.exs b/priv/repo/migrations/20250215165722_add_cost_to_products.exs new file mode 100644 index 0000000..d825b18 --- /dev/null +++ b/priv/repo/migrations/20250215165722_add_cost_to_products.exs @@ -0,0 +1,21 @@ +defmodule TheronsErp.Repo.Migrations.AddCostToProducts do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:products) do + add :cost, :money_with_currency + end + end + + def down do + alter table(:products) do + remove :cost + end + end +end diff --git a/priv/resource_snapshots/repo/products/20250215165722.json b/priv/resource_snapshots/repo/products/20250215165722.json new file mode 100644 index 0000000..0fbe899 --- /dev/null +++ b/priv/resource_snapshots/repo/products/20250215165722.json @@ -0,0 +1,148 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": true, + "primary_key?": false, + "references": null, + "size": null, + "source": "identifier", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "sales_price", + "type": "money_with_currency" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "cost", + "type": "money_with_currency" + }, + { + "allow_nil?": true, + "default": "\"goods\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "type", + "type": "text" + }, + { + "allow_nil?": true, + "default": "true", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "saleable", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "false", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "purchaseable", + "type": "boolean" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "products_category_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "product_categories" + }, + "size": null, + "source": "category_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "F31886E3775556805B04D0302335203FAA06802A3E6F4E739C5EE603C6D43DEE", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "products" +} \ No newline at end of file From d7cd012ba752b89d9e1e6bfc02b18019a520d9db Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 12:25:55 -0500 Subject: [PATCH 70/90] Copy over price and cost to sales line --- lib/therons_erp/sales/sales_line.ex | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/therons_erp/sales/sales_line.ex b/lib/therons_erp/sales/sales_line.ex index e8968c8..4213a3c 100644 --- a/lib/therons_erp/sales/sales_line.ex +++ b/lib/therons_erp/sales/sales_line.ex @@ -15,6 +15,20 @@ defmodule TheronsErp.Sales.SalesLine do create :create do primary? true accept [:sales_price, :unit_price, :quantity, :product_id] + + # Set unit_price and sales_price based on product price and cost + change fn changeset, _ -> + # Load the product if it exists + if product_id = Ash.Changeset.get_attribute(changeset, :product_id) do + product = Ash.get!(TheronsErp.Inventory.Product, product_id) + + changeset + |> Ash.Changeset.change_attribute(:unit_price, product.cost) + |> Ash.Changeset.change_attribute(:sales_price, product.sales_price) + else + changeset + end + end end update :update do @@ -29,7 +43,10 @@ defmodule TheronsErp.Sales.SalesLine do attribute :sales_price, :money attribute :unit_price, :money - attribute :quantity, :integer + + attribute :quantity, :integer do + default 1 + end attribute :total_price, :money @@ -52,6 +69,10 @@ defmodule TheronsErp.Sales.SalesLine do calculate :active_price, :money, expr(total_price || calculated_total_price) calculate :total_cost, :money, expr(unit_price * quantity) + + calculate :product_cost, :money, expr(product.cost) + calculate :product_price, :money, expr(product.price) + # calculate :margin do # end end From cd5769563f3218c8bfef8a21d0c5f0f5c19839d5 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 12:25:59 -0500 Subject: [PATCH 71/90] Product listing --- lib/therons_erp/inventory/product.ex | 3 +++ lib/therons_erp_web/live/product_live/index.ex | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/therons_erp/inventory/product.ex b/lib/therons_erp/inventory/product.ex index f0ef497..4d9d1d0 100644 --- a/lib/therons_erp/inventory/product.ex +++ b/lib/therons_erp/inventory/product.ex @@ -13,10 +13,13 @@ defmodule TheronsErp.Inventory.Product do defaults [:read] read :list do + primary? true + prepare build(sort: [identifier: :desc]) end read :list_saleable do filter expr(saleable == true) + prepare build(sort: [identifier: :desc]) end create :create do diff --git a/lib/therons_erp_web/live/product_live/index.ex b/lib/therons_erp_web/live/product_live/index.ex index 1ba0472..e406ef8 100644 --- a/lib/therons_erp_web/live/product_live/index.ex +++ b/lib/therons_erp_web/live/product_live/index.ex @@ -18,6 +18,7 @@ defmodule TheronsErpWeb.ProductLive.Index do rows={@streams.products} row_click={fn {_id, product} -> JS.navigate(~p"/products/#{product}") end} > + <:col :let={{_id, product}} label="Identifier">{product.identifier} <:col :let={{_id, product}} label="Name">{product.name} <:col :let={{_id, product}} label="Category"> {(product.category && product.category.full_name) || ""} @@ -55,8 +56,7 @@ defmodule TheronsErpWeb.ProductLive.Index do socket |> stream( :products, - Ash.read!(TheronsErp.Inventory.Product, actor: socket.assigns[:current_user]) - |> Ash.load!(:category) + TheronsErp.Inventory.get_products!(actor: socket.assigns[:current_user], load: [:category]) ) |> assign_new(:current_user, fn -> nil end)} end From c9068ff366b718637f15dd4fbf77f9f9dc20a92c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 12:37:54 -0500 Subject: [PATCH 72/90] Progress --- .../live/sales_order_live/show.ex | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index c106abd..7b78a78 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -232,17 +232,39 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do field={sales_line[:sales_price]} value={do_money(sales_line[:sales_price])} type="number" + inline_container={true} /> + <%= if to_string(Phoenix.HTML.Form.input_value(sales_line, :sales_price)) != (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: to_string(p.sales_price), else: "")do %> + <.button + phx-disable-with="Saving..." + class="revert-button" + name="revert" + value={"revert-price-#{sales_line.index}"} + > + <.icon name="hero-arrow-uturn-left" /> + + <% end %>
$ <.input - field={sales_line[:unit_price]} - value={do_money(sales_line[:unit_price])} + field={sales_line[:unit_cost]} + value={do_money(sales_line[:unit_cost])} type="number" + inline_container={true} /> + <%= if to_string(Phoenix.HTML.Form.input_value(sales_line, :unit_cost)) != (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: to_string(p.cost), else: "")do %> + <.button + phx-disable-with="Saving..." + class="revert-button" + name="revert" + value={"revert-cost-#{sales_line.index}"} + > + <.icon name="hero-arrow-uturn-left" /> + + <% end %> @@ -496,6 +518,42 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do socket end + def handle_event( + "save", + %{"revert" => "revert-cost-" <> index, "sales_order" => params}, + socket + ) do + new_params = put_in(params, ["sales_lines", index, "unit_cost"], nil) + + form = + AshPhoenix.Form.validate(socket.assigns.form, new_params) + + {:noreply, + socket + |> assign(:form, to_form(form)) + |> assign(:params, new_params) + |> assign(:unsaved_changes, form.source.changed?) + |> assign(:total_price_changes, Map.put(socket.assigns.total_price_changes, index, false))} + end + + def handle_event( + "save", + %{"revert" => "revert-price-" <> index, "sales_order" => params}, + socket + ) do + new_params = put_in(params, ["sales_lines", index, "sales_price"], nil) + + form = + AshPhoenix.Form.validate(socket.assigns.form, new_params) + + {:noreply, + socket + |> assign(:form, to_form(form)) + |> assign(:params, new_params) + |> assign(:unsaved_changes, form.source.changed?) + |> assign(:total_price_changes, Map.put(socket.assigns.total_price_changes, index, false))} + end + def handle_event( "save", %{"revert" => "revert-total-price-" <> index, "sales_order" => params}, From a49d9a48159f79618a14a48c80b671b48880f5d4 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 21:55:13 -0500 Subject: [PATCH 73/90] Fix resetting monies --- .../live/sales_order_live/show.ex | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 7b78a78..1e342b6 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -234,7 +234,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do type="number" inline_container={true} /> - <%= if to_string(Phoenix.HTML.Form.input_value(sales_line, :sales_price)) != (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: to_string(p.sales_price), else: "")do %> + <%= if compare_monies_neq(Phoenix.HTML.Form.input_value(sales_line, :sales_price), (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: p.sales_price, else: "")) do %> <.button phx-disable-with="Saving..." class="revert-button" @@ -250,12 +250,12 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do $ <.input - field={sales_line[:unit_cost]} - value={do_money(sales_line[:unit_cost])} + field={sales_line[:unit_price]} + value={do_money(sales_line[:unit_price])} type="number" inline_container={true} /> - <%= if to_string(Phoenix.HTML.Form.input_value(sales_line, :unit_cost)) != (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: to_string(p.cost), else: "")do %> + <%= if compare_monies_neq(Phoenix.HTML.Form.input_value(sales_line, :unit_price), (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: p.cost, else: "")) do %> <.button phx-disable-with="Saving..." class="revert-button" @@ -523,7 +523,12 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do %{"revert" => "revert-cost-" <> index, "sales_order" => params}, socket ) do - new_params = put_in(params, ["sales_lines", index, "unit_cost"], nil) + new_cost = + socket.assigns.form.source.data.sales_lines + |> Enum.at(String.to_integer(index)) + |> (&if(&1.product.cost, do: &1.product.cost.amount |> Decimal.to_string(), else: "")).() + + new_params = put_in(params, ["sales_lines", index, "unit_price"], new_cost) form = AshPhoenix.Form.validate(socket.assigns.form, new_params) @@ -541,7 +546,15 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do %{"revert" => "revert-price-" <> index, "sales_order" => params}, socket ) do - new_params = put_in(params, ["sales_lines", index, "sales_price"], nil) + new_price = + socket.assigns.form.source.data.sales_lines + |> Enum.at(String.to_integer(index)) + |> (&if(&1.product.sales_price, + do: &1.product.sales_price.amount |> Decimal.to_string(), + else: "" + )).() + + new_params = put_in(params, ["sales_lines", index, "sales_price"], new_price) form = AshPhoenix.Form.validate(socket.assigns.form, new_params) @@ -1067,4 +1080,14 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do id = Phoenix.HTML.Form.input_value(form, :address_id) Enum.find(addresses, &(&1.id == id)) end + + defp compare_monies_neq(a, b) do + case {a, b} do + {nil, nil} -> + false + + _ -> + not Money.equal?(a, b) + end + end end From f28300a7e08bc422cb5a214ac28e301e99cba149 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 22:04:11 -0500 Subject: [PATCH 74/90] Highlight empty fields --- assets/css/app.css | 5 + .../live/sales_order_live/show.ex | 121 ++++++++++-------- 2 files changed, 72 insertions(+), 54 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index a35d336..e67948e 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -179,3 +179,8 @@ display: inline-block; } } + +.empty-field { + @apply border-red-500 border-solid border-1; + box-sizing: border-box; +} diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 1e342b6..a6203ad 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -228,70 +228,83 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do $ - <.input - field={sales_line[:sales_price]} - value={do_money(sales_line[:sales_price])} - type="number" - inline_container={true} - /> - <%= if compare_monies_neq(Phoenix.HTML.Form.input_value(sales_line, :sales_price), (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: p.sales_price, else: "")) do %> - <.button - phx-disable-with="Saving..." - class="revert-button" - name="revert" - value={"revert-price-#{sales_line.index}"} - > - <.icon name="hero-arrow-uturn-left" /> - - <% end %> +
+ <.input + field={sales_line[:sales_price]} + value={do_money(sales_line[:sales_price])} + type="number" + inline_container={true} + /> + <%= if compare_monies_neq(Phoenix.HTML.Form.input_value(sales_line, :sales_price), (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: p.sales_price, else: "")) do %> + <.button + phx-disable-with="Saving..." + class="revert-button" + name="revert" + value={"revert-price-#{sales_line.index}"} + > + <.icon name="hero-arrow-uturn-left" /> + + <% end %> +
$ - <.input - field={sales_line[:unit_price]} - value={do_money(sales_line[:unit_price])} - type="number" - inline_container={true} - /> - <%= if compare_monies_neq(Phoenix.HTML.Form.input_value(sales_line, :unit_price), (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: p.cost, else: "")) do %> - <.button - phx-disable-with="Saving..." - class="revert-button" - name="revert" - value={"revert-cost-#{sales_line.index}"} - > - <.icon name="hero-arrow-uturn-left" /> - - <% end %> +
+ <.input + field={sales_line[:unit_price]} + value={do_money(sales_line[:unit_price])} + type="number" + inline_container={true} + /> + <%= if compare_monies_neq(Phoenix.HTML.Form.input_value(sales_line, :unit_price), (if p = Phoenix.HTML.Form.input_value(sales_line, :product), do: p.cost, else: "")) do %> + <.button + phx-disable-with="Saving..." + class="revert-button" + name="revert" + value={"revert-cost-#{sales_line.index}"} + > + <.icon name="hero-arrow-uturn-left" /> + + <% end %> +
$ - <.input - field={sales_line[:total_price]} - value={ - active_price_for_sales_line( - sales_line, - sales_line.index, - @total_price_changes - ) - } - type="number" - inline_container={true} - /> - <%= if (@total_price_changes[to_string(sales_line.index)] == true) || is_active_price_persisted?(sales_line, sales_line.index, @total_price_changes) do %> - <.button - phx-disable-with="Saving..." - class="revert-button" - name="revert" - value={"revert-total-price-#{sales_line.index}"} - > - <.icon name="hero-arrow-uturn-left" /> - - <% end %> +
+ <.input + field={sales_line[:total_price]} + value={ + active_price_for_sales_line( + sales_line, + sales_line.index, + @total_price_changes + ) + } + type="number" + inline_container={true} + /> + <%= if (@total_price_changes[to_string(sales_line.index)] == true) || is_active_price_persisted?(sales_line, sales_line.index, @total_price_changes) do %> + <.button + phx-disable-with="Saving..." + class="revert-button" + name="revert" + value={"revert-total-price-#{sales_line.index}"} + > + <.icon name="hero-arrow-uturn-left" /> + + <% end %> +
From f1f4a349257f518c079defb079e6ab3edc270112 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sat, 15 Feb 2025 22:10:23 -0500 Subject: [PATCH 75/90] Remove rendered math --- .../live/sales_order_live/show.ex | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index a6203ad..5eb26ef 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -38,29 +38,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <% end %> - <:subtitle> - Margin: - - - {@sales_order.total_cost} - {@sales_order.total_price} - - = - - {if @sales_order.total_cost != nil and - not Money.equal?(@sales_order.total_cost, Money.new(0, :USD)), - do: - (Decimal.mult( - Money.div!(@sales_order.total_price, @sales_order.total_cost.amount).amount, - 100 - ) - |> Decimal.sub(100) - |> Decimal.to_string()) <> - "%", - else: "undefined"} - - - + <:subtitle>
From d214c01a2b7d464741a3321e20efe9325da7a146 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 11:27:10 -0500 Subject: [PATCH 76/90] Fix border stylig --- assets/css/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/app.css b/assets/css/app.css index e67948e..b02f733 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -181,6 +181,6 @@ } .empty-field { - @apply border-red-500 border-solid border-1; + @apply border-red-500 border-solid border-2; box-sizing: border-box; } From 2307a3b2086db5518eb01b939bb6d7808bbd253e Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 11:27:26 -0500 Subject: [PATCH 77/90] Remove warning --- lib/therons_erp/inventory/product.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/therons_erp/inventory/product.ex b/lib/therons_erp/inventory/product.ex index 4d9d1d0..783329d 100644 --- a/lib/therons_erp/inventory/product.ex +++ b/lib/therons_erp/inventory/product.ex @@ -2,7 +2,8 @@ defmodule TheronsErp.Inventory.Product do use Ash.Resource, otp_app: :therons_erp, data_layer: AshPostgres.DataLayer, - domain: TheronsErp.Inventory + domain: TheronsErp.Inventory, + primary_read_warning?: false postgres do table "products" From 83d92ed6dd4a5a40fb61c9e31ca9885a4fcad198 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 11:27:31 -0500 Subject: [PATCH 78/90] Create invoices --- config/config.exs | 1 + lib/therons_erp/invoices.ex | 8 ++++++ lib/therons_erp/invoices/invoice.ex | 43 +++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 lib/therons_erp/invoices.ex create mode 100644 lib/therons_erp/invoices/invoice.ex diff --git a/config/config.exs b/config/config.exs index e7505d0..b67249b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -58,6 +58,7 @@ config :therons_erp, ecto_repos: [TheronsErp.Repo], generators: [timestamp_type: :utc_datetime], ash_domains: [ + TheronsErp.Invoices, TheronsErp.People, TheronsErp.Sales, TheronsErp.Inventory, diff --git a/lib/therons_erp/invoices.ex b/lib/therons_erp/invoices.ex new file mode 100644 index 0000000..d1e943d --- /dev/null +++ b/lib/therons_erp/invoices.ex @@ -0,0 +1,8 @@ +defmodule TheronsErp.Invoices do + use Ash.Domain, + otp_app: :therons_erp + + resources do + resource TheronsErp.Invoices.Invoice + end +end diff --git a/lib/therons_erp/invoices/invoice.ex b/lib/therons_erp/invoices/invoice.ex new file mode 100644 index 0000000..723aa93 --- /dev/null +++ b/lib/therons_erp/invoices/invoice.ex @@ -0,0 +1,43 @@ +defmodule TheronsErp.Invoices.Invoice do + use Ash.Resource, + otp_app: :therons_erp, + domain: TheronsErp.Invoices, + data_layer: AshPostgres.DataLayer, + extensions: [AshStateMachine] + + postgres do + table "invoices" + repo TheronsErp.Repo + end + + state_machine do + initial_states([:draft]) + default_initial_state(:draft) + + transitions do + transition(:send, from: [:draft], to: [:sent]) + end + end + + actions do + defaults [:read, create: [], update: []] + + update :send do + change transition_state(:sent) + end + end + + attributes do + uuid_primary_key :id + + attribute :identifier, :integer do + generated? true + end + + timestamps() + end + + relationships do + belongs_to :customer, TheronsErp.People.Entity + end +end From 4b927a133724a3d19a4f82a5ff2484ba0ef8641f Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 11:45:01 -0500 Subject: [PATCH 79/90] line items --- lib/therons_erp/invoices.ex | 1 + lib/therons_erp/invoices/invoice.ex | 1 + lib/therons_erp/invoices/line_item.ex | 25 +++ ...50216164429_add_line_items_to_invoices.exs | 93 +++++++++++ .../repo/invoices/20250216164429.json | 98 ++++++++++++ .../repo/line_items/20250216164429.json | 98 ++++++++++++ .../repo/sales_lines/20250216164429.json | 147 ++++++++++++++++++ test/therons_erp/invoice_test.exs | 16 ++ 8 files changed, 479 insertions(+) create mode 100644 lib/therons_erp/invoices/line_item.ex create mode 100644 priv/repo/migrations/20250216164429_add_line_items_to_invoices.exs create mode 100644 priv/resource_snapshots/repo/invoices/20250216164429.json create mode 100644 priv/resource_snapshots/repo/line_items/20250216164429.json create mode 100644 priv/resource_snapshots/repo/sales_lines/20250216164429.json create mode 100644 test/therons_erp/invoice_test.exs diff --git a/lib/therons_erp/invoices.ex b/lib/therons_erp/invoices.ex index d1e943d..7e6779b 100644 --- a/lib/therons_erp/invoices.ex +++ b/lib/therons_erp/invoices.ex @@ -4,5 +4,6 @@ defmodule TheronsErp.Invoices do resources do resource TheronsErp.Invoices.Invoice + resource TheronsErp.Invoices.LineItem end end diff --git a/lib/therons_erp/invoices/invoice.ex b/lib/therons_erp/invoices/invoice.ex index 723aa93..8670252 100644 --- a/lib/therons_erp/invoices/invoice.ex +++ b/lib/therons_erp/invoices/invoice.ex @@ -39,5 +39,6 @@ defmodule TheronsErp.Invoices.Invoice do relationships do belongs_to :customer, TheronsErp.People.Entity + has_many :line_items, TheronsErp.Invoices.LineItem end end diff --git a/lib/therons_erp/invoices/line_item.ex b/lib/therons_erp/invoices/line_item.ex new file mode 100644 index 0000000..7d06dc1 --- /dev/null +++ b/lib/therons_erp/invoices/line_item.ex @@ -0,0 +1,25 @@ +defmodule TheronsErp.Invoices.LineItem do + use Ash.Resource, + otp_app: :therons_erp, + domain: TheronsErp.Invoices, + data_layer: AshPostgres.DataLayer + + postgres do + table "line_items" + repo TheronsErp.Repo + end + + attributes do + uuid_primary_key :id + + attribute :price, :money + attribute :quantity, :integer + timestamps() + end + + relationships do + belongs_to :invoice, TheronsErp.Invoices.Invoice do + allow_nil? false + end + end +end diff --git a/priv/repo/migrations/20250216164429_add_line_items_to_invoices.exs b/priv/repo/migrations/20250216164429_add_line_items_to_invoices.exs new file mode 100644 index 0000000..a36553f --- /dev/null +++ b/priv/repo/migrations/20250216164429_add_line_items_to_invoices.exs @@ -0,0 +1,93 @@ +defmodule TheronsErp.Repo.Migrations.AddLineItemsToInvoices do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:sales_lines) do + modify :quantity, :bigint, default: 1 + end + + create table(:line_items, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :price, :money_with_currency + add :quantity, :bigint + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :invoice_id, :uuid + end + + create table(:invoices, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + end + + alter table(:line_items) do + modify :invoice_id, + references(:invoices, + column: :id, + name: "line_items_invoice_id_fkey", + type: :uuid, + prefix: "public" + ) + end + + alter table(:invoices) do + add :identifier, :bigserial + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :customer_id, + references(:entities, + column: :id, + name: "invoices_customer_id_fkey", + type: :uuid, + prefix: "public" + ) + + add :state, :text, null: false, default: "draft" + end + end + + def down do + drop constraint(:invoices, "invoices_customer_id_fkey") + + alter table(:invoices) do + remove :state + remove :customer_id + remove :updated_at + remove :inserted_at + remove :identifier + end + + drop constraint(:line_items, "line_items_invoice_id_fkey") + + alter table(:line_items) do + modify :invoice_id, :uuid + end + + drop table(:invoices) + + drop table(:line_items) + + alter table(:sales_lines) do + modify :quantity, :bigint, default: nil + end + end +end diff --git a/priv/resource_snapshots/repo/invoices/20250216164429.json b/priv/resource_snapshots/repo/invoices/20250216164429.json new file mode 100644 index 0000000..7ada1d4 --- /dev/null +++ b/priv/resource_snapshots/repo/invoices/20250216164429.json @@ -0,0 +1,98 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": true, + "primary_key?": false, + "references": null, + "size": null, + "source": "identifier", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "invoices_customer_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "entities" + }, + "size": null, + "source": "customer_id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "\"draft\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "8557B4FF1D37A5787B61167BE00116638CF15DCDCD95EA338456F8CFBB78EF85", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "invoices" +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/line_items/20250216164429.json b/priv/resource_snapshots/repo/line_items/20250216164429.json new file mode 100644 index 0000000..0274d9d --- /dev/null +++ b/priv/resource_snapshots/repo/line_items/20250216164429.json @@ -0,0 +1,98 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "price", + "type": "money_with_currency" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "quantity", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "line_items_invoice_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "invoices" + }, + "size": null, + "source": "invoice_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": false, + "hash": "7B923DA42D5B6D7981FE6DA1DFD68408F766F419D20FAB879AF8EFA94E9E487B", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "line_items" +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/sales_lines/20250216164429.json b/priv/resource_snapshots/repo/sales_lines/20250216164429.json new file mode 100644 index 0000000..460086c --- /dev/null +++ b/priv/resource_snapshots/repo/sales_lines/20250216164429.json @@ -0,0 +1,147 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "sales_price", + "type": "money_with_currency" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "unit_price", + "type": "money_with_currency" + }, + { + "allow_nil?": true, + "default": "1", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "quantity", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "total_price", + "type": "money_with_currency" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "sales_lines_sales_order_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "sales_orders" + }, + "size": null, + "source": "sales_order_id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "sales_lines_product_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "products" + }, + "size": null, + "source": "product_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "730EA0DCCA6A1B7AE56232F80AF7C12D32F50CD2DD88F719AF9B491529CF0728", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "sales_lines" +} \ No newline at end of file diff --git a/test/therons_erp/invoice_test.exs b/test/therons_erp/invoice_test.exs new file mode 100644 index 0000000..37df570 --- /dev/null +++ b/test/therons_erp/invoice_test.exs @@ -0,0 +1,16 @@ +defmodule TheronsErp.InvoiceTest do + use TheronsErp.DataCase + alias TheronsErp.Sales + alias TheronsErp.Inventory + alias TheronsErp.Sales.{SalesOrder, SalesLine} + + test "copying line items" do + sales_order = Sales.create_draft() + product = Inventory.create_product(%{name: "Test Product", sales_price: 100, cost: 50}) + sales_line = Ash.create!(SalesLine, %{sales_order_id: sales_order.id, product_id: product.id, quantity: 1}) + + invoice = Ash.create!(Invoice, %{sales_order_id: sales_order.id, sales_lines: [sales_line]}) + + # assert invoice.line_items == [] + end +end From 3c7a04a41319caca64790fb3c93b691a5a5bc292 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 11:46:03 -0500 Subject: [PATCH 80/90] add product to line item --- lib/therons_erp/invoices/line_item.ex | 4 ++++ test/therons_erp/invoice_test.exs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/therons_erp/invoices/line_item.ex b/lib/therons_erp/invoices/line_item.ex index 7d06dc1..9e2ef18 100644 --- a/lib/therons_erp/invoices/line_item.ex +++ b/lib/therons_erp/invoices/line_item.ex @@ -21,5 +21,9 @@ defmodule TheronsErp.Invoices.LineItem do belongs_to :invoice, TheronsErp.Invoices.Invoice do allow_nil? false end + + belongs_to :product, TheronsErp.Inventory.Product do + allow_nil? false + end end end diff --git a/test/therons_erp/invoice_test.exs b/test/therons_erp/invoice_test.exs index 37df570..a41aa37 100644 --- a/test/therons_erp/invoice_test.exs +++ b/test/therons_erp/invoice_test.exs @@ -11,6 +11,6 @@ defmodule TheronsErp.InvoiceTest do invoice = Ash.create!(Invoice, %{sales_order_id: sales_order.id, sales_lines: [sales_line]}) - # assert invoice.line_items == [] + [line_item] = invoice.line_items end end From d5ca6b1e79d7b7061b7d2d59e765eec8a7247543 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 11:47:06 -0500 Subject: [PATCH 81/90] Write the test --- ...0250216164600_add_product_to_line_item.exs | 33 +++++ .../repo/line_items/20250216164600.json | 127 ++++++++++++++++++ test/therons_erp/invoice_test.exs | 3 + 3 files changed, 163 insertions(+) create mode 100644 priv/repo/migrations/20250216164600_add_product_to_line_item.exs create mode 100644 priv/resource_snapshots/repo/line_items/20250216164600.json diff --git a/priv/repo/migrations/20250216164600_add_product_to_line_item.exs b/priv/repo/migrations/20250216164600_add_product_to_line_item.exs new file mode 100644 index 0000000..92d7dee --- /dev/null +++ b/priv/repo/migrations/20250216164600_add_product_to_line_item.exs @@ -0,0 +1,33 @@ +defmodule TheronsErp.Repo.Migrations.AddProductToLineItem do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:line_items) do + modify :invoice_id, :uuid, null: false + + add :product_id, + references(:products, + column: :id, + name: "line_items_product_id_fkey", + type: :uuid, + prefix: "public" + ), + null: false + end + end + + def down do + drop constraint(:line_items, "line_items_product_id_fkey") + + alter table(:line_items) do + remove :product_id + modify :invoice_id, :uuid, null: true + end + end +end diff --git a/priv/resource_snapshots/repo/line_items/20250216164600.json b/priv/resource_snapshots/repo/line_items/20250216164600.json new file mode 100644 index 0000000..86d14bb --- /dev/null +++ b/priv/resource_snapshots/repo/line_items/20250216164600.json @@ -0,0 +1,127 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "price", + "type": "money_with_currency" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "quantity", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "line_items_invoice_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "invoices" + }, + "size": null, + "source": "invoice_id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "line_items_product_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "products" + }, + "size": null, + "source": "product_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": false, + "hash": "1A994869EE5887D16346A6EE9E895BA9ECA6CF90FF268AB54EBCD7DE054D01C5", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "line_items" +} \ No newline at end of file diff --git a/test/therons_erp/invoice_test.exs b/test/therons_erp/invoice_test.exs index a41aa37..ecf1c46 100644 --- a/test/therons_erp/invoice_test.exs +++ b/test/therons_erp/invoice_test.exs @@ -12,5 +12,8 @@ defmodule TheronsErp.InvoiceTest do invoice = Ash.create!(Invoice, %{sales_order_id: sales_order.id, sales_lines: [sales_line]}) [line_item] = invoice.line_items + assert line_item.product_id == product.id + assert Money.equal?(line_item.price, sales_line.sales_price) + assert Money.equal?(line_item.quantity, sales_line.quantity) end end From eb5d0d166af96ee1300c484f4ed4fea4be14256c Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 11:58:06 -0500 Subject: [PATCH 82/90] Work on invoice line items --- lib/therons_erp/invoices/invoice.ex | 16 ++- lib/therons_erp/invoices/line_item.ex | 4 + lib/therons_erp/sales/sales_line.ex | 4 +- ...50216165331_add_sales_order_to_invoice.exs | 29 ++++ .../repo/invoices/20250216165331.json | 127 ++++++++++++++++++ test/therons_erp/invoice_test.exs | 16 ++- 6 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 priv/repo/migrations/20250216165331_add_sales_order_to_invoice.exs create mode 100644 priv/resource_snapshots/repo/invoices/20250216165331.json diff --git a/lib/therons_erp/invoices/invoice.ex b/lib/therons_erp/invoices/invoice.ex index 8670252..04be272 100644 --- a/lib/therons_erp/invoices/invoice.ex +++ b/lib/therons_erp/invoices/invoice.ex @@ -20,11 +20,24 @@ defmodule TheronsErp.Invoices.Invoice do end actions do - defaults [:read, create: [], update: []] + defaults [ + :read, + update: [:customer_id, :sales_order_id] + ] update :send do change transition_state(:sent) end + + create :create do + accept [:customer_id, :sales_order_id] + argument :sales_lines, {:array, :map} + + change fn changeset, context -> + IO.inspect(changeset) + changeset + end + end end attributes do @@ -39,6 +52,7 @@ defmodule TheronsErp.Invoices.Invoice do relationships do belongs_to :customer, TheronsErp.People.Entity + belongs_to :sales_order, TheronsErp.Sales.SalesOrder has_many :line_items, TheronsErp.Invoices.LineItem end end diff --git a/lib/therons_erp/invoices/line_item.ex b/lib/therons_erp/invoices/line_item.ex index 9e2ef18..ac59d98 100644 --- a/lib/therons_erp/invoices/line_item.ex +++ b/lib/therons_erp/invoices/line_item.ex @@ -9,6 +9,10 @@ defmodule TheronsErp.Invoices.LineItem do repo TheronsErp.Repo end + actions do + defaults [:read] + end + attributes do uuid_primary_key :id diff --git a/lib/therons_erp/sales/sales_line.ex b/lib/therons_erp/sales/sales_line.ex index 4213a3c..ab1a35d 100644 --- a/lib/therons_erp/sales/sales_line.ex +++ b/lib/therons_erp/sales/sales_line.ex @@ -14,7 +14,7 @@ defmodule TheronsErp.Sales.SalesLine do create :create do primary? true - accept [:sales_price, :unit_price, :quantity, :product_id] + accept [:sales_price, :unit_price, :quantity, :product_id, :sales_order_id] # Set unit_price and sales_price based on product price and cost change fn changeset, _ -> @@ -34,7 +34,7 @@ defmodule TheronsErp.Sales.SalesLine do update :update do require_atomic? false primary? true - accept [:sales_price, :unit_price, :quantity, :product_id, :total_price] + accept [:sales_price, :unit_price, :quantity, :product_id, :total_price, :sales_order_id] end end diff --git a/priv/repo/migrations/20250216165331_add_sales_order_to_invoice.exs b/priv/repo/migrations/20250216165331_add_sales_order_to_invoice.exs new file mode 100644 index 0000000..1bf3451 --- /dev/null +++ b/priv/repo/migrations/20250216165331_add_sales_order_to_invoice.exs @@ -0,0 +1,29 @@ +defmodule TheronsErp.Repo.Migrations.AddSalesOrderToInvoice do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:invoices) do + add :sales_order_id, + references(:sales_orders, + column: :id, + name: "invoices_sales_order_id_fkey", + type: :uuid, + prefix: "public" + ) + end + end + + def down do + drop constraint(:invoices, "invoices_sales_order_id_fkey") + + alter table(:invoices) do + remove :sales_order_id + end + end +end diff --git a/priv/resource_snapshots/repo/invoices/20250216165331.json b/priv/resource_snapshots/repo/invoices/20250216165331.json new file mode 100644 index 0000000..d0ae4cd --- /dev/null +++ b/priv/resource_snapshots/repo/invoices/20250216165331.json @@ -0,0 +1,127 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": true, + "primary_key?": false, + "references": null, + "size": null, + "source": "identifier", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "invoices_customer_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "entities" + }, + "size": null, + "source": "customer_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "invoices_sales_order_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "sales_orders" + }, + "size": null, + "source": "sales_order_id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "\"draft\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "E7336F732F424A985DFE0B6FB7F865BFA563256664C8FCB5016BB3EBB0BECB5B", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "invoices" +} \ No newline at end of file diff --git a/test/therons_erp/invoice_test.exs b/test/therons_erp/invoice_test.exs index ecf1c46..0b0fe69 100644 --- a/test/therons_erp/invoice_test.exs +++ b/test/therons_erp/invoice_test.exs @@ -3,13 +3,21 @@ defmodule TheronsErp.InvoiceTest do alias TheronsErp.Sales alias TheronsErp.Inventory alias TheronsErp.Sales.{SalesOrder, SalesLine} + alias TheronsErp.Invoices.Invoice test "copying line items" do - sales_order = Sales.create_draft() - product = Inventory.create_product(%{name: "Test Product", sales_price: 100, cost: 50}) - sales_line = Ash.create!(SalesLine, %{sales_order_id: sales_order.id, product_id: product.id, quantity: 1}) + sales_order = Sales.create_draft!() + product = Inventory.create_product!("Test Product", Money.new(100, :USD), %{cost: Money.new(50, :USD)}) + sales_line = + SalesLine + |> Ash.Changeset.for_create(:create, %{sales_order_id: sales_order.id, product_id: product.id, quantity: 1}) + |> Ash.create!() - invoice = Ash.create!(Invoice, %{sales_order_id: sales_order.id, sales_lines: [sales_line]}) + invoice = + Invoice + |> Ash.Changeset.for_create(:create, %{sales_order_id: sales_order.id, sales_lines: [sales_line]}) + |> Ash.create!() + |> Ash.load!(:line_items) [line_item] = invoice.line_items assert line_item.product_id == product.id From 26669568e51c51f7852b4e09b33530ab8cb96e38 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 12:05:37 -0500 Subject: [PATCH 83/90] Line items for invoices --- lib/therons_erp/invoices/invoice.ex | 18 ++++++++++++++++-- lib/therons_erp/invoices/line_item.ex | 8 ++++++++ test/therons_erp/invoice_test.exs | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/therons_erp/invoices/invoice.ex b/lib/therons_erp/invoices/invoice.ex index 04be272..c8c1e20 100644 --- a/lib/therons_erp/invoices/invoice.ex +++ b/lib/therons_erp/invoices/invoice.ex @@ -5,6 +5,8 @@ defmodule TheronsErp.Invoices.Invoice do data_layer: AshPostgres.DataLayer, extensions: [AshStateMachine] + alias TheronsErp.Invoices.LineItem + postgres do table "invoices" repo TheronsErp.Repo @@ -34,8 +36,20 @@ defmodule TheronsErp.Invoices.Invoice do argument :sales_lines, {:array, :map} change fn changeset, context -> - IO.inspect(changeset) - changeset + Ash.Changeset.after_action(changeset, fn changeset, result -> + sales_lines = Ash.Changeset.get_argument(changeset, :sales_lines) + + for line <- sales_lines do + LineItem.create(%{ + price: line.sales_price, + quantity: line.quantity, + invoice_id: result.id, + product_id: line.product_id + }) + end + + {:ok, result} + end) end end end diff --git a/lib/therons_erp/invoices/line_item.ex b/lib/therons_erp/invoices/line_item.ex index ac59d98..027fa72 100644 --- a/lib/therons_erp/invoices/line_item.ex +++ b/lib/therons_erp/invoices/line_item.ex @@ -9,8 +9,16 @@ defmodule TheronsErp.Invoices.LineItem do repo TheronsErp.Repo end + code_interface do + define :create, args: [] + end + actions do defaults [:read] + + create :create do + accept [:price, :quantity, :invoice_id, :product_id] + end end attributes do diff --git a/test/therons_erp/invoice_test.exs b/test/therons_erp/invoice_test.exs index 0b0fe69..38be833 100644 --- a/test/therons_erp/invoice_test.exs +++ b/test/therons_erp/invoice_test.exs @@ -22,6 +22,6 @@ defmodule TheronsErp.InvoiceTest do [line_item] = invoice.line_items assert line_item.product_id == product.id assert Money.equal?(line_item.price, sales_line.sales_price) - assert Money.equal?(line_item.quantity, sales_line.quantity) + assert line_item.quantity == sales_line.quantity end end From c3fd921638831d06f157680aff38b44def823f3e Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 12:25:47 -0500 Subject: [PATCH 84/90] Work on invoices --- lib/therons_erp/sales/sales_order.ex | 38 ++++++++++++++++++- lib/therons_erp_web/breadcrumbs.ex | 4 ++ .../live/sales_order_live/show.ex | 21 ++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/therons_erp/sales/sales_order.ex b/lib/therons_erp/sales/sales_order.ex index b395a7f..aa3e81d 100644 --- a/lib/therons_erp/sales/sales_order.ex +++ b/lib/therons_erp/sales/sales_order.ex @@ -3,7 +3,10 @@ defmodule TheronsErp.Sales.SalesOrder do otp_app: :therons_erp, domain: TheronsErp.Sales, data_layer: AshPostgres.DataLayer, - extensions: [AshStateMachine] + extensions: [AshStateMachine], + primary_read_warning?: false + + alias TheronsErp.Invoices.Invoice postgres do table "sales_orders" @@ -16,13 +19,40 @@ defmodule TheronsErp.Sales.SalesOrder do transitions do transition(:ready, from: :draft, to: [:ready, :cancelled]) + transition(:invoice, from: :ready, to: [:invoiced]) transition(:cancel, from: [:draft, :ready], to: :cancelled) + transition(:cancel_invoice, from: [:invoiced], to: :cancelled) transition(:revive, from: [:cancelled, :ready], to: [:draft, :ready]) - transition(:complete, from: [:draft, :ready], to: :complete) + transition(:complete, from: [:draft, :ready, :invoiced], to: :complete) end end actions do + update :invoice do + require_atomic? false + change transition_state(:invoiced) + + change fn changeset, result -> + Ash.Changeset.after_action(changeset, fn changeset, result -> + IO.inspect(result) + + Invoice + |> Ash.Changeset.for_create(:create, %{ + sales_order_id: result.id, + sales_lines: result.sales_lines + }) + |> Ash.create!() + + {:ok, result} + end) + end + end + + update :cancel_invoice do + # TODO cancel the invoice here + change transition_state(:cancelled) + end + update :ready do change transition_state(:ready) end @@ -88,6 +118,10 @@ defmodule TheronsErp.Sales.SalesOrder do belongs_to :customer, TheronsErp.People.Entity belongs_to :address, TheronsErp.People.Address + + has_one :invoice, TheronsErp.Invoices.Invoice do + destination_attribute :sales_order_id + end end aggregates do diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index 135ab6a..5d496e8 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -181,6 +181,10 @@ defmodule TheronsErpWeb.Breadcrumbs do ~p"/people/#{customer_id}/new_address?#{[breadcrumbs: append_and_encode(breadcrumbs, from)]}" end + def navigate_to_url(breadcrumbs, {"invoices", id, _identifier}, from) do + ~p"/invoices/#{id}?#{[breadcrumbs: append_and_encode(breadcrumbs, from)]}" + end + defp append_and_encode(breadcrumbs, breadcrumb) do [breadcrumb | breadcrumbs] |> encode_breadcrumbs() diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 5eb26ef..6b9277a 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -36,11 +36,26 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do <.button phx-disable-with="Saving..." phx-click="set-draft"> Return to draft + <.button phx-disable-with="Saving..." phx-click="set-invoice"> + Generate invoice + <% end %> <:subtitle> + <%= if @sales_order.state == :invoiced do %> + <.link navigate={ + TheronsErpWeb.Breadcrumbs.navigate_to_url( + @breadcrumbs, + {"invoices", @sales_order.invoice.id, @sales_order.invoice.identifier}, + {"sales_orders", @sales_order.id, @params, @sales_order.identifier} + ) + }> + <.button>View invoice + + <% end %> +

Customer

@@ -439,6 +454,7 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do :total_price, :total_cost, :address, + :invoice, sales_lines: [:total_price, :product, :active_price, :calculated_total_price, :total_cost], customer: [:addresses] ] @@ -756,6 +772,11 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} end + def handle_event("set-invoice", _, socket) do + Ash.Changeset.for_update(socket.assigns.sales_order, :invoice) |> Ash.update!() + {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} + end + defp is_active_price_persisted?(sales_line, index, total_price_changes) do if total_price_changes[to_string(index)] == false do false From c2c2f4adf6341f6ce61a2d1f1dc552be630a65a5 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 22:41:05 -0500 Subject: [PATCH 85/90] Fix new product breadcrumbs --- lib/therons_erp_web/breadcrumbs.ex | 4 ++++ lib/therons_erp_web/live/product_live/show.ex | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index 5d496e8..be58d5f 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -11,6 +11,10 @@ defmodule TheronsErpWeb.Breadcrumbs do [] end + def decode_breadcrumbs("") do + [] + end + def decode_breadcrumbs(breadcrumbs) do # base 64 code then JSON decode breadcrumbs diff --git a/lib/therons_erp_web/live/product_live/show.ex b/lib/therons_erp_web/live/product_live/show.ex index 1d41cc6..b740b26 100644 --- a/lib/therons_erp_web/live/product_live/show.ex +++ b/lib/therons_erp_web/live/product_live/show.ex @@ -12,7 +12,7 @@ defmodule TheronsErpWeb.ProductLive.Show do <.input field={@form[:name]} label="" data-1p-ignore /> - <%= if @line_id do %> + <%= if @line_id not in ["", nil] do %> <.button phx-disable-with="Saving..." class="save-button"> <.icon name="hero-check-circle" /> Return to Sales Order From 0ec26d09e481384e1d580120b5fc75e2486632df Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 23:16:57 -0500 Subject: [PATCH 86/90] Add GitHub workflow --- .github/workflows/elixir.yml | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/elixir.yml diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml new file mode 100644 index 0000000..4ab2b3b --- /dev/null +++ b/.github/workflows/elixir.yml @@ -0,0 +1,46 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Elixir CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + contents: read + +jobs: + build: + name: Build and test + runs-on: ubuntu-22.04 + + steps: + - uses: ikalnytskyi/action-setup-postgres@v7 # https://github.com/marketplace/actions/setup-postgresql-for-linux-macos-windows + + - run: psql postgresql://postgres:postgres@localhost:5432/postgres -c "SELECT 1" + - run: psql service=postgres -c "SELECT 1" + - run: psql -c "SELECT 1" + env: + PGSERVICE: postgres + + - uses: actions/checkout@v4 + - name: Set up Elixir + uses: erlef/setup-beam@61e01a43a562a89bfc54c7f9a378ff67b03e4a21 # v1.16.0 https://github.com/erlef/setup-beam/releases/tag/v1.16.0 + with: + elixir-version: "1.18.2" # [Required] Define the Elixir version + otp-version: "27.2" # [Required] Define the Erlang/OTP version + - name: Restore dependencies cache + uses: actions/cache@v3 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + - name: Install dependencies + run: mix deps.get + - name: Run tests + run: mix test From f0f7422e8386b42f6a8a4ca3fc2a14cd336e699f Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 23:22:03 -0500 Subject: [PATCH 87/90] pin in the test --- test/breadcrumb_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/breadcrumb_test.exs b/test/breadcrumb_test.exs index 0a4cefe..f2eae23 100644 --- a/test/breadcrumb_test.exs +++ b/test/breadcrumb_test.exs @@ -9,6 +9,6 @@ defmodule TheronsErpWeb.BreadcrumbTest do |> TheronsErpWeb.Breadcrumbs.stream_crumbs() |> Enum.to_list() - assert out = [[1, 2, 3], [2, 3], [3]] + assert ^out = [[1, 2, 3], [2, 3], [3]] end end From a74b6bd68c096af0a3a6e582156bf7049fffc0c5 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 23:23:27 -0500 Subject: [PATCH 88/90] Warnings as errors --- .github/workflows/elixir.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 4ab2b3b..2d88974 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -43,4 +43,4 @@ jobs: - name: Install dependencies run: mix deps.get - name: Run tests - run: mix test + run: mix test --warnings-as-errors From c0b6ceff43a5a3a8bcf4e54c8ffe9522f7cb386d Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 23:27:26 -0500 Subject: [PATCH 89/90] Fix warnings --- lib/therons_erp_web/breadcrumbs.ex | 2 +- lib/therons_erp_web/live/entity_live/show.ex | 1 - .../live/product_category_live/index.ex | 2 +- .../live/product_live/index.ex | 2 +- .../live/sales_order_live/form_component.ex | 2 - .../live/sales_order_live/show.ex | 304 +++++++++--------- test/breadcrumb_test.exs | 2 - test/therons_erp/invoice_test.exs | 18 +- 8 files changed, 168 insertions(+), 165 deletions(-) diff --git a/lib/therons_erp_web/breadcrumbs.ex b/lib/therons_erp_web/breadcrumbs.ex index be58d5f..1080137 100644 --- a/lib/therons_erp_web/breadcrumbs.ex +++ b/lib/therons_erp_web/breadcrumbs.ex @@ -80,7 +80,7 @@ defmodule TheronsErpWeb.Breadcrumbs do defp _navigate_back(_breadcrumbs, _from, args \\ nil) - defp _navigate_back([], from, args) do + defp _navigate_back([], from, _args) do which = case from do {"product_category", "new", _product_category_id} -> diff --git a/lib/therons_erp_web/live/entity_live/show.ex b/lib/therons_erp_web/live/entity_live/show.ex index de9b4fa..39e592e 100644 --- a/lib/therons_erp_web/live/entity_live/show.ex +++ b/lib/therons_erp_web/live/entity_live/show.ex @@ -1,5 +1,4 @@ defmodule TheronsErpWeb.EntityLive.Show do - alias Bandit.DelegatingHandler use TheronsErpWeb, :live_view @impl true diff --git a/lib/therons_erp_web/live/product_category_live/index.ex b/lib/therons_erp_web/live/product_category_live/index.ex index 0d274e4..f4cd3b0 100644 --- a/lib/therons_erp_web/live/product_category_live/index.ex +++ b/lib/therons_erp_web/live/product_category_live/index.ex @@ -92,7 +92,7 @@ defmodule TheronsErpWeb.ProductCategoryLive.Index do ) end - defp apply_action(socket, :new, params) do + defp apply_action(socket, :new, _params) do socket |> assign(:page_title, "New Product category") |> assign(:product_category, nil) diff --git a/lib/therons_erp_web/live/product_live/index.ex b/lib/therons_erp_web/live/product_live/index.ex index e406ef8..1e193c7 100644 --- a/lib/therons_erp_web/live/product_live/index.ex +++ b/lib/therons_erp_web/live/product_live/index.ex @@ -51,7 +51,7 @@ defmodule TheronsErpWeb.ProductLive.Index do end @impl true - def mount(params, _session, socket) do + def mount(_params, _session, socket) do {:ok, socket |> stream( diff --git a/lib/therons_erp_web/live/sales_order_live/form_component.ex b/lib/therons_erp_web/live/sales_order_live/form_component.ex index cae05d5..249e883 100644 --- a/lib/therons_erp_web/live/sales_order_live/form_component.ex +++ b/lib/therons_erp_web/live/sales_order_live/form_component.ex @@ -21,8 +21,6 @@ defmodule TheronsErpWeb.SalesOrderLive.FormComponent do |> assign_form()} end - defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) - defp assign_form(%{assigns: %{sales_order: sales_order}} = socket) do form = if sales_order do diff --git a/lib/therons_erp_web/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex index 6b9277a..f5e47db 100644 --- a/lib/therons_erp_web/live/sales_order_live/show.ex +++ b/lib/therons_erp_web/live/sales_order_live/show.ex @@ -610,42 +610,6 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do |> assign(:total_cost_changes, Map.put(socket.assigns.total_cost_changes, index, false))} end - def erase_total_price_changes(sales_order_params, price_changes) do - new_lines = - for {id, line} <- sales_order_params["sales_lines"], into: %{} do - if price_changes[id] != false do - {id, line} - else - new_line = put_in(line["total_price"], nil) - {id, new_line} - end - end - - put_in(sales_order_params["sales_lines"], new_lines) - end - - def erase_total_cost_changes(sales_order_params, cost_changes) do - new_lines = - for {id, line} <- sales_order_params["sales_lines"], into: %{} do - if cost_changes[id] != false do - {id, line} - else - new_line = put_in(line["total_cost"], nil) - {id, new_line} - end - end - - put_in(sales_order_params["sales_lines"], new_lines) - end - - def process_modifications(sales_order_params, socket) do - sales_order_params = - erase_total_price_changes(sales_order_params, socket.assigns.total_price_changes) - - sales_order_params = - erase_total_cost_changes(sales_order_params, socket.assigns.total_cost_changes) - end - @impl true def handle_event( "validate", @@ -777,6 +741,157 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do {:noreply, socket |> assign(:sales_order, load_by_id(socket.assigns.sales_order.id, socket))} end + def handle_event( + "live_select_change", + %{"text" => text, "id" => "sales_order[sales_lines]" <> _ = id}, + socket + ) do + opts = + get_products("") + |> prepare_matches(text) + + send_update(LiveSelect.Component, id: id, options: opts) + + {:noreply, socket} + end + + def handle_event( + "live_select_change", + %{"text" => text, "id" => "sales_order_customer_id" <> _ = id}, + socket + ) do + opts = + get_customers("") + |> prepare_matches(text) + + send_update(LiveSelect.Component, id: id, options: opts) + + {:noreply, socket} + end + + @impl true + def handle_event( + "set-default-customers", + %{ + "id" => id + }, + socket + ) do + if cid = socket.assigns.from_args["customer_id"] do + opts = get_initial_customer_options(cid) + send_update(LiveSelect.Component, options: opts, id: id, value: cid) + else + text = + socket.assigns.set_customer.text || + (socket.assigns.sales_order.customer && socket.assigns.sales_order.customer.name) || "" + + value = socket.assigns.set_customer.value || socket.assigns.sales_order.customer_id + + opts = prepare_matches(socket.assigns.default_customers, text) + + send_update(LiveSelect.Component, options: opts, id: id, value: value) + end + + {:noreply, socket} + end + + @impl true + def handle_event( + "set-default", + %{"id" => id}, + socket + ) do + number = parse_select_id!(id) + + pid = socket.assigns.from_args["product_id"] + + if socket.assigns.from_args["product_id"] && + socket.assigns.from_args["line_id"] == to_string(number) do + opts = get_initial_product_options(pid) + + send_update(LiveSelect.Component, + options: opts, + id: id, + value: pid + ) + else + value = + socket.assigns.form + |> Phoenix.HTML.Form.input_value(:sales_lines) + |> Enum.at(String.to_integer(number)) + |> Phoenix.HTML.Form.input_value(:product_id) + + if value not in [nil, ""] do + opts = get_initial_product_options(value) + + send_update(LiveSelect.Component, + options: opts, + id: id, + value: value + ) + else + opts = get_initial_product_options(nil) + + send_update(LiveSelect.Component, + options: opts, + id: id, + value: nil + ) + end + end + + {:noreply, socket} + end + + defp get_address(form, addresses) do + id = Phoenix.HTML.Form.input_value(form, :address_id) + Enum.find(addresses, &(&1.id == id)) + end + + defp compare_monies_neq(a, b) do + case {a, b} do + {nil, nil} -> + false + + _ -> + not Money.equal?(a, b) + end + end + + def erase_total_price_changes(sales_order_params, price_changes) do + new_lines = + for {id, line} <- sales_order_params["sales_lines"], into: %{} do + if price_changes[id] != false do + {id, line} + else + new_line = put_in(line["total_price"], nil) + {id, new_line} + end + end + + put_in(sales_order_params["sales_lines"], new_lines) + end + + def erase_total_cost_changes(sales_order_params, cost_changes) do + new_lines = + for {id, line} <- sales_order_params["sales_lines"], into: %{} do + if cost_changes[id] != false do + {id, line} + else + new_line = put_in(line["total_cost"], nil) + {id, new_line} + end + end + + put_in(sales_order_params["sales_lines"], new_lines) + end + + def process_modifications(sales_order_params, socket) do + sales_order_params + |> erase_total_price_changes(socket.assigns.total_price_changes) + |> erase_total_cost_changes(socket.assigns.total_cost_changes) + end + defp is_active_price_persisted?(sales_line, index, total_price_changes) do if total_price_changes[to_string(index)] == false do false @@ -985,121 +1100,4 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do number end - - def handle_event( - "live_select_change", - %{"text" => text, "id" => "sales_order[sales_lines]" <> _ = id}, - socket - ) do - opts = - get_products("") - |> prepare_matches(text) - - send_update(LiveSelect.Component, id: id, options: opts) - - {:noreply, socket} - end - - def handle_event( - "live_select_change", - %{"text" => text, "id" => "sales_order_customer_id" <> _ = id}, - socket - ) do - opts = - get_customers("") - |> prepare_matches(text) - - send_update(LiveSelect.Component, id: id, options: opts) - - {:noreply, socket} - end - - @impl true - def handle_event( - "set-default-customers", - %{ - "id" => id - }, - socket - ) do - if cid = socket.assigns.from_args["customer_id"] do - opts = get_initial_customer_options(cid) - send_update(LiveSelect.Component, options: opts, id: id, value: cid) - else - text = - socket.assigns.set_customer.text || - (socket.assigns.sales_order.customer && socket.assigns.sales_order.customer.name) || "" - - value = socket.assigns.set_customer.value || socket.assigns.sales_order.customer_id - - opts = prepare_matches(socket.assigns.default_customers, text) - - send_update(LiveSelect.Component, options: opts, id: id, value: value) - end - - {:noreply, socket} - end - - @impl true - def handle_event( - "set-default", - %{"id" => id}, - socket - ) do - number = parse_select_id!(id) - - pid = socket.assigns.from_args["product_id"] - - if socket.assigns.from_args["product_id"] && - socket.assigns.from_args["line_id"] == to_string(number) do - opts = get_initial_product_options(pid) - - send_update(LiveSelect.Component, - options: opts, - id: id, - value: pid - ) - else - value = - socket.assigns.form - |> Phoenix.HTML.Form.input_value(:sales_lines) - |> Enum.at(String.to_integer(number)) - |> Phoenix.HTML.Form.input_value(:product_id) - - if value not in [nil, ""] do - opts = get_initial_product_options(value) - - send_update(LiveSelect.Component, - options: opts, - id: id, - value: value - ) - else - opts = get_initial_product_options(nil) - - send_update(LiveSelect.Component, - options: opts, - id: id, - value: nil - ) - end - end - - {:noreply, socket} - end - - defp get_address(form, addresses) do - id = Phoenix.HTML.Form.input_value(form, :address_id) - Enum.find(addresses, &(&1.id == id)) - end - - defp compare_monies_neq(a, b) do - case {a, b} do - {nil, nil} -> - false - - _ -> - not Money.equal?(a, b) - end - end end diff --git a/test/breadcrumb_test.exs b/test/breadcrumb_test.exs index f2eae23..9b18fa8 100644 --- a/test/breadcrumb_test.exs +++ b/test/breadcrumb_test.exs @@ -1,8 +1,6 @@ defmodule TheronsErpWeb.BreadcrumbTest do use TheronsErpWeb.ConnCase - import Phoenix.LiveViewTest - test "stream_crumbs" do out = [1, 2, 3] diff --git a/test/therons_erp/invoice_test.exs b/test/therons_erp/invoice_test.exs index 38be833..b6cec76 100644 --- a/test/therons_erp/invoice_test.exs +++ b/test/therons_erp/invoice_test.exs @@ -2,20 +2,30 @@ defmodule TheronsErp.InvoiceTest do use TheronsErp.DataCase alias TheronsErp.Sales alias TheronsErp.Inventory - alias TheronsErp.Sales.{SalesOrder, SalesLine} + alias TheronsErp.Sales.{SalesLine} alias TheronsErp.Invoices.Invoice test "copying line items" do sales_order = Sales.create_draft!() - product = Inventory.create_product!("Test Product", Money.new(100, :USD), %{cost: Money.new(50, :USD)}) + + product = + Inventory.create_product!("Test Product", Money.new(100, :USD), %{cost: Money.new(50, :USD)}) + sales_line = SalesLine - |> Ash.Changeset.for_create(:create, %{sales_order_id: sales_order.id, product_id: product.id, quantity: 1}) + |> Ash.Changeset.for_create(:create, %{ + sales_order_id: sales_order.id, + product_id: product.id, + quantity: 1 + }) |> Ash.create!() invoice = Invoice - |> Ash.Changeset.for_create(:create, %{sales_order_id: sales_order.id, sales_lines: [sales_line]}) + |> Ash.Changeset.for_create(:create, %{ + sales_order_id: sales_order.id, + sales_lines: [sales_line] + }) |> Ash.create!() |> Ash.load!(:line_items) From 3cfd07f180a7ee16375c6982a9e54c534e1a3693 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 16 Feb 2025 23:29:27 -0500 Subject: [PATCH 90/90] Tool versions and mix version bump --- .tool-versions | 2 ++ mix.exs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..5ba9039 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.18.2 +erlang 27.2.2 diff --git a/mix.exs b/mix.exs index 21a02aa..a784c5f 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule TheronsErp.MixProject do [ app: :therons_erp, version: "0.1.0", - elixir: "~> 1.14", + elixir: "~> 1.18.2", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(),