How can I infer values from a form field and copy them to other (embedded) fields?

Hello there,
I’m new to Elixir and I’m currently having trouble with:

I need to infer values from a form’s fields and put those values into a changeset. What makes it harder is that I’m using embedded schemas.

Let’s say I have an “Invoice Item” entity. The invoice item has country specific fields according to its emission country.

My schema ‘invoice_item’ is as follows (simplified):

  schema "purchase_invoice_items" do
    field(:quantity, :integer)
    field(:unit_price, Money.Ecto.Type)

    embeds_one(
      :purchase_invoice_item_country_fields,
      PurchaseInvoiceItemCountryFields,
      on_replace: :update
    )

    belongs_to(:invoice, Invoice)
  end

In country fields, there are different fields according to countries’ specific tax system.

In my invoice_item changeset, I have changesets for updating and creation. These changesets call for specific country changesets to cast changes for the embedded entity country_fields:

  def create_changeset(attrs, %Invoice{} = invoice, %Org{} = org) do
    %InvoiceItem{}
    |> cast(attrs, [:quantity, :unit_price, :provider_product_id])
    |> cast_creation_country_fields(invoice.country, attrs, org)
    |> put_change(:invoice_id, invoice.id)
    |> validate_required([
      :quantity,
      :unit_price,
      :invoice_id,
      :provider_product_id,
      :purchase_invoice_item_country_fields
    ])
    |> validate_number(:quantity, greater_than: 0)
    |> validate_invoice_status(invoice, "pending")
  end 

  @doc false
  def update_changeset(%InvoiceItem{} = invoice_item, %Org{} = org, attrs) do
    invoice_item
    |> cast(attrs, [:quantity, :unit_price])
    |> cast_update_country_fields(
      Purchase.get_invoice!(invoice_item.invoice_id, org).country,
      attrs,
      org,
      invoice_item
    )
    |> validate_required([:quantity, :unit_price, :purchase_invoice_item_country_fields])
    |> validate_number(:quantity, greater_than: 0)
    |> validate_invoice_status("pending")
  end

Ok so, within my purchase_invoice_item_country_fields, I have my specific changeset for each country, where each country has its changeset for country specific field validation:

  def brazil_changeset(%PurchaseInvoiceItemCountryFields{} = fields, attrs) do
    |> cast(attrs, [
      :codigo_prod_sistema,
      :unidade,
      :quantidade,
      :valor_unitario
    ])
    |> validate_required([
      :codigo_prod_sistema,
      :unidade,
      :quantidade,
      :valor_unitario
    ])
  end

This changeset is called by cast_creation_country_fields and cast_update_country_fields on their upper changesets. I will show cast_creation_country_fields as an example of what I am trying to do when inferring some field values to others:

  defp cast_creation_country_fields(%Ecto.Changeset{} = changeset, country, attrs, %Org{} = org)
      when country == "Brazil" do
    # attrs["provider_product_id"] will usually be nil on new action,
    # but defined on create action
    float_unit_price = normalize_unit_price(attrs["unit_price"], nil)
    if attrs["provider_product_id"] && attrs["provider_product_id"] != "" do

      provider_product =
        Purchase.get_provider_product!(attrs["provider_product_id"], org, preload: [:product])

      changeset
      |> cast_embed(
        :purchase_invoice_item_country_fields,
        with: &PurchaseInvoiceItemCountryFields.brazil_changeset/2
      )
      |> put_embed(:purchase_invoice_item_country_fields, %{
            quantidade: attrs["quantity"],
            valor_unitario: float_unit_price,
            codigo_prod_sistema: provider_product.product.id,
            unidade: provider_product.product.measure_unit
                   })
    else
      changeset
      |> cast_embed(
        :purchase_invoice_item_country_fields,
        with: &PurchaseInvoiceItemCountryFields.brazil_changeset/2
      )
      |> put_embed(:purchase_invoice_item_country_fields, %{
          quantidade: attrs["quantity"],
          valor_unitario: float_unit_price,
               })
   end
 end

Problems I get when doing this include:

  • Sometimes attrs are not loaded with any relevant values, thus yielding blank country fields and invalid changesets.
  • Sometimes provider_product doesn’t seem to load properly (It will basically never load upon creation, only update, since a fresh invoice item won’t have any product tied to it) making the application rely on attrs and going back to the first problem)
  • **Invoice_item unit_price field uses Money type (**Ecto.Money.Type). It seems like Money Type doesn’t work with embedded schemas (somebody correct me if I’m wrong, since it would solve a big part of my problem). This problem forces me to use integer as a representation for fields like ‘valor_unitario’ (unit_price in Brazil’s tax system). Sometimes it doesn’t parse appropriately to integer generating status 500.

I know it’s a lot to process, but thanks in advance to anyone who has ideas or suggestions!

4 Likes

Something I can see just giving a look over is that you’re using both cast_embed and put_embed and one cancel the other actually, don’t know actually which one will prevail, but you shouldn’t be using both.

Now about the problems:

You mean in brazil_changeset? Well, here it is: for what I can presume just looking at your code, you’re probably not using inputs_for on your form right? Give it a check: https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#inputs_for/4

The problem is that when you call cast_embed, the attrs sent to the changeset function are not actually the same attrs you set on your “parent” chageset, it is the value of the key with the same name of your embed name, in your case "purchase_invoice_item_country_fields".

Here is a simple tutorial how to use embeds_one: https://robots.thoughtbot.com/embedding-elixir-structs-in-ecto-models. It’s old, but it still applies.

Well, you answered that for yourself. :slight_smile: It’s because you only receive the provider_product_id, you should manually load it if you need it.

Well, it should work. If it does not work, open an issue, or even better, open a PR for them.

1 Like

Oops, sorry the tutorial I intented to send is this: http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/

1 Like

Yeah I’m using cast_embed and put_embed because I intended to use the cast_embed to apply all changes from the form, and put_embed as a way to force those inferences I wanted to make (copying some fields’ values and relation values to embedded country fields values).

I am currently using input_for, but some fields (the inferred ones) are being shown as hidden_input, example:

<%= inputs_for @f, :purchase_invoice_item_country_fields, fn f -> %>
  <%= hidden_input f, :codigo_prod_sistema %>
  <%= hidden_input f, :unidade %>
  <%= hidden_input f, :quantidade %>
  <%= hidden_input f, :valor_unitario %>

  <div class="input-field">
    <%= text_input f, :cst,
      class: errors_class(f, :cst, "validate") %>
    <%= label f, gettext("Código de Substituição Tributária (CST)"),
      "data-error": errors_on(f, :cst) %>
  </div>
<% end %>

Oh, right, what about instead of using put_embed on the parent changeset, using put_change on the child changeset? I mean the embeds_one changeset?