From 07bee12d035d9cc9e5c2f0dd2e8b87a38c177479 Mon Sep 17 00:00:00 2001 From: Theron Boerner Date: Sun, 2 Mar 2025 22:24:56 -0500 Subject: [PATCH] Test for moving up POs --- lib/scheduler/scheduler_agent.ex | 76 +++-- lib/therons_erp/inventory/movement.ex | 8 +- lib/therons_erp/inventory/product.ex | 2 + lib/therons_erp/sales/sales_order.ex | 8 +- lib/therons_erp/scheduler.ex | 135 +++++--- ...302162514_add_sales_order_to_movements.exs | 29 ++ ...64422_add_purchase_date_to_sales_order.exs | 21 ++ .../repo/movements/20250302162514.json | 321 ++++++++++++++++++ .../repo/sales_orders/20250302164422.json | 147 ++++++++ test/support/generator.ex | 3 +- test/therons_erp/scheduling_test.exs | 96 ++++-- 11 files changed, 758 insertions(+), 88 deletions(-) create mode 100644 priv/repo/migrations/20250302162514_add_sales_order_to_movements.exs create mode 100644 priv/repo/migrations/20250302164422_add_purchase_date_to_sales_order.exs create mode 100644 priv/resource_snapshots/repo/movements/20250302162514.json create mode 100644 priv/resource_snapshots/repo/sales_orders/20250302164422.json diff --git a/lib/scheduler/scheduler_agent.ex b/lib/scheduler/scheduler_agent.ex index 5061511..f2eaa49 100644 --- a/lib/scheduler/scheduler_agent.ex +++ b/lib/scheduler/scheduler_agent.ex @@ -20,6 +20,10 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do GenServer.call(pid, {:add_to_peer, peer}) end + def accelerate(pid) do + GenServer.call(pid, :accelerate) + end + def set_product_inventory(pid, {_product, _amount} = inv) do GenServer.call(pid, {:set_product_inventory, inv}) end @@ -36,12 +40,12 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do GenServer.call(pid, {:add_movement, movement}) end - def propagate_push_route(pid, quantity, product_id) do - GenServer.call(pid, {:propagate_push_route, quantity, product_id}) + def propagate_push_route(pid, quantity, product_id, purchase_order) do + GenServer.call(pid, {:propagate_push_route, quantity, product_id, purchase_order}) end - def propagate_pull_route(pid, quantity, product_id, from_location) do - GenServer.call(pid, {:propagate_pull_route, quantity, product_id, from_location}) + def propagate_pull_route(pid, quantity, product_id, from_location, sales_line) do + GenServer.call(pid, {:propagate_pull_route, quantity, product_id, from_location, sales_line}) end def add_purchase_order(pid, purchase_order) do @@ -184,9 +188,10 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do {:reply, :ok, Map.put(state, :stale_movements, [movement | state.stale_movements])} end - def handle_call({:propagate_push_route, quantity, product_id}, _from, state) + def handle_call({:propagate_push_route, quantity, product_id, purchase_order}, _from, state) when not is_nil(quantity) do # TODO modify a stale movement if possible. + # TODO pass in the purchase order so we can set the date peer = get_to_peer_for_product(state, product_id) if peer do @@ -196,10 +201,12 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do quantity: quantity, product_id: product_id, from_inventory_id: state.inventory_id, - to_inventory_id: location_id + to_inventory_id: location_id, + date: purchase_order.delivery_date, + purchase_order_id: purchase_order.id } - propagate_push_route(pid, quantity, product_id) + propagate_push_route(pid, quantity, product_id, purchase_order) {:reply, :ok, add_movements(state, movement)} else @@ -207,7 +214,11 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do end end - def handle_call({:propagate_pull_route, quantity, product_id, from_location}, from, state) do + def handle_call( + {:propagate_pull_route, quantity, product_id, from_location, sales_line}, + from, + state + ) do peer = get_from_peer_for_product(state, product_id) movement = @@ -216,7 +227,8 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do product_id: product_id, to_inventory_id: from_location, from_inventory_id: state.location_id, - date: nil + date: nil, + sales_order_id: sales_line.sales_order_id } inv_amt = get_total_inventory_amount(state, product_id) @@ -229,7 +241,8 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do pid, Decimal.sub(quantity, inv_amt.amount), product_id, - state.location_id + state.location_id, + sales_line ) {:reply, {:ok, date}, @@ -283,11 +296,16 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do end if !peer && lt?(inv_amt, quantity) do - {:reply, {:ok, Date.add(MyDate.today(), product.replenishment.lead_time_days)}, - add_movements(state, movement) |> sub_inv(inv_amt, product_id)} + new_date = Date.add(MyDate.today(), product.replenishment.lead_time_days) + + {:reply, {:ok, new_date}, + add_movements(state, %{movement | date: new_date}) |> sub_inv(inv_amt, product_id)} else - {:reply, {:ok, Date.add(MyDate.today(), product.replenishment.lead_time_days)}, - add_movements(state, movement) |> sub_inv(Money.new(quantity, :XIT), product_id)} + new_date = Date.add(MyDate.today(), product.replenishment.lead_time_days) + + {:reply, {:ok, new_date}, + add_movements(state, %{movement | date: new_date}) + |> sub_inv(Money.new(quantity, :XIT), product_id)} end end end @@ -344,6 +362,11 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do {:reply, changesets, state} end + def handle_call(:accelerate, _from, state) do + {sales_lines, purchase_orders, stale_movements} = _accelerate(state) + {:reply, {sales_lines, purchase_orders, stale_movements}, state} + end + def handle_call(:process, _from, state) do # Move purchase orders through push for po <- state.purchase_orders do @@ -377,7 +400,13 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do {{_peer_location_id, _product_id}, pid} = peer {:ok, date} = - propagate_pull_route(pid, sales_line.quantity, sales_line.product_id, state.location_id) + propagate_pull_route( + pid, + sales_line.quantity, + sales_line.product_id, + state.location_id, + sales_line + ) [ %{ @@ -385,16 +414,21 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do product_id: sales_line.product_id, from_inventory_id: state.location_id, to_inventory_id: location_id, - date: date + date: date, + sales_order_id: sales_line.sales_order_id } ] else + date = get_inv_amount_day(state, sales_line.product_id, sales_line.quantity) + [ %{ quantity: sales_line.quantity, product_id: sales_line.product_id, to_inventory_id: location_id, - from_inventory_id: state.location_id + from_inventory_id: state.location_id, + date: date, + sales_order_id: sales_line.sales_order_id } ] end @@ -504,7 +538,7 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do if peer do {{location_id, _product_id}, pid} = peer - propagate_push_route(pid, item.quantity, item.product_id) + propagate_push_route(pid, item.quantity, item.product_id, purchase_order) [ %{ @@ -574,6 +608,10 @@ defmodule TheronsErp.Scheduler.SchedulerAgent do date end - def accelerate_purchase_order(movements, purchase_orders, quantity, product_id) do + def _accelerate(state) do + sales_lines = state.sales_lines + purchase_orders = state.purchase_orders + stale_movements = state.stale_movements + {sales_lines, purchase_orders, stale_movements} end end diff --git a/lib/therons_erp/inventory/movement.ex b/lib/therons_erp/inventory/movement.ex index 4355eb1..bb0f095 100644 --- a/lib/therons_erp/inventory/movement.ex +++ b/lib/therons_erp/inventory/movement.ex @@ -43,7 +43,8 @@ defmodule TheronsErp.Inventory.Movement do :actual_transfer_id, :predicted_transfer_id, :eager_transfer_id, - :purchase_order_id + :purchase_order_id, + :sales_order_id ] change fn changeset, _ -> @@ -66,7 +67,8 @@ defmodule TheronsErp.Inventory.Movement do :actual_transfer_id, :predicted_transfer_id, :eager_transfer_id, - :purchase_order_id + :purchase_order_id, + :sales_order_id ] end @@ -173,6 +175,8 @@ defmodule TheronsErp.Inventory.Movement do end belongs_to :purchase_order, TheronsErp.Purchasing.PurchaseOrder + + belongs_to :sales_order, TheronsErp.Sales.SalesOrder end def get_acct_id(identifier) do diff --git a/lib/therons_erp/inventory/product.ex b/lib/therons_erp/inventory/product.ex index ff6f012..c6f68c1 100644 --- a/lib/therons_erp/inventory/product.ex +++ b/lib/therons_erp/inventory/product.ex @@ -92,6 +92,8 @@ defmodule TheronsErp.Inventory.Product do relationships do belongs_to :category, TheronsErp.Inventory.ProductCategory + # This is a many to many to support having a pull and a push route. This + # should not be used with multiple pulls or multiple pushes. many_to_many :routes, TheronsErp.Inventory.Routes do through TheronsErp.Inventory.ProductRoutes source_attribute :id diff --git a/lib/therons_erp/sales/sales_order.ex b/lib/therons_erp/sales/sales_order.ex index 3818a35..0eca57c 100644 --- a/lib/therons_erp/sales/sales_order.ex +++ b/lib/therons_erp/sales/sales_order.ex @@ -94,7 +94,7 @@ defmodule TheronsErp.Sales.SalesOrder do end create :create do - accept [:customer_id, :address_id, :process_date] + accept [:customer_id, :address_id, :process_date, :purchase_date] primary? true argument :sales_lines, {:array, :map} @@ -105,7 +105,7 @@ defmodule TheronsErp.Sales.SalesOrder do end update :update do - accept [:customer_id, :address_id, :process_date] + accept [:customer_id, :address_id, :process_date, :purchase_date] require_atomic? false argument :sales_lines, {:array, :map} @@ -128,6 +128,10 @@ defmodule TheronsErp.Sales.SalesOrder do attribute :process_date, :date + attribute :purchase_date, :date do + default &MyDate.today/0 + end + timestamps() end diff --git a/lib/therons_erp/scheduler.ex b/lib/therons_erp/scheduler.ex index 2b855bd..0967b72 100644 --- a/lib/therons_erp/scheduler.ex +++ b/lib/therons_erp/scheduler.ex @@ -41,44 +41,7 @@ defmodule TheronsErp.Scheduler do # Initialize each location with its actual product inventory - for product <- products do - for location <- locations do - id = - TheronsErp.Inventory.Movement.get_inv_identifier( - :actual, - location.id, - product.identifier - ) - - TheronsErp.Inventory.Movement.get_acct_id(id) - - balance = - TheronsErp.Ledger.Account - |> Ash.get!(%{identifier: id}, - load: :balance_as_of - ) - |> Map.get(:balance_as_of) - - SchedulerAgent.set_product_inventory(location_map[location.id], {product, balance}) - end - - routes = product.routes - - for route <- routes do - if route do - for r <- route.routes do - to = location_map[r.to_location_id] - from = location_map[r.from_location_id] - - if route.type == :push do - SchedulerAgent.add_to_peer(from, {r.to_location_id, product.id, to}) - else - SchedulerAgent.add_peer(to, {r.from_location_id, product.id, from}) - end - end - end - end - end + init_products(location_map, locations, products) for movement <- movements do SchedulerAgent.add_movement(location_map[movement.to_location_id], movement) @@ -115,8 +78,52 @@ defmodule TheronsErp.Scheduler do SchedulerAgent.generate_accounts(agent) end - change_list = + # Process accelerated POs + + items = for {_loc, agent} <- location_map do + {sales_lines2, purchase_orders2, stale_movements} = SchedulerAgent.accelerate(agent) + end + + sales_lines2 = Enum.map(items, fn {sales_lines, _, _} -> sales_lines end) |> List.flatten() + + purchase_orders2 = + Enum.map(items, fn {_, purchase_orders, _} -> purchase_orders end) |> List.flatten() + + stale_movements = + Enum.map(items, fn {_, _, stale_movements} -> stale_movements end) |> List.flatten() + + location_map2 = + for location <- locations, into: %{} do + {:ok, pid} = SchedulerAgent.start_link(location_id: location.id) + Ecto.Adapters.SQL.Sandbox.allow(TheronsErp.Repo, self(), pid) + + {location.id, pid} + end + + init_products(location_map2, locations, products) + + for movement <- stale_movements do + SchedulerAgent.add_movement(location_map2[movement.to_location_id], movement) + end + + for purchase_order <- purchase_orders2 do + SchedulerAgent.add_purchase_order( + location_map2[purchase_order.destination_location_id], + purchase_order + ) + end + + for {_loc, agent} <- location_map2 do + SchedulerAgent.process(agent) + end + + for sales_line <- sales_lines2 do + SchedulerAgent.add_sales_line(location_map2[sales_line.pull_location_id], sales_line) + end + + change_list = + for {_loc, agent} <- location_map2 do SchedulerAgent.persist(agent) end |> List.flatten() @@ -135,5 +142,55 @@ defmodule TheronsErp.Scheduler do end end end) + + # Shut down agents + for {_loc, agent} <- location_map do + SchedulerAgent.stop(agent) + end + + for {_loc, agent} <- location_map2 do + SchedulerAgent.stop(agent) + end + end + + defp init_products(location_map, locations, products) do + for product <- products do + for location <- locations do + id = + TheronsErp.Inventory.Movement.get_inv_identifier( + :actual, + location.id, + product.identifier + ) + + TheronsErp.Inventory.Movement.get_acct_id(id) + + balance = + TheronsErp.Ledger.Account + |> Ash.get!(%{identifier: id}, + load: :balance_as_of + ) + |> Map.get(:balance_as_of) + + SchedulerAgent.set_product_inventory(location_map[location.id], {product, balance}) + end + + routes = product.routes + + for route <- routes do + if route do + for r <- route.routes do + to = location_map[r.to_location_id] + from = location_map[r.from_location_id] + + if route.type == :push do + SchedulerAgent.add_to_peer(from, {r.to_location_id, product.id, to}) + else + SchedulerAgent.add_peer(to, {r.from_location_id, product.id, from}) + end + end + end + end + end end end diff --git a/priv/repo/migrations/20250302162514_add_sales_order_to_movements.exs b/priv/repo/migrations/20250302162514_add_sales_order_to_movements.exs new file mode 100644 index 0000000..dd9e837 --- /dev/null +++ b/priv/repo/migrations/20250302162514_add_sales_order_to_movements.exs @@ -0,0 +1,29 @@ +defmodule TheronsErp.Repo.Migrations.AddSalesOrderToMovements 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(:movements) do + add :sales_order_id, + references(:sales_orders, + column: :id, + name: "movements_sales_order_id_fkey", + type: :uuid, + prefix: "public" + ) + end + end + + def down do + drop constraint(:movements, "movements_sales_order_id_fkey") + + alter table(:movements) do + remove :sales_order_id + end + end +end diff --git a/priv/repo/migrations/20250302164422_add_purchase_date_to_sales_order.exs b/priv/repo/migrations/20250302164422_add_purchase_date_to_sales_order.exs new file mode 100644 index 0000000..b94d145 --- /dev/null +++ b/priv/repo/migrations/20250302164422_add_purchase_date_to_sales_order.exs @@ -0,0 +1,21 @@ +defmodule TheronsErp.Repo.Migrations.AddPurchaseDateToSalesOrder 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 :purchase_date, :date + end + end + + def down do + alter table(:sales_orders) do + remove :purchase_date + end + end +end diff --git a/priv/resource_snapshots/repo/movements/20250302162514.json b/priv/resource_snapshots/repo/movements/20250302162514.json new file mode 100644 index 0000000..ee481c4 --- /dev/null +++ b/priv/resource_snapshots/repo/movements/20250302162514.json @@ -0,0 +1,321 @@ +{ + "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": "quantity", + "type": "decimal" + }, + { + "allow_nil?": true, + "default": "false", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "manually_created", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "date", + "type": "date" + }, + { + "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": "movements_product_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "products" + }, + "size": null, + "source": "product_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": "movements_actual_transfer_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "ledger_transfers" + }, + "size": null, + "source": "actual_transfer_id", + "type": "binary" + }, + { + "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": "movements_predicted_transfer_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "ledger_transfers" + }, + "size": null, + "source": "predicted_transfer_id", + "type": "binary" + }, + { + "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": "movements_eager_transfer_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "ledger_transfers" + }, + "size": null, + "source": "eager_transfer_id", + "type": "binary" + }, + { + "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": "movements_from_location_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "locations" + }, + "size": null, + "source": "from_location_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": "movements_to_location_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "locations" + }, + "size": null, + "source": "to_location_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": "movements_purchase_order_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "purchase_orders" + }, + "size": null, + "source": "purchase_order_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": "movements_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": "\"ready\"", + "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": "5142C96F471002047931FEA4E3459B3B70CF4B8BF4E01B3FB21DC54A901B3673", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.TheronsErp.Repo", + "schema": null, + "table": "movements" +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/sales_orders/20250302164422.json b/priv/resource_snapshots/repo/sales_orders/20250302164422.json new file mode 100644 index 0000000..2d3719e --- /dev/null +++ b/priv/resource_snapshots/repo/sales_orders/20250302164422.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?": 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": "process_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "purchase_date", + "type": "date" + }, + { + "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": "107BEABAA3E5DC3143EDCF438DA9BE13272B96F0E03AE69848DB7897E14506FA", + "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/support/generator.ex b/test/support/generator.ex index 5c8f8eb..c99b8ac 100644 --- a/test/support/generator.ex +++ b/test/support/generator.ex @@ -128,7 +128,8 @@ defmodule TheronsErp.Generator do defaults: [ customer_id: generate(customer()).id, address_id: nil, - state: :draft + state: :draft, + purchase_date: nil ], overrides: opts ) diff --git a/test/therons_erp/scheduling_test.exs b/test/therons_erp/scheduling_test.exs index f925ab1..4cb99cf 100644 --- a/test/therons_erp/scheduling_test.exs +++ b/test/therons_erp/scheduling_test.exs @@ -368,31 +368,6 @@ defmodule TheronsErp.SchedulingTest do test "mtso fallback" - test "accelerate purchase order" do - product = - generate(product()) - - vendor = generate(vendor()) - - replenishment = - generate( - replenishment( - product_id: product.id, - vendor_id: vendor.id, - trigger_quantity: 0, - quantity_multiple: 5 - ) - ) - - product = Ash.load(product, [:replenishment]) - - loc_a = generate(location()) - loc_b = generate(location()) - - # SchedulerAgent.accelerate_purchase_order(movements, purchase_orders, 1, product_id) - # TODO not done - end - test "Move up purchase order" do vendor = generate(vendor()) product = generate(product()) @@ -480,6 +455,77 @@ defmodule TheronsErp.SchedulingTest do ) end + def pull_up_pos(pos, quantity, lead_time, by_date, multiple) do + {_, poses} = + Enum.reduce(pos, {0, []}, fn one_pos, {acc, new_poses} -> + if acc >= quantity do + {acc, [one_pos | new_poses]} + else + if acc + one_pos.quantity >= quantity do + first_qty = quantity - acc + + new_pos1 = %{ + date: floor_date(Date.add(by_date, -lead_time)), + quantity: multiple * ceil(first_qty / multiple) + } + + new_pos2 = %{ + date: one_pos.date, + quantity: one_pos.quantity - multiple * ceil(first_qty / multiple) + } + + if new_pos2.quantity > 0 do + {acc + one_pos.quantity, [new_pos1, new_pos2 | new_poses]} + else + {acc + one_pos.quantity, [new_pos1 | new_poses]} + end + else + date = floor_date(Date.add(by_date, -lead_time)) + + date = + if Date.before?(one_pos.date, date) do + one_pos.date + else + date + end + + {acc + one_pos.quantity, + [ + %{date: date, quantity: one_pos.quantity} + | new_poses + ]} + end + end + end) + + poses + end + + defp floor_date(date) do + if Date.after?(MyDate.today(), date) do + MyDate.today() + else + date + end + end + + test "pulling POs to fill order" do + multiple = 5 + + pos = [ + %{date: MyDate.today(), quantity: 5}, + %{date: Date.add(MyDate.today(), 10), quantity: 10} + ] + + pos = pull_up_pos(pos, 6, 2, Date.add(MyDate.today(), 3), multiple) + + assert Enum.sort_by(pos, & &1.date, Date) == [ + %{date: MyDate.today(), quantity: 5}, + %{date: Date.add(MyDate.today(), 1), quantity: 5}, + %{date: Date.add(MyDate.today(), 10), quantity: 5} + ] + end + test "inventory amount day" do state = SchedulerAgent.increase_inv(nil, %{id: 1}, 1, %{inventory: %{}}, MyDate.today()) state = SchedulerAgent.increase_inv(nil, %{id: 1}, 3, state, Date.add(MyDate.today(), 14))