Skip to content

Commit

Permalink
Continued with #19
Browse files Browse the repository at this point in the history
  • Loading branch information
lentschi committed Apr 14, 2023
1 parent 9337d08 commit 5e2c91b
Show file tree
Hide file tree
Showing 15 changed files with 171 additions and 81 deletions.
23 changes: 16 additions & 7 deletions app/assets/javascripts/article-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,16 @@ class ArticleForm {
}

initializeFormSubmitListener() {
(this.multiForm$ === undefined ? this.articleForm$ : this.multiForm$).submit(() => {
this.undoSequentialRatioDataRepresentation();
this.loadRatios();
this.undoPriceConversion();
this.undoOrderAndReceivedUnitsConversion();
(this.multiForm$ === undefined ? this.articleForm$ : this.multiForm$).submit((e) => {
try {
this.undoSequentialRatioDataRepresentation();
this.loadRatios();
this.undoPriceConversion();
this.undoOrderAndReceivedUnitsConversion();
} catch(err) {
e.preventDefault();
throw err;
}
});
}

Expand Down Expand Up @@ -477,7 +482,7 @@ class ArticleForm {
if (previousValue !== undefined) {
const td$ = currentField$.closest('td');
const name = currentField$.attr('name');
const ratioNameRegex = new RegExp(`${this.unitFieldsNamePrefix}\\[article_unit_ratios_attributes\\]\\[([0-9]+)\\]`);
const ratioNameRegex = new RegExp(`${escapeForRegex(this.unitFieldsNamePrefix)}\\[article_unit_ratios_attributes\\]\\[([0-9]+)\\]`);
const index = name.match(ratioNameRegex)[1];
quantity = quantity * previousValue;
currentField$ = $(`<input type="hidden" name="${this.ratioQuantityFieldNameByIndex(index)}" value="${quantity}" />`);
Expand All @@ -489,7 +494,7 @@ class ArticleForm {
}

ratioQuantityFieldNameByIndex(i) {
return `${this.unitFieldsIdPrefix}[article_unit_ratios_attributes][${i}][quantity]`;
return `${this.unitFieldsNamePrefix}[article_unit_ratios_attributes][${i}][quantity]`;
}
}

Expand All @@ -508,3 +513,7 @@ function round(num, precision) {
const factor = Math.pow(10, precision);
return Math.round((num + Number.EPSILON) * factor) / factor;
}

function escapeForRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
62 changes: 49 additions & 13 deletions app/controllers/articles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ class ArticlesController < ApplicationController
before_action :authenticate_article_meta, :find_supplier

before_action :load_article, only: [:edit, :update]
before_action :load_article_units, only: [:edit, :update, :new, :create, :parse_upload]
before_action :new_empty_article_ratio, only: [:edit, :update, :new, :create, :parse_upload]
before_action :load_article_units, only: [:edit, :update, :new, :create, :parse_upload, :sync]
before_action :new_empty_article_ratio, only: [:edit, :update, :new, :create, :parse_upload, :sync]

def index
if params['sort']
Expand All @@ -30,6 +30,24 @@ def index
return
end

# TODO: Move this to API (see discussion in https://github.com/foodcoopsat/foodsoft_hackathon/issues/20)
if request.format.json?
data = @articles.map do |article|
version_attributes = article.latest_article_version.attributes
version_attributes.delete_if { |key| key == 'id' || key.end_with?('_id') }

version_attributes['article_unit_ratios'] = article.latest_article_version.article_unit_ratios.map do |ratio|
ratio_attributes = ratio.attributes
ratio_attributes.delete_if { |key| key == 'id' || key.end_with?('_id') }
end

version_attributes
end

render json: data
return
end

@articles = @articles.where('articles.name LIKE ?', "%#{params[:query]}%") unless params[:query].nil?

@articles = @articles.page(params[:page]).per(@per_page)
Expand Down Expand Up @@ -164,27 +182,41 @@ def parse_upload
redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.parse_upload.notice')
end
@ignored_article_count = 0
# rescue => error
# redirect_to upload_supplier_articles_path(@supplier), :alert => I18n.t('errors.general_msg', :msg => error.message)
rescue => error
redirect_to upload_supplier_articles_path(@supplier), :alert => I18n.t('errors.general_msg', :msg => error.message)
end

# sync all articles with the external database
# renders a form with articles, which should be updated
def sync
# check if there is an shared_supplier
unless @supplier.shared_supplier
redirect_to supplier_articles_url(@supplier), :alert => I18n.t('articles.controller.sync.shared_alert', :supplier => @supplier.name)
end
# sync articles against external database
@updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_all
# unless @supplier.shared_supplier
# redirect_to supplier_articles_url(@supplier), :alert => I18n.t('articles.controller.sync.shared_alert', :supplier => @supplier.name)
# end
# # sync articles against external database
# @updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_all
# if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty?
# redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.sync.notice')
# end

additional_headers = {}
# TODO: This is just temporary - use proper API token or whatever instead:
additional_headers['Cookie'] = request.headers['Cookie']
additional_headers['Referer'] = request.headers['Referer']
additional_headers['Host'] = request.headers['Host']

@updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_from_remote(additional_headers: additional_headers)
if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty?
redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.sync.notice')
redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.parse_upload.notice')
end
@ignored_article_count = 0
# rescue => error
# redirect_to upload_supplier_articles_path(@supplier), :alert => I18n.t('errors.general_msg', :msg => error.message)
end

# Updates, deletes articles when upload or sync form is submitted
def update_synchronized
@outlisted_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:outlisted_articles]&.values&.map { |v| v[:id] } || [] })
@outlisted_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:outlisted_articles]&.values || [] })
@updated_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:articles]&.values&.map { |v| v[:id] } || [] })
@new_articles = (params[:new_articles]&.values || []).map do |a|
article = @supplier.articles.build
Expand All @@ -205,8 +237,12 @@ def update_synchronized
has_error = true
end
# Update articles
@updated_articles.each do |a|
a.assign_attributes(params[:articles][a.id.to_s])
@updated_articles.each_with_index do |a, index|
current_params = params[:articles][index.to_s]
current_params.delete(:id)

a.latest_article_version.article_unit_ratios.clear
a.latest_article_version.assign_attributes(current_params)
a.save
end or has_error = true
# Add new articles
Expand Down
10 changes: 5 additions & 5 deletions app/controllers/suppliers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ def new
end
end

def edit
@supplier = Supplier.find(params[:id])
end

def create
@supplier = Supplier.new(supplier_params)
@supplier.supplier_category ||= SupplierCategory.first
Expand All @@ -35,10 +39,6 @@ def create
end
end

def edit
@supplier = Supplier.find(params[:id])
end

def update
@supplier = Supplier.find(params[:id])
if @supplier.update_attributes(supplier_params)
Expand Down Expand Up @@ -71,6 +71,6 @@ def supplier_params
.require(:supplier)
.permit(:name, :address, :phone, :phone2, :fax, :email, :url, :contact_person, :customer_number,
:iban, :custom_fields, :delivery_days, :order_howto, :note, :supplier_category_id,
:shared_supplier_id, :min_order_quantity, :shared_sync_method)
:shared_supplier_id, :min_order_quantity, :shared_sync_method, :supplier_remote_source)
end
end
6 changes: 4 additions & 2 deletions app/helpers/articles_helper.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
module ArticlesHelper
# useful for highlighting attributes, when synchronizing articles
def highlight_new(unequal_attributes, attribute)
def highlight_new(unequal_attributes, attributes)
attributes = [attributes] unless attributes.is_a?(Array)
return unless unequal_attributes

unequal_attributes.has_key?(attribute) ? "background-color: yellow" : ""
intersection = (unequal_attributes.keys & attributes)
intersection.empty? ? "" : "background-color: yellow"
end

def row_classes(article)
Expand Down
2 changes: 1 addition & 1 deletion app/models/article.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def unequal_attributes(new_article, options = {})
:unit => [self.latest_article_version.unit, new_unit],
:supplier_order_unit => [self.latest_article_version.supplier_order_unit, new_article.supplier_order_unit],
:minimum_order_quantity => [self.latest_article_version.minimum_order_quantity, new_article.minimum_order_quantity],
:billing_unit => [self.latest_article_version.billing_unit, new_article.billing_unit],
:billing_unit => [self.latest_article_version.billing_unit || self.latest_article_version.supplier_order_unit, new_article.billing_unit || new_article.supplier_order_unit],
:group_order_granularity => [self.latest_article_version.group_order_granularity, new_article.group_order_granularity],
:group_order_unit => [self.latest_article_version.group_order_unit, new_article.group_order_unit],
:price => [self.latest_article_version.price.to_f.round(2), new_price.to_f.round(2)],
Expand Down
2 changes: 1 addition & 1 deletion app/models/article_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ArticleVersion < ApplicationRecord
# @return [Array<OrderArticle>] Order articles this price is associated with.
has_many :order_articles

has_many :article_unit_ratios, after_add: :on_article_unit_ratios_change, after_remove: :on_article_unit_ratios_change
has_many :article_unit_ratios, after_add: :on_article_unit_ratios_change, after_remove: :on_article_unit_ratios_change, dependent: :destroy

localize_input_of :price, :tax, :deposit

Expand Down
91 changes: 56 additions & 35 deletions app/models/supplier.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'net/http'

class Supplier < ApplicationRecord
include MarkAsDeletedWithName
include CustomFields
Expand Down Expand Up @@ -81,44 +83,24 @@ def sync_all
# @option options [Boolean] :outlist_absent Set to +true+ to remove articles not in spreadsheet.
# @option options [Boolean] :convert_units Omit or set to +true+ to keep current units, recomputing unit quantity and price.
def sync_from_file(file, options = {})
all_order_numbers = []
updated_article_pairs, outlisted_articles, new_articles = [], [], []
FoodsoftFile::parse file, options do |status, new_attrs, line|
article = articles.includes(:latest_article_version).undeleted.where(article_versions: { order_number: new_attrs[:order_number] }).first
new_attrs[:article_category] = ArticleCategory.find_match(new_attrs[:article_category])
new_attrs[:tax] ||= FoodsoftConfig[:tax_default]
new_attrs[:article_unit_ratios] = new_attrs[:article_unit_ratios].map { |ratio_hash| ArticleUnitRatio.new(ratio_hash) }
new_article = articles.build
new_article_version = new_article.article_versions.build(new_attrs)
new_article.article_versions << new_article_version
new_article.latest_article_version = new_article_version
data = FoodsoftFile::parse(file, options)
self.parse_import_data(data, options)
end

if status.nil?
if article.nil?
new_articles << new_article
else
unequal_attributes = article.unequal_attributes(new_article, options.slice(:convert_units))
unless unequal_attributes.empty?
article.latest_article_version.article_unit_ratios.target.clear unless unequal_attributes[:article_unit_ratios_attributes].nil?
article.latest_article_version.attributes = unequal_attributes
updated_article_pairs << [article, unequal_attributes]
end
end
elsif status == :outlisted && article.present?
outlisted_articles << article
def sync_from_remote(options = {})
url = URI(self.supplier_remote_source)
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Get.new(url)

# stop when there is a parsing error
elsif status.is_a? String
# @todo move I18n key to model
raise I18n.t('articles.model.error_parse', :msg => status, :line => line.to_s)
end
# TODO: This is just temporary - use proper API token or whatever instead:
request["Cookie"] = options[:additional_headers]["Cookie"]
request["Referer"] = options[:additional_headers]["Referer"]
request["Host"] = options[:additional_headers]["Host"]

all_order_numbers << article.order_number if article
end
if options[:outlist_absent]
outlisted_articles += articles.undeleted.where.not(order_number: all_order_numbers + [nil])
end
return [updated_article_pairs, outlisted_articles, new_articles]
response = http.request(request)
data = JSON.parse(response.body, symbolize_names: true)

self.parse_import_data(data, options)
end

# default value
Expand Down Expand Up @@ -163,4 +145,43 @@ def uniqueness_of_name
errors.add :name, message
end
end

def parse_import_data(data, options = {})
all_order_numbers = []
updated_article_pairs, outlisted_articles, new_articles = [], [], []

data.each do |new_attrs|
article = articles.includes(:latest_article_version).undeleted.where(article_versions: { order_number: new_attrs[:order_number] }).first
new_attrs[:article_category] = ArticleCategory.find_match(new_attrs[:article_category])
new_attrs[:tax] ||= FoodsoftConfig[:tax_default]
new_attrs[:article_unit_ratios] = new_attrs[:article_unit_ratios].map { |ratio_hash| ArticleUnitRatio.new(ratio_hash) }
new_article = articles.build
new_article_version = new_article.article_versions.build(new_attrs)
new_article.article_versions << new_article_version
new_article.latest_article_version = new_article_version

if new_attrs[:availability]
if article.nil?
new_articles << new_article
else
unequal_attributes = article.unequal_attributes(new_article, options.slice(:convert_units))
unless unequal_attributes.empty?
article.latest_article_version.article_unit_ratios.target.clear unless unequal_attributes[:article_unit_ratios_attributes].nil?
article.latest_article_version.attributes = unequal_attributes
duped_ratios = article.latest_article_version.article_unit_ratios.map(&:dup)
article.latest_article_version.article_unit_ratios.target.clear
article.latest_article_version.article_unit_ratios.target.push(*duped_ratios)
updated_article_pairs << [article, unequal_attributes]
end
end
elsif article.present?
outlisted_articles << article
end
all_order_numbers << article.order_number if article
end
if options[:outlist_absent]
outlisted_articles += articles.includes(:latest_article_version).undeleted.where.not(article_versions: { order_number: all_order_numbers + [nil] })
end
return [updated_article_pairs, outlisted_articles, new_articles]
end
end
4 changes: 2 additions & 2 deletions app/views/articles/_sync.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
%p
= t('.outlist.body').html_safe
%ul
- for article in @outlisted_articles
- @outlisted_articles.each_with_index do |article, index|
%li
= hidden_field_tag "outlisted_articles[#{article.id}]", '1'
= hidden_field_tag "outlisted_articles[#{index}]", article.id
= article.name
- if article.in_open_order
.alert= t '.outlist.alert_used', article: article.name
Expand Down
13 changes: 6 additions & 7 deletions app/views/articles/_sync_table.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
%th= heading_helper Article, :deposit
%th= heading_helper Article, :article_category
%tbody
- index = 0
- articles.each do |changed_article, attrs|
- index += 1
- articles.each_with_index do |data, index|
- changed_article, attrs = data
- unless changed_article.new_record?
- article = Article.find(changed_article.id)
%tr{:style => 'color:grey'}
Expand Down Expand Up @@ -47,8 +46,8 @@
.d-flex.gap-1
= form.input :supplier_order_unit, as: :select, collection: @article_units, label: false, value: changed_article.supplier_order_unit, include_blank: t('Custom (avoid if possible!)') + ':', input_html: {class: 'input-medium'}
= form.input :unit, input_html: {class: 'input-mini ml-1'}, label: false
%btn.btn-link.toggle-extra-units
%i.icon-cog
%div.btn-link.toggle-extra-units
%i.icon-cog{:style => highlight_new(attrs, [:article_unit_ratio_attributes, :minimum_order_quantity, :billing_unit, :group_order_unit, :group_order_granularity])}
%div.extra-unit-fields.form-horizontal
.fold-line
.control-group
Expand All @@ -57,7 +56,7 @@
%table#fc_base_price{:class => "controls"}
%tbody
- ratios = changed_article.article_unit_ratios
= render :partial => 'shared/article_unit_ratio', :as => 'article_unit_ratio', :collection => ratios, locals: {f: form, changes: attrs[:article_unit_ratios_attributes]}
= render :partial => 'shared/article_unit_ratio', :as => 'article_unit_ratio', :collection => ratios, locals: {f: form, original_ratios: article&.article_unit_ratios}
%tfoot
%tr
%td{:colspan => 6}
Expand All @@ -79,7 +78,7 @@
- unless changed_article.new_record?
%p.help-block{style: 'color: grey;'}=article.minimum_order_quantity.to_s
.fold-line
= form.input :billing_unit, hint: changed_article.new_record? ? nil : ArticleUnits.get_translated_name_for_code(article.billing_unit), hint_html: {style: 'color: grey;'}, as: :select, collection: [], input_html: {'data-initial-value': changed_article.billing_unit, class: 'input-medium', style: highlight_new(attrs, :billing_unit)}, include_blank: false
= form.input :billing_unit, hint: changed_article.new_record? ? nil : ArticleUnits.get_translated_name_for_code(article.billing_unit || article.supplier_order_unit), hint_html: {style: 'color: grey;'}, as: :select, collection: [], input_html: {'data-initial-value': changed_article.billing_unit, class: 'input-medium', style: highlight_new(attrs, :billing_unit)}, include_blank: false
.fold-line
= form.input :group_order_granularity, hint: changed_article.new_record? ? nil : "#{article.group_order_granularity} x #{ArticleUnits.get_translated_name_for_code(article.group_order_unit)}", hint_html: {style: 'color: grey;'}, label: "Allow orders per", input_html: {class: 'input-mini', style: highlight_new(attrs, :group_order_granularity), title: "steps in which ordergroups can order this article"}
= form.input :group_order_unit, as: :select, collection: [], input_html: {'data-initial-value': changed_article.group_order_unit, class: 'input-medium', style: highlight_new(attrs, :group_order_unit)}, label: '&times;'.html_safe, include_blank: false
Expand Down
4 changes: 2 additions & 2 deletions app/views/articles/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
= text_field_tag :query, params[:query], class: 'input-medium search-query',
placeholder: t('.search_placeholder')

- if @supplier.shared_supplier
- unless @supplier.supplier_remote_source.blank?
.btn-group
- if @supplier.shared_sync_method == 'import'
= link_to t('.ext_db.import'), "#import", 'data-toggle-this' => '#import', class: 'btn btn-primary'
Expand All @@ -30,7 +30,7 @@
- if current_user.role_orders?
= link_to t('.new_order'), new_order_path(supplier_id: @supplier), class: 'btn'

- unless @supplier.shared_supplier.nil?
- unless @supplier.supplier_remote_source.blank?
#import.well.well-small(style="display:none;")
= form_tag shared_supplier_articles_path(@supplier), method: :get, remote: true, class: 'form-search',
'data-submit-onchange' => true do
Expand Down
Loading

0 comments on commit 5e2c91b

Please sign in to comment.