defmodule Athena.Inventory do
@moduledoc """
The Inventory context.
"""
import Ecto.Query, warn: false
alias Athena.Inventory.Event
alias Athena.Inventory.Item
alias Athena.Inventory.ItemGroup
alias Athena.Inventory.Location
alias Athena.Inventory.Movement
alias Athena.Repo
alias Phoenix.PubSub
@movement_sum from(m in Movement,
select: sum(m.amount)
)
@movement_relocations where(
@movement_sum,
[m],
not is_nil(m.source_location_id) and
not is_nil(m.destination_location_id)
)
@movement_supply where(@movement_sum, [m], is_nil(m.source_location_id))
@movement_consumption where(@movement_sum, [m], is_nil(m.destination_location_id))
defp notify_pubsub(result, action, resource_name, modifiers \\ [], extra \\ %{})
defp notify_pubsub({:ok, %{id: id} = result}, action, resource_name, modifiers, extra) do
message = {action, result, extra}
for modifier <- modifiers do
:ok = PubSub.broadcast(Athena.PubSub, "#{resource_name}:#{modifier}", message)
:ok = PubSub.broadcast(Athena.PubSub, "#{resource_name}:#{action}:#{modifier}", message)
:ok =
PubSub.broadcast(Athena.PubSub, "#{resource_name}:#{action}:#{id}:#{modifier}", message)
end
:ok = PubSub.broadcast(Athena.PubSub, "#{resource_name}", message)
:ok = PubSub.broadcast(Athena.PubSub, "#{resource_name}:#{action}", message)
:ok = PubSub.broadcast(Athena.PubSub, "#{resource_name}:#{action}:#{id}", message)
{:ok, result}
end
defp notify_pubsub(other_result, _action, _resource_name, _modifiers, _extra), do: other_result
@doc """
Returns the list of events.
## Examples
iex> list_events()
[%Event{}, ...]
"""
def list_events, do: Repo.all(Event)
@doc """
Gets a single event.
Raises `Ecto.NoResultsError` if the Event does not exist.
## Examples
iex> get_event!(123)
%Event{}
iex> get_event!(456)
** (Ecto.NoResultsError)
"""
def get_event!(id), do: Repo.get!(Event, id)
@doc """
Creates a event.
## Examples
iex> create_event(%{field: value})
{:ok, %Event{}}
iex> create_event(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_event(attrs \\ %{}),
do:
%Event{}
|> change_event(attrs)
|> Repo.insert()
|> notify_pubsub(:created, "event")
@doc """
Updates a event.
## Examples
iex> update_event(event, %{field: new_value})
{:ok, %Event{}}
iex> update_event(event, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_event(%Event{} = event, attrs),
do:
event
|> change_event(attrs)
|> Repo.update()
|> notify_pubsub(:updated, "event")
@doc """
Deletes a event.
## Examples
iex> delete_event(event)
{:ok, %Event{}}
iex> delete_event(event)
{:error, %Ecto.Changeset{}}
"""
def delete_event(%Event{} = event),
do:
event
|> Repo.delete()
|> notify_pubsub(:deleted, "event")
@doc """
Returns an `%Ecto.Changeset{}` for tracking event changes.
## Examples
iex> change_event(event)
%Ecto.Changeset{source: %Event{}}
"""
def change_event(%Event{} = event, attrs \\ %{}), do: Event.changeset(event, attrs)
@doc """
Returns the list of locations.
## Examples
iex> list_locations()
[%Location{}, ...]
"""
def list_locations(event),
do: event |> Ecto.assoc(:locations) |> order_by([l], l.name) |> Repo.all()
@doc """
Gets a single location.
Raises `Ecto.NoResultsError` if the Location does not exist.
## Examples
iex> get_location!(123)
%Location{}
iex> get_location!(456)
** (Ecto.NoResultsError)
"""
def get_location!(id), do: Repo.get!(Location, id)
@doc """
Creates a location.
## Examples
iex> create_location(%{field: value})
{:ok, %Location{}}
iex> create_location(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_location(%Event{id: event_id} = event, attrs \\ %{}),
do:
event
|> Ecto.build_assoc(:locations)
|> change_location(attrs)
|> Repo.insert()
|> notify_pubsub(:created, "location", ["event:#{event_id}"])
@doc """
Updates a location.
## Examples
iex> update_location(location, %{field: new_value})
{:ok, %Location{}}
iex> update_location(location, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_location(%Location{event_id: event_id} = location, attrs),
do:
location
|> change_location(attrs)
|> Repo.update()
|> notify_pubsub(:updated, "location", ["event:#{event_id}"])
@doc """
Deletes a location.
## Examples
iex> delete_location(location)
{:ok, %Location{}}
iex> delete_location(location)
{:error, %Ecto.Changeset{}}
"""
def delete_location(%Location{event_id: event_id} = location),
do:
location
|> Repo.delete()
|> notify_pubsub(:deleted, "location", ["event:#{event_id}"])
@doc """
Returns an `%Ecto.Changeset{}` for tracking location changes.
## Examples
iex> change_location(location)
%Ecto.Changeset{source: %Location{}}
"""
def change_location(%Location{} = location, attrs \\ %{}),
do: Location.changeset(location, attrs)
@doc """
Returns the list of item_groups.
## Examples
iex> list_item_groups()
[%ItemGroup{}, ...]
"""
def list_item_groups(event),
do: event |> Ecto.assoc(:item_groups) |> order_by([ig], ig.name) |> Repo.all()
def list_relevant_item_groups(%Location{id: location_id}),
do:
Repo.all(
from(ig in ItemGroup,
join: i in assoc(ig, :items),
join: m in assoc(i, :movements),
where:
m.source_location_id == ^location_id or m.destination_location_id == ^location_id,
group_by: ig.id,
order_by: ig.name
)
)
@doc """
Gets a single item_group.
Raises `Ecto.NoResultsError` if the Item group does not exist.
## Examples
iex> get_item_group!(123)
%ItemGroup{}
iex> get_item_group!(456)
** (Ecto.NoResultsError)
"""
def get_item_group!(id), do: Repo.get!(ItemGroup, id)
@doc """
Creates a item_group.
## Examples
iex> create_item_group(%{field: value})
{:ok, %ItemGroup{}}
iex> create_item_group(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_item_group(%Event{id: event_id} = event, attrs \\ %{}),
do:
event
|> Ecto.build_assoc(:item_groups)
|> change_item_group(attrs)
|> Repo.insert()
|> notify_pubsub(:created, "item_group", ["event:#{event_id}"])
@doc """
Updates a item_group.
## Examples
iex> update_item_group(item_group, %{field: new_value})
{:ok, %ItemGroup{}}
iex> update_item_group(item_group, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_item_group(%ItemGroup{event_id: event_id} = item_group, attrs),
do:
item_group
|> change_item_group(attrs)
|> Repo.update()
|> notify_pubsub(:updated, "item_group", ["event:#{event_id}"])
@doc """
Deletes a item_group.
## Examples
iex> delete_item_group(item_group)
{:ok, %ItemGroup{}}
iex> delete_item_group(item_group)
{:error, %Ecto.Changeset{}}
"""
def delete_item_group(%ItemGroup{event_id: event_id} = item_group),
do:
item_group
|> Repo.delete()
|> notify_pubsub(:deleted, "item_group", ["event:#{event_id}"])
@doc """
Returns an `%Ecto.Changeset{}` for tracking item_group changes.
## Examples
iex> change_item_group(item_group)
%Ecto.Changeset{source: %ItemGroup{}}
"""
def change_item_group(%ItemGroup{} = item_group, attrs \\ %{}),
do: ItemGroup.changeset(item_group, attrs)
@doc """
Returns the list of items.
## Examples
iex> list_items()
[%Item{}, ...]
"""
def list_items(%ItemGroup{} = item_group), do: item_group |> Ecto.assoc(:items) |> Repo.all()
def list_items(%Event{} = event), do: event |> Ecto.assoc(:items) |> Repo.all()
def list_relevant_items_query(location)
def list_relevant_items_query(%Location{id: location_id}) do
from(item in Item,
join: item_group in assoc(item, :item_group),
as: :item_group,
join: movement in assoc(item, :movements),
as: :movement,
where:
movement.source_location_id == ^location_id or
movement.destination_location_id == ^location_id,
group_by: item.id,
order_by: item.name
)
end
def list_relevant_item_groups_query(%Location{} = location) do
relevant_items_query =
location
|> list_relevant_items_query
|> Ecto.Query.exclude(:group_by)
|> Ecto.Query.exclude(:order_by)
from([item, item_group: item_group] in relevant_items_query,
group_by: item_group.id,
select: item_group,
order_by: item_group.name
)
end
def list_relevant_items(%Location{} = location, %ItemGroup{id: item_group_id}) do
Repo.all(
from(
[item, item_group: item_group] in list_relevant_items_query(location),
where: item_group.id == ^item_group_id
)
)
end
@doc """
Gets a single item.
Raises `Ecto.NoResultsError` if the Item does not exist.
## Examples
iex> get_item!(123)
%Item{}
iex> get_item!(456)
** (Ecto.NoResultsError)
"""
def get_item!(id), do: Repo.get!(Item, id)
defp get_item_movement_sum(query, %Item{id: item_id}) do
query
|> where([m], m.item_id == ^item_id)
|> Repo.all()
|> case do
[nil] -> 0
[sum] -> sum
end
end
defp get_item_movement_sum_out(query, item, %Location{id: location_id}),
do:
query
|> where([m], m.source_location_id == ^location_id)
|> get_item_movement_sum(item)
defp get_item_movement_sum_in(query, item, %Location{id: location_id}),
do:
query
|> where([m], m.destination_location_id == ^location_id)
|> get_item_movement_sum(item)
def get_item_consumption(item), do: get_item_movement_sum(@movement_consumption, item)
def get_item_consumption(item, location),
do: get_item_movement_sum_out(@movement_consumption, item, location)
def get_item_supply(item), do: get_item_movement_sum(@movement_supply, item)
def get_item_supply(item, location),
do: get_item_movement_sum_in(@movement_supply, item, location)
def get_item_relocations(item), do: get_item_movement_sum(@movement_relocations, item)
def get_item_relocations_out(item, location),
do: get_item_movement_sum_out(@movement_relocations, item, location)
def get_item_relocations_in(item, location),
do: get_item_movement_sum_in(@movement_relocations, item, location)
def get_item_stock(item), do: get_item_supply(item) - get_item_consumption(item)
def get_item_stock(item, location),
do:
get_item_supply(item, location) - get_item_relocations_out(item, location) +
get_item_relocations_in(item, location) - get_item_consumption(item, location)
@doc """
Creates a item.
## Examples
iex> create_item(%{field: value})
{:ok, %Item{}}
iex> create_item(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_item(%{id: item_group_id, event_id: event_id} = item_group, attrs \\ %{}),
do:
item_group
|> Ecto.build_assoc(:items)
|> change_item(attrs)
|> Repo.insert()
|> notify_pubsub(:created, "item", ["item_group:#{item_group_id}", "event:#{event_id}"])
@doc """
Updates a item.
## Examples
iex> update_item(item, %{field: new_value})
{:ok, %Item{}}
iex> update_item(item, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_item(%Item{item_group_id: item_group_id} = item, attrs) do
%{event: %{id: event_id}} = Repo.preload(item, :event)
item
|> change_item(attrs)
|> Repo.update()
|> notify_pubsub(:updated, "item", ["item_group:#{item_group_id}", "event:#{event_id}"])
end
@doc """
Deletes a item.
## Examples
iex> delete_item(item)
{:ok, %Item{}}
iex> delete_item(item)
{:error, %Ecto.Changeset{}}
"""
def delete_item(%Item{item_group_id: item_group_id} = item) do
%{event: %{id: event_id}} = Repo.preload(item, :event)
item
|> Repo.delete()
|> notify_pubsub(:deleted, "item", ["item_group:#{item_group_id}", "event:#{event_id}"])
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking item changes.
## Examples
iex> change_item(item)
%Ecto.Changeset{source: %Item{}}
"""
def change_item(%Item{} = item, attrs \\ %{}), do: Item.changeset(item, attrs)
@doc """
Returns the list of movements.
## Examples
iex> list_movements()
[%Movement{}, ...]
"""
def list_movements(%Location{id: location_id}),
do:
Repo.all(
from m in Movement,
where:
m.source_location_id == ^location_id or m.destination_location_id == ^location_id,
order_by: m.inserted_at
)
def list_movements(%Item{} = item), do: item |> Ecto.assoc(:movements) |> Repo.all()
@doc """
Gets a single movement.
Raises `Ecto.NoResultsError` if the Movement does not exist.
## Examples
iex> get_movement!(123)
%Movement{}
iex> get_movement!(456)
** (Ecto.NoResultsError)
"""
def get_movement!(id), do: Repo.get!(Movement, id)
@doc """
Creates a movement.
## Examples
iex> create_movement(%{field: value})
{:ok, %Movement{}}
iex> create_movement(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_movement(%Item{id: item_id} = item, attrs \\ %{}) do
%{event: %{id: event_id}, item_group: %{id: item_group_id}} =
Repo.preload(item, [:item_group, :event])
item
|> Ecto.build_assoc(:movements)
|> change_movement(attrs)
|> Repo.insert()
|> case do
{:ok,
%{source_location_id: source_location_id, destination_location_id: destination_location_id} =
movement} ->
notify_pubsub(
{:ok, movement},
:created,
"movement",
[
"location:#{source_location_id}",
"location:#{destination_location_id}",
"item:#{item_id}",
"item_group:#{item_group_id}",
"event:#{event_id}"
],
%{event_id: event_id}
)
other ->
other
end
end
def create_movement_directly(attrs \\ %{}) do
%Movement{}
|> change_movement(attrs)
|> Repo.insert()
|> case do
{:ok,
%{
source_location_id: source_location_id,
destination_location_id: destination_location_id,
item_id: item_id
} = movement} ->
%{event: %{id: event_id}, item_group: %{id: item_group_id}} =
Repo.preload(movement, [:item_group, :event])
notify_pubsub(
{:ok, movement},
:created,
"movement",
[
"location:#{source_location_id}",
"location:#{destination_location_id}",
"item:#{item_id}",
"item_group:#{item_group_id}",
"event:#{event_id}"
],
%{event_id: event_id}
)
other ->
other
end
end
@doc """
Updates a movement.
## Examples
iex> update_movement(movement, %{field: new_value})
{:ok, %Movement{}}
iex> update_movement(movement, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_movement(%Movement{item_id: item_id} = movement, attrs) do
%{event: %{id: event_id}, item_group: %{id: item_group_id}} =
Repo.preload(movement, [:item_group, :event])
movement
|> change_movement(attrs)
|> Repo.update()
|> case do
{:ok,
%{source_location_id: source_location_id, destination_location_id: destination_location_id} =
movement} ->
notify_pubsub(
{:ok, movement},
:updated,
"movement",
[
"location:#{source_location_id}",
"location:#{destination_location_id}",
"item:#{item_id}",
"item_group:#{item_group_id}",
"event:#{event_id}"
],
%{event_id: event_id}
)
other ->
other
end
end
@doc """
Deletes a movement.
## Examples
iex> delete_movement(movement)
{:ok, %Movement{}}
iex> delete_movement(movement)
{:error, %Ecto.Changeset{}}
"""
def delete_movement(
%Movement{
item_id: item_id,
source_location_id: source_location_id,
destination_location_id: destination_location_id
} = movement
) do
%{event: %{id: event_id}, item_group: %{id: item_group_id}} =
Repo.preload(movement, [:item_group, :event])
movement
|> Repo.delete()
|> notify_pubsub(
:deleted,
"movement",
[
"location:#{source_location_id}",
"location:#{destination_location_id}",
"item:#{item_id}",
"item_group:#{item_group_id}",
"event:#{event_id}"
],
%{event_id: event_id}
)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking movement changes.
## Examples
iex> change_movement(movement)
%Ecto.Changeset{source: %Movement{}}
"""
def change_movement(%Movement{} = movement, attrs \\ %{}),
do: Movement.changeset(movement, attrs)
@spec stock_query :: Ecto.Queryable.t()
def stock_query do
from(
event in Event,
join: stock_entry in assoc(event, :stock_entries),
as: :stock_entry,
join: l in assoc(stock_entry, :location),
as: :location,
join: ig in assoc(stock_entry, :item_group),
as: :item_group,
join: i in assoc(stock_entry, :item),
as: :item,
where:
stock_entry.supply > 0 or stock_entry.consumption > 0 or stock_entry.movement_in > 0 or
stock_entry.movement_out > 0,
select: stock_entry,
order_by: l.name,
order_by: ig.name,
order_by: i.name
)
end
@spec logistics_table_query(event :: Event.t()) :: Ecto.Queryable.t()
def logistics_table_query(%Event{id: event_id}),
do: from(event in stock_query(), where: event.id == ^event_id)
@spec location_totals_by_location_query(query :: Ecto.Queryable.t(), location :: Location.t()) ::
Ecto.Queryable.t()
def location_totals_by_location_query(query \\ Location.Total, %Location{id: location_id}),
do: from(total in query, where: total.location_id == ^location_id)
@spec location_totals_by_item_query(query :: Ecto.Queryable.t(), item :: Item.t()) ::
Ecto.Queryable.t()
def location_totals_by_item_query(query \\ Location.Total, %Item{id: item_id}),
do: from(total in query, where: total.item_id == ^item_id)
@spec event_totals_by_event_query(query :: Ecto.Queryable.t(), event :: Event.t()) ::
Ecto.Queryable.t()
def event_totals_by_event_query(query \\ Event.Total, %Event{id: event_id}),
do: from(total in query, where: total.event_id == ^event_id)
@spec event_totals_by_item_query(query :: Ecto.Queryable.t(), item :: Item.t()) ::
Ecto.Queryable.t()
def event_totals_by_item_query(query \\ Event.Total, %Item{id: item_id}),
do: from(total in query, where: total.item_id == ^item_id)
end