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..b9d5908
--- /dev/null
+++ b/lib/therons_erp_web/live/entity_live/form_component.ex
@@ -0,0 +1,142 @@
+defmodule TheronsErpWeb.EntityLive.FormComponent do
+ # I don't think this is used because the new action creates
+ use TheronsErpWeb, :live_component
+ alias TheronsErpWeb.Breadcrumbs
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ {@title}
+ <:subtitle>
+
+
+ <%= 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"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.input field={@form[:name]} type="text" label="Name" />
+ <.inputs_for :let={address} field={@form[:addresses]}>
+
+
+
+
+
+ <: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")
+ |> Breadcrumbs.navigate_back({"people", entity.id}, %{customer_id: entity.id})
+
+ {:noreply, socket}
+
+ {:error, form} ->
+ {:noreply, assign(socket, form: form)}
+ 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
+ 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..253c192
--- /dev/null
+++ b/lib/therons_erp_web/live/entity_live/index.ex
@@ -0,0 +1,90 @@
+defmodule TheronsErpWeb.EntityLive.Index do
+ use TheronsErpWeb, :live_view
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <.header>
+ Listing Entities
+ <:actions>
+ <.link patch={~p"/people/new"}>
+ <.button>New Entity
+
+
+
+
+ <.table
+ id="entities"
+ rows={@streams.entities}
+ row_click={fn {_id, entity} -> JS.navigate(~p"/people/#{entity}") end}
+ >
+ <:col :let={{_id, entity}} label="Name">{entity.name}
+
+ <:action :let={{_id, entity}}>
+
+
+ <.link patch={~p"/people/#{entity}/edit"}>Edit
+
+
+
+ <.modal
+ :if={@live_action in [:new, :edit]}
+ id="entity-modal"
+ show
+ on_cancel={JS.patch(~p"/people")}
+ >
+ <.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"/people"}
+ breadcrumbs={@breadcrumbs}
+ />
+
+ """
+ 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/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
new file mode 100644
index 0000000..39e592e
--- /dev/null
+++ b/lib/therons_erp_web/live/entity_live/show.ex
@@ -0,0 +1,144 @@
+defmodule TheronsErpWeb.EntityLive.Show do
+ use TheronsErpWeb, :live_view
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <.header>
+ Entity {@entity.name}
+ <:subtitle>
+
+ <:actions>
+ <.link patch={~p"/people/#{@entity}/show/edit"} phx-click={JS.push_focus()}>
+ <.button>Edit entity
+
+
+
+
+
+ <% end %>
+
+ <.back navigate={~p"/people"}>Back to entities
+
+ <.modal
+ :if={@live_action == :edit}
+ id="entity-modal"
+ show
+ on_cancel={
+ JS.navigate(
+ TheronsErpWeb.Breadcrumbs.get_previous_path(
+ @breadcrumbs,
+ {"people", @entity.id}
+ )
+ )
+ }
+ >
+ <.live_component
+ module={TheronsErpWeb.EntityLive.FormComponent}
+ id={@entity.id}
+ title={@page_title}
+ action={@live_action}
+ current_user={@current_user}
+ entity={@entity}
+ patch={~p"/people/#{@entity}"}
+ 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
+
+ @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,
+ load: [:addresses]
+ )
+ )}
+ end
+
+ 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/nav.ex b/lib/therons_erp_web/live/nav.ex
new file mode 100644
index 0000000..89d4b12
--- /dev/null
+++ b/lib/therons_erp_web/live/nav.ex
@@ -0,0 +1,36 @@
+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
+
+ {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
+
+ {:cont, assign(socket, :active_tab, active_tab)}
+ end
+end
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_category_live/show.ex b/lib/therons_erp_web/live/product_category_live/show.ex
index 905a177..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
- raise e
+ reraise e, __STACKTRACE__
end
_ ->
- raise e
+ reraise e, __STACKTRACE__
end
end
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
deleted file mode 100644
index 4604f2e..0000000
--- a/lib/therons_erp_web/live/product_live/form_component.ex
+++ /dev/null
@@ -1,151 +0,0 @@
-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
- ~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"
- />
- <%!-- <.input
- field={@form[:category_id]}
- type="text"
- label="Category"
- /> --%>
-
- <.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
- # TODO add breadcrumbs
- 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 eb53781..1e193c7 100644
--- a/lib/therons_erp_web/live/product_live/index.ex
+++ b/lib/therons_erp_web/live/product_live/index.ex
@@ -18,17 +18,24 @@ 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) || ""}
+ <:col :let={{_id, product}} label="Saleable">
+
-
- <.link patch={~p"/products/#{product}/edit"}>Edit
<:action :let={{id, product}}>
@@ -40,37 +47,16 @@ 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
@impl true
- def mount(params, _session, socket) do
+ def mount(_params, _session, socket) do
{:ok,
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
@@ -92,10 +78,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}?#{[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 0a06b67..b740b26 100644
--- a/lib/therons_erp_web/live/product_live/show.ex
+++ b/lib/therons_erp_web/live/product_live/show.ex
@@ -9,84 +9,81 @@ 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}]
- <%= if @unsaved_changes do %>
+
+ <%= if @line_id not in ["", nil] 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>
-
- <: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 %>
+
+ <.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"
+ 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) not in ["create", nil] 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
- :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
@@ -115,6 +112,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
@@ -194,7 +192,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}
@@ -245,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/lib/therons_erp_web/live/sales_order_live/form_component.ex b/lib/therons_erp_web/live/sales_order_live/form_component.ex
index 15d936f..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,31 +21,6 @@ defmodule TheronsErpWeb.SalesOrderLive.FormComponent do
|> assign_form()}
end
- @impl true
- def handle_event("validate", %{"sales_order" => sales_order_params}, socket) do
- {:noreply,
- assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, sales_order_params))}
- end
-
- def handle_event("save", %{"sales_order" => sales_order_params}, socket) do
- case AshPhoenix.Form.submit(socket.assigns.form, params: sales_order_params) do
- {:ok, sales_order} ->
- notify_parent({:saved, sales_order})
-
- socket =
- socket
- |> put_flash(:info, "Sales order #{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: %{sales_order: sales_order}} = socket) do
form =
if sales_order do
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..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
@@ -18,7 +19,14 @@ 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}
+ <.status_badge state={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}}>
@@ -57,13 +65,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
@@ -78,7 +91,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
@@ -88,7 +104,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/live/sales_order_live/show.ex b/lib/therons_erp_web/live/sales_order_live/show.ex
index ff8194f..f5e47db 100644
--- a/lib/therons_erp_web/live/sales_order_live/show.ex
+++ b/lib/therons_erp_web/live/sales_order_live/show.ex
@@ -1,12 +1,19 @@
defmodule TheronsErpWeb.SalesOrderLive.Show do
+ alias TheronsErpWeb.Breadcrumbs
use TheronsErpWeb, :live_view
+ 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"""
<.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}
+ <.status_badge state={@sales_order.state} />
<%= if @unsaved_changes do %>
<.button phx-disable-with="Saving..." class="save-button">
<.icon name="hero-check-circle" />
@@ -18,68 +25,383 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do
<% end %>
<% end %>
- <:subtitle>This is a sales_order record from your database.
<:actions>
- <%!-- <.link patch={~p"/sales_orders/#{@sales_order}/show/edit"} phx-click={JS.push_focus()}>
- <.button>Edit sales_order
- --%>
+ <%= 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
+
+ <.button phx-disable-with="Saving..." phx-click="set-invoice">
+ Generate invoice
+
+ <% end %>
+ <:subtitle>
- <.list>
- <:item title="Id">{@sales_order.id}
-
+ <%= 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
+
+ <.live_select
+ field={@form[:customer_id]}
+ 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="Customer"
+ >
+ <: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 %>
+
+
+
+ <%= if Phoenix.HTML.Form.input_value(@form, :customer_id) do %>
+
+ <.input
+ field={@form[:address_id]}
+ type="select"
+ label="Address"
+ options={
+ [{"Unselected", nil}] ++
+ Enum.map(@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 %>
+
+ <%!-- 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 %>
+
+ Product |
Quantity |
Sales Price |
Unit Price |
+ Total Price |
+ Total Cost |
- <.inputs_for :let={sales_line} field={@form[:sales_lines]}>
-
-
- <.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"
- />
- |
-
-
- |
-
-
+ <%= if @sales_order.state == :draft do %>
+ <.inputs_for :let={sales_line} field={@form[:sales_lines]}>
+
+
+ <.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"
+ 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[: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_cost]}
+ value={
+ total_cost_for_sales_line(sales_line, sales_line.index, @total_cost_changes)
+ }
+ type="number"
+ inline_container={true}
+ />
+ <%= 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"
+ name="revert"
+ value={"revert-total-cost-#{sales_line.index}"}
+ >
+ <.icon name="hero-arrow-uturn-left" />
+
+ <% end %>
+
+ |
+
+
+ |
+
+
+ <% else %>
+ <%= for sales_line <- @sales_order.sales_lines do %>
+
+
+ <.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}
+ |
+
+ $ {sales_line.sales_price.amount |> Decimal.to_float()}
+ |
+
+ $ {sales_line.unit_price.amount |> Decimal.to_float()}
+ |
+
+ $ {sales_line.active_price.amount |> Decimal.to_float()}
+ |
+
+ $ {sales_line.total_cost.amount |> Decimal.to_float()}
+ |
+
+ <% end %>
+ <% end %>
-
+ <%= if @sales_order.state == :draft do %>
+
+ <% end %>
+
+
+
+ (Subtotal) $ {if @sales_order.total_price,
+ do: @sales_order.total_price.amount |> Decimal.to_float(),
+ else: 0}
+
+
+ (Cost) $ {if @sales_order.total_cost,
+ do: @sales_order.total_cost.amount |> Decimal.to_float(),
+ else: 0}
+
+
+
+ (Net) $ {Money.sub!(
+ @sales_order.total_price || Money.new(0, :USD),
+ @sales_order.total_cost || Money.new(0, :USD)
+ ).amount
+ |> Decimal.to_float()}
+
+
<.back navigate={~p"/sales_orders"}>Back to sales_orders
@@ -125,37 +447,259 @@ 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,
+ :address,
+ :invoice,
+ sales_lines: [:total_price, :product, :active_price, :calculated_total_price, :total_cost],
+ customer: [:addresses]
+ ]
+ )
+ end
+
@impl true
- def handle_params(%{"id" => id}, _, socket) do
+ def handle_params(%{"id" => id} = params, _, socket) do
+ sales_order = load_by_id(id, socket)
+
+ 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]
- )
+ 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(: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
defp page_title(:show), do: "Show Sales order"
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"])
+ 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
+
+ def handle_event(
+ "save",
+ %{"revert" => "revert-cost-" <> index, "sales_order" => params},
+ socket
+ ) do
+ 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)
+
+ {: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_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)
+
+ {: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},
+ socket
+ ) do
+ new_params = put_in(params, ["sales_lines", index, "total_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-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)
{:noreply,
- assign(socket, form: form)
- |> assign(:unsaved_changes, form.source.changed? || drop > 0)
- |> assign(:drop_sales, drop)}
+ socket
+ |> assign(:form, to_form(form))
+ |> assign(:params, new_params)
+ |> assign(:unsaved_changes, form.source.changed?)
+ |> assign(:total_cost_changes, Map.put(socket.assigns.total_cost_changes, index, false))}
+ end
+
+ @impl true
+ def handle_event(
+ "validate",
+ %{"sales_order" => sales_order_params, "_target" => target} = params,
+ socket
+ ) do
+ sales_order_params = process_modifications(sales_order_params, socket)
+
+ 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} ->
+ {id, val["product_id"]}
+ end)
+ |> Enum.find(fn {_id, val} -> val == "create" end)
+
+ has_create =
+ case output do
+ {id, "create"} -> id
+ _ -> nil
+ end
+
+ if sales_order_params["address_id"] == "create" and
+ sales_order_params["customer_id"] not in [nil, "create"] and
+ target != ["sales_order", "customer_id"] do
+ {:noreply,
+ socket
+ |> Breadcrumbs.navigate_to(
+ {"addresses", "new", sales_order_params["customer_id"]},
+ {"sales_orders", socket.assigns.sales_order.id, sales_order_params,
+ socket.assigns.sales_order.identifier}
+ )}
+ else
+ if has_create do
+ {:noreply,
+ socket
+ |> Breadcrumbs.navigate_to(
+ {"products", "new", has_create},
+ {"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
+
+ {: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
+
+ # 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"] || [])
+
+ {:noreply,
+ 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
+ end
+ end
end
def handle_event("save", %{"sales_order" => sales_order_params}, socket) do
+ sales_order_params = process_modifications(sales_order_params, socket)
+
case AshPhoenix.Form.submit(socket.assigns.form, params: sales_order_params) do
{:ok, sales_order} ->
# notify_parent({:saved, sales_order})
@@ -165,12 +709,13 @@ 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()
+ |> push_navigate(
+ to:
+ ~p"/sales_orders/#{sales_order.id}?#{[breadcrumbs: Breadcrumbs.encode_breadcrumbs(socket.assigns.breadcrumbs)]}"
+ )
# |> push_patch(to: socket.assigns.patch)
@@ -181,7 +726,288 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do
end
end
- defp assign_form(%{assigns: %{sales_order: sales_order}} = socket) do
+ 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
+
+ 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
+
+ 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
+
+ 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
+ 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
+ end
+ end
+ end
+
+ 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
+ end
+ end
+ end
+
+ 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)
+ nil -> nil
+ total_price -> total_price
+ end
+
+ 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()
+
+ _ ->
+ 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)
+
+ case {sales_price, quantity} do
+ {nil, nil} ->
+ ""
+
+ {_, nil} ->
+ ""
+
+ {nil, _} ->
+ ""
+
+ {_, _} ->
+ Ash.calculate!(TheronsErp.Sales.SalesLine, :calculated_total_price,
+ refs: %{sales_price: sales_price, quantity: quantity}
+ )
+ |> Money.to_decimal()
+ |> Decimal.to_string()
+ end
+ end
+ end
+
+ # TODO consider using Ash.calculate!/2
+ 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)
+ nil -> nil
+ total_cost -> total_cost
+ end
+
+ 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()
+
+ _ ->
+ 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)
+
+ case {unit_price, quantity} do
+ {nil, nil} ->
+ ""
+
+ {_, nil} ->
+ ""
+
+ {nil, _} ->
+ ""
+
+ {_, _} ->
+ Ash.calculate!(TheronsErp.Sales.SalesLine, :total_cost,
+ refs: %{unit_price: unit_price, quantity: quantity}
+ )
+ |> Money.to_decimal()
+ |> Decimal.to_string()
+ end
+ end
+ end
+
+ 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,
@@ -195,8 +1021,83 @@ defmodule TheronsErpWeb.SalesOrderLive.Show do
)
end
+ form =
+ case {args, from_args} do
+ {nil, nil} ->
+ form
+
+ {_, nil} ->
+ AshPhoenix.Form.validate(form, args)
+
+ {nil, _} ->
+ new_args =
+ put_in(
+ 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 =
+ 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)
+ end
+ end
+
socket
|> assign(form: to_form(form))
|> assign(:unsaved_changes, form.changed?)
end
+
+ defp update_live_forms(new_args) do
+ for {line_no, line} <- new_args["sales_lines"] do
+ pid =
+ line["product_id"]
+
+ if pid not in [nil, ""] do
+ opts = get_initial_product_options(pid)
+
+ id =
+ "sales_order[sales_lines][#{line_no}]_product_id_live_select_component"
+
+ send_update(LiveSelect.Component,
+ options: opts,
+ id: id,
+ value: pid
+ )
+ end
+ end
+ end
+
+ defp parse_select_id!(id) do
+ [_, number] =
+ Regex.run(~r/sales_order\[sales_lines\]\[(\d+)\]_product_id_live_select_component/, id)
+
+ number
+ end
end
diff --git a/lib/therons_erp_web/router.ex b/lib/therons_erp_web/router.ex
index 01b6610..a6a770a 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
@@ -57,17 +61,22 @@ 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
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/new_address", EntityLive.Show, :new_address
+ 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_line.ex b/lib/therons_erp_web/sales/sales_line.ex
deleted file mode 100644
index 15ed6b5..0000000
--- a/lib/therons_erp_web/sales/sales_line.ex
+++ /dev/null
@@ -1,39 +0,0 @@
-defmodule TheronsErp.Sales.SalesLine do
- use Ash.Resource,
- otp_app: :therons_erp,
- domain: TheronsErp.Sales,
- data_layer: AshPostgres.DataLayer
-
- postgres do
- table "sales_lines"
- repo TheronsErp.Repo
- end
-
- actions do
- defaults [:destroy, :read]
-
- create :create do
- primary? true
- accept [:sales_price, :unit_price, :quantity]
- end
-
- update :update do
- primary? true
- accept [:sales_price, :unit_price, :quantity]
- end
- end
-
- attributes do
- uuid_primary_key :id
-
- attribute :sales_price, :money
- attribute :unit_price, :money
- attribute :quantity, :integer
- timestamps()
- end
-
- relationships do
- belongs_to :sales_order, TheronsErp.Sales.SalesOrder
- belongs_to :product, TheronsErp.Inventory.Product
- end
-end
diff --git a/lib/therons_erp_web/sales/sales_order.ex b/lib/therons_erp_web/sales/sales_order.ex
deleted file mode 100644
index 07acc27..0000000
--- a/lib/therons_erp_web/sales/sales_order.ex
+++ /dev/null
@@ -1,43 +0,0 @@
-defmodule TheronsErp.Sales.SalesOrder do
- use Ash.Resource,
- otp_app: :therons_erp,
- domain: TheronsErp.Sales,
- data_layer: AshPostgres.DataLayer
-
- postgres do
- table "sales_orders"
- repo TheronsErp.Repo
- end
-
- actions do
- defaults [:read]
-
- destroy :destroy do
- end
-
- create :create do
- argument :sales_lines, {:array, :map}
-
- change manage_relationship(:sales_lines, type: :create)
- end
-
- update :update do
- require_atomic? false
- argument :sales_lines, {:array, :map}
-
- change manage_relationship(:sales_lines, type: :direct_control)
- end
- end
-
- attributes do
- uuid_primary_key :id
-
- timestamps()
- end
-
- relationships do
- has_many :sales_lines, TheronsErp.Sales.SalesLine do
- destination_attribute :sales_order_id
- end
- end
-end
diff --git a/lib/therons_erp_web/selects.ex b/lib/therons_erp_web/selects.ex
index 8368d7b..11ca157 100644
--- a/lib/therons_erp_web/selects.ex
+++ b/lib/therons_erp_web/selects.ex
@@ -1,24 +1,37 @@
defmodule TheronsErpWeb.Selects do
+ alias TheronsErp.People
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_saleable_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 =
- 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,16 +56,57 @@ defmodule TheronsErpWeb.Selects do
]
end
+ def additional_product_options do
+ [
+ %{
+ value: :create,
+ label: "Create New",
+ matches: []
+ }
+ ]
+ end
+
+ def additional_customer_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)
+ (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()
+ |> Enum.take(4)) ++ additional_product_options()
+ end
def get_category_name(categories, id) do
found =
categories
|> Enum.find(&(to_string(&1.value) == to_string(id)))
+ if found do
+ found.label
+ else
+ nil
+ end
+ end
+
+ def get_product_name(products, id) do
+ found =
+ products
+ |> Enum.find(&(to_string(&1.value) == to_string(id)))
+
found.label
end
end
diff --git a/mix.exs b/mix.exs
index 23e1a38..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(),
@@ -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"},
diff --git a/priv/repo/migrations/20250210171140_total_prices_on_sales_orders.exs b/priv/repo/migrations/20250210171140_total_prices_on_sales_orders.exs
new file mode 100644
index 0000000..4cf973d
--- /dev/null
+++ b/priv/repo/migrations/20250210171140_total_prices_on_sales_orders.exs
@@ -0,0 +1,25 @@
+defmodule TheronsErp.Repo.Migrations.TotalPricesOnSalesOrders 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 :product_id, :uuid, null: false
+ modify :sales_order_id, :uuid, null: false
+ add :total_price, :money_with_currency
+ end
+ end
+
+ def down do
+ alter table(:sales_lines) do
+ remove :total_price
+ modify :sales_order_id, :uuid, null: true
+ modify :product_id, :uuid, null: true
+ end
+ end
+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/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/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/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/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/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/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/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/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/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/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/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/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/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
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
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
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/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/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/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/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
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
diff --git a/priv/resource_snapshots/repo/sales_lines/20250210171140.json b/priv/resource_snapshots/repo/sales_lines/20250210171140.json
new file mode 100644
index 0000000..add9914
--- /dev/null
+++ b/priv/resource_snapshots/repo/sales_lines/20250210171140.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": "nil",
+ "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": "E32866C9550F2867C2A49A869209523D057E8EE7B3B1679DC3D0B8E4CF2B8D2C",
+ "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/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/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
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
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
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
diff --git a/test/breadcrumb_test.exs b/test/breadcrumb_test.exs
index 83ca9c1..9b18fa8 100644
--- a/test/breadcrumb_test.exs
+++ b/test/breadcrumb_test.exs
@@ -1,14 +1,12 @@
-defmodule TheronsErpWeb.ProductCategoryLiveTest do
+defmodule TheronsErpWeb.BreadcrumbTest do
use TheronsErpWeb.ConnCase
- import Phoenix.LiveViewTest
-
test "stream_crumbs" do
out =
[1, 2, 3]
|> 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
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/invoice_test.exs b/test/therons_erp/invoice_test.exs
new file mode 100644
index 0000000..b6cec76
--- /dev/null
+++ b/test/therons_erp/invoice_test.exs
@@ -0,0 +1,37 @@
+defmodule TheronsErp.InvoiceTest do
+ use TheronsErp.DataCase
+ alias TheronsErp.Sales
+ alias TheronsErp.Inventory
+ 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)})
+
+ sales_line =
+ SalesLine
+ |> 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.create!()
+ |> Ash.load!(:line_items)
+
+ [line_item] = invoice.line_items
+ assert line_item.product_id == product.id
+ assert Money.equal?(line_item.price, sales_line.sales_price)
+ assert line_item.quantity == sales_line.quantity
+ 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
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..b4162ee
--- /dev/null
+++ b/test/therons_erp_web/live/sales_orders_live.exs
@@ -0,0 +1,3 @@
+defmodule TheronsErpWeb.SalesOrdersLiveTest do
+ use TheronsErpWeb.ConnCase
+end