Insert Many-Many at same time as parent

Hi,

I’m trying to post a list of (existing) products to a new order in Phoenix using Ecto. However, I am encountering the following issue:

INSERT INTO "receiving_orders_products" ("quantity","receiving_order_id") VALUES ($1,$2) [15, 39]
[debug] QUERY OK db=0.0ms
rollback []
[error] GenServer #PID<0.10237.0> terminating
** (Postgrex.Error) ERROR 23502 (not_null_violation) null value in column "product_id" violates not-null constraint

    table: receiving_orders_products
    column: product_id

The relationship is a Many-Many however I have configured it as a has_many due to the fact I needed an additional field and read that was the recommended way to handle it.

Below is my schema:
Order:

  @required_fields ~w(supplier_name address_id)a
  @optional_fields ~w(purchase_order_number type)a

  schema "receiving_orders" do
    field :supplier_name, :string
    field :purchase_order_number, :string
    field :type, :string
    field :delivery_date, :utc_datetime
    belongs_to :address, Rapport.Erp.Address
    has_many :products, Rapport.Domain.ReceivingOrderProduct
    timestamps()
  end

  def changeset(receiving_order, attrs) do
    receiving_order
    |> cast(attrs, @required_fields ++ @optional_fields)
    |> cast_assoc(:products)
    |> validate_required(@required_fields)
    |> assoc_constraint(:address)
  end

Order Product:

  @required_fields ~w(quantity product_id receiving_order_id)a
  @primary_key false

  schema "receiving_orders_products" do
    field :quantity, :integer
    belongs_to(:receiving_order, Rapport.Domain.ReceivingOrder)
    belongs_to(:product, Rapport.Erp.Product)
  end

  def changeset(purchase_order, attrs) do
    purchase_order
    |> cast(attrs, @required_fields)
    |> validate_required(@required_fields)
  end

This is how I am presenting it in the form:

    <%= inputs_for f, :products, fn p -> %>
      <%= inputs_for p, :product, fn product -> %>
        <%= input product, :part_number, [disabled: true] %>
        <%= input p, :quantity %>
        <%= input product, :description, [disabled: true] %>
      <% end %>
      <hr />
    <% end %>

I am then trying to save it here:

  def create(attrs) do
    IO.inspect(attrs)
    %ReceivingOrder{}
    |> ReceivingOrder.changeset(attrs)
    |> IO.inspect()
    |> Repo.insert()
  end

And this is what both inspects looks like:

%{
  "address" => %{"id" => "1"},
  "address_id" => "1",
  "products" => %{
    "0" => %{"product" => %{"id" => "7"}, "quantity" => "15"},
    "1" => %{"product" => %{"id" => "1"}, "quantity" => "10"}
  },
  "purchase_order_number" => "ORD03834",
  "supplier_name" => "Some Supplier"
}
#Ecto.Changeset<
  action: nil,
  changes: %{
    address_id: 1,
    delivery_date: ~U[2020-08-19 20:25:43Z],
    products: [
      #Ecto.Changeset<
        action: :insert,
        changes: %{quantity: 15},
        errors: [],
        data: #Rapport.Domain.ReceivingOrderProduct<>,
        valid?: true
      >,
      #Ecto.Changeset<
        action: :insert,
        changes: %{quantity: 10},
        errors: [],
        data: #Rapport.Domain.ReceivingOrderProduct<>,
        valid?: true
      >
    ],
    purchase_order_number: "ORD03834",
    supplier_name: "Some Supplier",
    type: "Planned"
  },
  errors: [],
  data: #Rapport.Domain.ReceivingOrder<>,
  valid?: true
>

Any help appreciated. Thanks.

There’s no code in ReceivingOrderProduct.changeset to tell Ecto how to turn product: %{id: 7} into anything - either use cast_assoc or switch to passing product_id explicitly instead.

Hi,

Thanks for the reply. I’ve added a hidden input for the product id and am now passing that, however I get a new error stating the receiving_order_id can’t be blank:

This is what my products look like now:

    "0" => %{
      "product" => %{"id" => "7"},
      "product_id" => "7",
      "quantity" => "15"
    },

And this is what the Ecto Changeset looks like:

 products: [
      #Ecto.Changeset<
        action: :insert,
        changes: %{product_id: 7, quantity: 15},
        errors: [
          receiving_order_id: {"can't be blank", [validation: :required]}
        ],
        data: #Rapport.Domain.ReceivingOrderProduct<>,
        valid?: false
      >,

I removed receiving_order_id from the @required_fields and that seems to work now! Thanks for your help!