Solidus Brazilian Adaptations

CircleCI codecov

Essa gem tem como objetivo adaptar os modelos do Solidus para se adequar aos padrões brasileiros, tais como a criação dos atributos CPF/CNPJ (tax_id) em Spree::Order, e número (number) do imóvel e bairro (district) em Spree::Address.

Installation

Add solidus_brazilian_adaptations to your Gemfile:

gem 'solidus_brazilian_adaptations'

Bundle your dependencies and run the installation generator:

bin/rails generate solidus_brazilian_adaptations:install

Usage

Tradução e moeda padrão

Editar config/initializers/spree.rb adicionando as seguintes configurações:

Mudar moeda e país padrão:

Spree.config do |config|
  # ...

  # Define BRL como moeda padrão
  config.currency = "BRL"
  # Preenche automaticamente o campo de pais que vamos remover do formulário de endereço:
  config.default_country_iso = "BR"

  # ...
end

Mudar idioma da interface do admin:

Spree::Backend::Config.configure do |config|
  # ...

  config.locale = 'pt-BR'

  # ...
end

Incluir no config/application.rb para definir os locales disponiveis:

module SuaLoja
  class Application < Rails::Application
    # ...

    config.i18n.available_locales = ['pt-BR']
    config.i18n.default_locale = 'pt-BR'

    # ...
  end
end

Seeds

Substituir Spree::Core::Engine.load_seed por SolidusBrazilianAdaptations::Engine.load_seed em db/seeds.rb para utilizar o seeds da gem.

Configurações

A partir do initializer da gem é possivel configurar se será permitido efetuar compras utilizando CNPJ. Por padrão é essa opção é true.

Storefront

Essa gem utiliza o starter frontend como base, então os exemplos serão feitos a partir de suas views.

Para adicionar os campos de CPF/CNPJ, número e bairro é possivel editar o form da etapa de endereço. Porém a edição é feita em duas partials diferentes:

Na partial app/views/checkouts/_checkout_step.html.erb o seguinte HTML referente ao CPF/CNPJ pode ser inserido logo abaixo ao input de e-mail:


<%= form_for order, url: update_checkout_path(order.state), html: { id: "checkout_form_#{order.state}" } do |form| %>
  <% if order.state == "address" || !order.email? %>
    <div class="text-input">
      <%= form.label :email, 'E-Mail:' %>
      <%= form.email_field :email, required: true, placeholder: '[email protected]' %>
    </div>

    <!-- Adicionar -->
    <% label = SolidusBrazilianAdaptations.config.allow_cnpj ? "CPF/CPNJ" : "CPF" %>
    <div class="text-input" style="margin-top: 4px;">
      <%= form.label :tax_id, "#{label}:" %>
      <%= form.text_field :tax_id, required: true, placeholder: label %>
    </div>
    <!-- ********* -->

  <% end %>

  <%= render "checkouts/steps/#{order.state}_step", form: form, differentiator: @differentiator %>
<% end %>

E na partial app/views/checkouts/steps/address_step/_address_inputs.html.erb adicionar o seguinte código referente ao número e bairro, podendo ser alterada a ordem dos campos:

  <!-- Adicionar -->
  <div class="text-input">
    <%= form.label :number, "Número:" %>
    <%= form.text_field :number, required: true %>
  </div>
  <!-- ********* -->

  <div class="text-input">
    <%= form.label :address2, "#{I18n.t("spree.street_address_2")}:" %>
    <%= form.text_field :address2, autocomplete: "#{address_type} address-line2" %>
  </div>

  <!-- Adicionar -->
  <div class="text-input">
    <%= form.label :district, "Bairro:" %>
    <%= form.text_field :district, required: true %>
  </div>
  <!-- ********* -->

  <div class="text-input">
    <%= form.label :city, "#{I18n.t("spree.city")}:" %>
    <%= form.text_field :city, required: true, autocomplete: "#{address_type} address-level2" %>
  </div>

Admin

Para visualizar os campos de CPF/CNPJ, número e bairro no painel do Admin na aba Order/Order_Number/Customer Details é necessário criar as seguintes views em seu projeto:

Essas novas views irão sobrescrever as padrões, localizadas na gem solidus_backend. Portanto, você deve ficar atento às atualizações do Solidus, pois ao sobrescrever as views as atualizações não serão aplicadas.

app/views/spree/admin/orders/customer_details/_form.html.erb

<fieldset data-hook="admin_customer_detail_form_fields" class="no-border-top">

  <fieldset class="index no-border-bottom" data-hook="customer_guest">
    <legend align="center"><%= t('spree.account') %></legend>

    <div data-hook="customer_fields" class="row">
      <div class="col-9">
        <div class="field">
          <%= f.label :email %>
          <%= f.email_field :email, required: true, class: 'fullwidth' %>
        </div>

        <!-- Adicionado -->
        <div class="field">
          <%= f.label :tax_id, "CPF/CNPJ" %>
          <%= f.text_field :tax_id, required: true, class: 'fullwidth' %>
        </div>
        <!-- ********* -->
      </div>
      <div class="col-3">
        <div class="field">
          <%= label_tag nil, t('spree.guest_checkout') %>
          <ul>
            <% if @order.completed? %>
              <li>
                <%= @order.user.nil? ? t('spree.say_yes') : t('spree.say_no') %>
              </li>
            <% else %>
              <% guest = @order.user.nil? %>
              <li>
                <label>
                  <%= radio_button_tag :guest_checkout, true, guest %>
                  <%= t('spree.say_yes') %>
                </label>
              </li>
              <li>
                <label>
                  <%= radio_button_tag :guest_checkout, false, !guest, disabled: @order.cart? %>
                  <%= t('spree.say_no') %>
                </label>
              </li>
              <%= hidden_field_tag :user_id, @order.user_id %>
            <% end %>
          </ul>
        </div>
      </div>
    </div>
  </fieldset>

  <div class="row">
    <% if Spree::Config[:order_bill_address_used] %>
      <div class="col-6" data-hook="bill_address_wrapper">
        <fieldset class="no-border-bottom">
          <legend align="center"><%= t('spree.billing_address') %></legend>
          <div class="js-billing-address">
            <%= f.fields_for :bill_address do |ba_form| %>
              <%= render partial: 'spree/admin/shared/address_form', locals: { f: ba_form, type: "billing" } %>
            <% end %>
          </div>
        </fieldset>
      </div>
    <% end %>

    <div class="col-6" data-hook="ship_address_wrapper">
      <fieldset class="no-border-bottom">
        <legend align="center"><%= t('spree.shipping_address') %></legend>
        <% if Spree::Config[:order_bill_address_used] %>
          <div class="field">
            <span data-hook="use_billing">
              <label>
                <%= check_box_tag 'order[use_billing]', '1', (@order.ship_address.new_record? && @order.bill_address == @order.ship_address) %>
                <%= t('spree.use_billing_address') %>
              </label>
            </span>
          </div>
        <% end %>

        <div class="js-shipping-address">
          <%= f.fields_for :ship_address do |ba_form| %>
            <%= render partial: 'spree/admin/shared/address_form', locals: { f: ba_form, type: "shipping" } %>
          <% end %>
        </div>
      </fieldset>
    </div>
  </div>

  <div class="clear"></div>

  <div class="form-buttons filter-actions actions" data-hook="buttons">
    <%= button_tag t('spree.actions.update'), class: 'btn btn-primary' %>
  </div>
</fieldset>

app/views/spree/admin/shared/_address_form.html.erb

<% s_or_b = type.chars.first %>

<div id="<%= type %>" data-hook="address_fields">
  <div class="field <%= "#{type}-row" %>">
    <%= f.label :name %>
    <%= f.text_field :name, class: 'fullwidth' %>
  </div>

  <% if Spree::Config[:company] %>
    <div class="field <%= "#{type}-row" %>">
      <%= f.label :company %>
      <%= f.text_field :company, class: 'fullwidth' %>
    </div>
  <% end %>

  <div class="field <%= "#{type}-row" %>">
    <%= f.label :address1 %>
    <%= f.text_field :address1, class: 'fullwidth' %>
  </div>

  <!-- Adicionado -->
  <div class="field <%= "#{type}-row" %>">
    <%= f.label :number, "Número" %>
    <%= f.text_field :number, class: 'fullwidth' %>
  </div>
  <!-- ********** -->

  <div class="field <%= "#{type}-row" %>">
    <%= f.label :address2 %>
    <%= f.text_field :address2, class: 'fullwidth' %>
  </div>

  <!-- Adicionado -->
  <div class="field <%= "#{type}-row" %>">
    <%= f.label :district, "Bairro" %>
    <%= f.text_field :district, class: 'fullwidth' %>
  </div>
  <!-- ********** -->

  <div class="field <%= "#{type}-row" %>">
    <%= f.label :city %>
    <%= f.text_field :city, class: 'fullwidth' %>
  </div>

  <div class="field <%= "#{type}-row" %>">
    <%= f.label :zipcode %>
    <%= f.text_field :zipcode, class: 'fullwidth' %>
  </div>

  <div class="field <%= "#{type}-row" %>">
    <%= f.label :country_id, Spree::Country.model_name.human %>
    <span id="<%= s_or_b %>country">
      <%= f.collection_select :country_id, available_countries, :id, :name, {}, {class: 'custom-select fullwidth js-country_id'} %>
    </span>
  </div>

  <div class="field <%= "#{type}-row" %>">
    <%= f.label :state_id, Spree::State.model_name.human %>
    <span id="<%= s_or_b %>state">
      <%= f.hidden_field :state_name, value: nil %>
      <% states = f.object.country.try(:states).nil? ? [] : f.object.country.states %>
      <%= f.text_field :state_name,
            style: "display: #{states.empty? ? 'block' : 'none' };",
            disabled: !states.empty?, class: 'fullwidth state_name js-state_name' %>
      <%= f.hidden_field :state_id, value: nil %>
      <%= f.collection_select :state_id,
            states.sort,
            :id, :name,
            { include_blank: true },
            { class: 'custom-select fullwidth js-state_id',
              style: "display: #{states.empty? ? 'none' : 'block' };",
              disabled: states.empty? } %>
    </span>
  </div>

  <div class="field <%= "#{type}-row" %>">
    <%= f.label :phone %>
    <%= f.phone_field :phone, class: 'fullwidth' %>
  </div>
</div>

Development

Testing the extension

First bundle your dependencies, then run bin/rake. bin/rake will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using bin/rake extension:test_app.

bin/rake

To run Rubocop static code analysis run

bundle exec rubocop

When testing your application's integration with this extension you may use its factories. You can load Solidus core factories along with this extension's factories using this statement:

SolidusDevSupport::TestingSupport::Factories.load_for(SolidusBrazilianAdaptations::Engine)

Running the sandbox

To run this extension in a sandboxed Solidus application, you can run bin/sandbox. The path for the sandbox app is ./sandbox and bin/rails will forward any Rails commands to sandbox/bin/rails.

Here's an example:

$ bin/rails server
=> Booting Puma
=> Rails 6.0.2.1 application starting in development
* Listening on tcp://127.0.0.1:3000
Use Ctrl-C to stop

Releasing new versions

Please refer to the dedicated page in the Solidus wiki.

License

Copyright (c) 2023 ulysses-bull, released under the New BSD License.