mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-12-25 01:03:18 +08:00
Add RepositoryStockValueZipExport service and connect export consumption button [SCI-9023] (#6092)
* Add RepositoryStockValueZipExport service and connect export consumption button [SCI-9023] * Add consumption export modal and improve report generation [SCI-9023] * Add repository_row association to repository_ledger_records [SCI-9023]
This commit is contained in:
parent
d2c0e3dca2
commit
a59f94c5a9
14 changed files with 302 additions and 32 deletions
|
@ -6,7 +6,6 @@
|
|||
|
||||
//= require repositories/row_editor.js
|
||||
|
||||
|
||||
var RepositoryDatatable = (function(global) {
|
||||
'use strict';
|
||||
|
||||
|
@ -437,42 +436,51 @@ var RepositoryDatatable = (function(global) {
|
|||
});
|
||||
}
|
||||
|
||||
function initExportActions() {
|
||||
$(document).on('click', '#exportRepositoryRowsButton', function(e) {
|
||||
function exportActionCallback(exportType, formId) {
|
||||
const exportModal = $(`#export${exportType}Modal`);
|
||||
$(document).on('click', `#export${exportType}Button`, function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$('#exportRepositoryModal').modal('show');
|
||||
exportModal.modal('show');
|
||||
});
|
||||
|
||||
$('form#form-export').off().submit(function() {
|
||||
$(`form#${formId}`).off().submit(function() {
|
||||
var form = this;
|
||||
if (currentMode === 'viewMode') {
|
||||
// Remove all hidden fields
|
||||
$(form).find('input[name=row_ids\\[\\]]').remove();
|
||||
$(form).find('input[name=header_ids\\[\\]]').remove();
|
||||
|
||||
// Append visible column information
|
||||
$('table' + TABLE_ID + ' thead tr th').each(function() {
|
||||
var th = $(this);
|
||||
var val = prepareRepositoryHeaderForExport(th);
|
||||
// Append visible column information for repository export
|
||||
if (formId === 'form-repository-rows-export') {
|
||||
$('table' + TABLE_ID + ' thead tr th').each(function() {
|
||||
var th = $(this);
|
||||
var val = prepareRepositoryHeaderForExport(th);
|
||||
|
||||
if (val) {
|
||||
appendInput(form, val, 'header_ids[]');
|
||||
}
|
||||
});
|
||||
if (val) {
|
||||
appendInput(form, val, 'header_ids[]');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Append records
|
||||
$.each(rowsSelected, function(index, rowId) {
|
||||
appendInput(form, rowId, 'row_ids[]');
|
||||
});
|
||||
const exportRows = $('#exportStockConsumptionModal').attr('data-rows');
|
||||
|
||||
if (exportRows) {
|
||||
appendInput(form, JSON.parse(exportRows)[0], 'row_ids[]');
|
||||
} else {
|
||||
$.each(rowsSelected, function(index, rowId) {
|
||||
appendInput(form, rowId, 'row_ids[]');
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('ajax:beforeSend', function() {
|
||||
animateSpinner(null, true);
|
||||
})
|
||||
.on('ajax:complete', function() {
|
||||
$('#exportRepositoryModal').modal('hide');
|
||||
exportModal.modal('hide');
|
||||
animateSpinner(null, false);
|
||||
})
|
||||
.on('ajax:success', function(ev, data) {
|
||||
|
@ -483,6 +491,14 @@ var RepositoryDatatable = (function(global) {
|
|||
});
|
||||
}
|
||||
|
||||
function initExportActions() {
|
||||
// Stock Consumption Export Action
|
||||
exportActionCallback('StockConsumption', 'form-stock-consumption-export');
|
||||
|
||||
// RepositoryRow Export Action
|
||||
exportActionCallback('RepositoryRows', 'form-repository-rows-export');
|
||||
}
|
||||
|
||||
// Adjust columns width in table header
|
||||
function adjustTableHeader() {
|
||||
TABLE.columns.adjust();
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
});
|
||||
$('#modal-info-repository-row #bar-code-image').attr('src', barCodeCanvas.toDataURL('image/png'));
|
||||
|
||||
|
||||
$('#repository_row-info-table').DataTable({
|
||||
dom: 'RBltpi',
|
||||
stateSave: false,
|
||||
|
@ -59,6 +58,25 @@
|
|||
animateSpinner(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Stock Consumption Export Action
|
||||
$(document).on('click', '.export-consumption-button', function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
$('#modal-info-repository-row').modal('hide');
|
||||
// set and unset data-rows value so export knows to ignore selected rows or not
|
||||
$('#exportStockConsumptionModal')
|
||||
.on('show.bs.modal', function() {
|
||||
$('#exportStockConsumptionModal').attr(
|
||||
'data-rows',
|
||||
$('#modal-info-repository-row .print-label-button').attr('data-rows')
|
||||
);
|
||||
})
|
||||
.on('hide.bs.modal', function() {
|
||||
$('#exportStockConsumptionModal').attr('data-rows', null);
|
||||
})
|
||||
.modal('show');
|
||||
});
|
||||
});
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
|
|
@ -339,7 +339,7 @@ class RepositoriesController < ApplicationController
|
|||
if params[:row_ids] && params[:header_ids]
|
||||
RepositoryZipExport.generate_zip(params, @repository, current_user)
|
||||
log_activity(:export_inventory_items)
|
||||
render json: { message: t('zip_export.export_request_success') }, status: :ok
|
||||
render json: { message: t('zip_export.export_request_success') }
|
||||
else
|
||||
render json: { message: t('zip_export.export_error') }, status: :unprocessable_entity
|
||||
end
|
||||
|
@ -356,6 +356,16 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def export_repository_stock_items
|
||||
row_ids = @repository.repository_rows.where(id: params[:row_ids]).pluck(:id)
|
||||
if row_ids.any?
|
||||
RepositoryStockLedgerZipExport.generate_zip(row_ids, current_user.id)
|
||||
render json: { message: t('zip_export.export_request_success') }
|
||||
else
|
||||
render json: { message: t('zip_export.export_error') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def assigned_my_modules
|
||||
my_modules = MyModule.joins(:repository_rows).where(repository_rows: { repository: @repository })
|
||||
.readable_by_user(current_user).distinct
|
||||
|
|
|
@ -5,7 +5,7 @@ class RepositoryRowsController < ApplicationController
|
|||
include MyModulesHelper
|
||||
|
||||
MAX_PRINTABLE_ITEM_NAME_LENGTH = 64
|
||||
before_action :load_repository, except: %i(show print rows_to_print print_zpl
|
||||
before_action :load_repository, except: %i(print rows_to_print print_zpl
|
||||
validate_label_template_columns actions_toolbar)
|
||||
before_action :load_repository_row_print, only: %i(print rows_to_print print_zpl validate_label_template_columns)
|
||||
before_action :load_repository_or_snapshot, only: %i(print rows_to_print print_zpl validate_label_template_columns)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class RepositoryLedgerRecord < ApplicationRecord
|
||||
auto_strip_attributes :comment
|
||||
|
||||
belongs_to :repository_stock_value, optional: true
|
||||
belongs_to :repository_stock_value
|
||||
belongs_to :reference, polymorphic: true
|
||||
belongs_to :user
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ class RepositoryStockValue < ApplicationRecord
|
|||
belongs_to :created_by, class_name: 'User', optional: true, inverse_of: :created_repository_stock_values
|
||||
belongs_to :last_modified_by, class_name: 'User', optional: true, inverse_of: :modified_repository_stock_values
|
||||
has_one :repository_cell, as: :value, dependent: :destroy, inverse_of: :value
|
||||
has_one :repository_row, through: :repository_cell
|
||||
has_many :repository_ledger_records, dependent: :destroy
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
|
||||
|
|
109
app/services/repository_stock_ledger_zip_export.rb
Normal file
109
app/services/repository_stock_ledger_zip_export.rb
Normal file
|
@ -0,0 +1,109 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'csv'
|
||||
|
||||
module RepositoryStockLedgerZipExport
|
||||
COLUMNS = %w(
|
||||
consumption_type
|
||||
item_name
|
||||
item_id
|
||||
consumed_amount
|
||||
consumed_amount_unit
|
||||
added_amount
|
||||
added_amount_unit
|
||||
consumed_by
|
||||
consumed_on
|
||||
team
|
||||
project
|
||||
experiment
|
||||
task
|
||||
task_id
|
||||
stock_amount_balance
|
||||
stock_balance_unit
|
||||
).freeze
|
||||
|
||||
def self.generate_zip(row_ids, user_id)
|
||||
rows = generate_data(row_ids)
|
||||
|
||||
zip = ZipExport.create(user_id: user_id)
|
||||
zip.generate_exportable_zip(
|
||||
user_id,
|
||||
to_csv(rows),
|
||||
:repositories
|
||||
)
|
||||
end
|
||||
|
||||
def self.to_csv(rows)
|
||||
csv_header = COLUMNS.map { |col| I18n.t("repository_stock_values.stock_export.headers.#{col}") }
|
||||
|
||||
CSV.generate do |csv|
|
||||
csv << csv_header
|
||||
rows.each do |row|
|
||||
csv << row
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.generate_data(row_ids)
|
||||
data = []
|
||||
repository_ledger_records =
|
||||
RepositoryLedgerRecord.joins(repository_stock_value: :repository_row)
|
||||
.includes(:user, { repository_stock_value: :repository_row })
|
||||
.where(repository_row: { id: row_ids })
|
||||
.joins('LEFT OUTER JOIN my_module_repository_rows ON
|
||||
repository_ledger_records.reference_id = my_module_repository_rows.id')
|
||||
.joins('LEFT OUTER JOIN my_modules ON
|
||||
my_modules.id = my_module_repository_rows.my_module_id')
|
||||
.joins('LEFT OUTER JOIN experiments ON experiments.id = my_modules.experiment_id')
|
||||
.joins('LEFT OUTER JOIN projects ON projects.id = experiments.project_id')
|
||||
.joins('LEFT OUTER JOIN teams ON teams.id = projects.team_id')
|
||||
.order('repository_row.created_at, repository_ledger_records.created_at')
|
||||
.select('repository_ledger_records.*,
|
||||
my_modules.id AS module_id, my_modules.name AS module_name,
|
||||
projects.name AS project_name, teams.name AS team_name,
|
||||
experiments.name AS experiment_name')
|
||||
# rubocop:disable Metrics/BlockLength
|
||||
repository_ledger_records.each do |record|
|
||||
consumption_type = record.reference_type == 'MyModuleRepositoryRow' ? 'Task' : 'Inventory'
|
||||
|
||||
if record.amount.positive?
|
||||
added_amount = record.amount.to_d
|
||||
added_amount_unit = record.unit
|
||||
else
|
||||
consumed_amount = record.amount.abs.to_d
|
||||
consumed_amount_unit = record.unit
|
||||
end
|
||||
|
||||
breadcrumbs_data = Array.new(4, '')
|
||||
|
||||
row_data = [
|
||||
consumption_type,
|
||||
record.repository_stock_value.repository_row.name,
|
||||
record.repository_stock_value.repository_row.code,
|
||||
consumed_amount,
|
||||
consumed_amount_unit,
|
||||
added_amount,
|
||||
added_amount_unit,
|
||||
record.user.full_name,
|
||||
record.created_at.strftime(record.user.date_format),
|
||||
record.team_name,
|
||||
record.unit,
|
||||
record.balance.to_d
|
||||
]
|
||||
|
||||
if consumption_type == 'Task'
|
||||
breadcrumbs_data = [
|
||||
record.project_name,
|
||||
record.experiment_name,
|
||||
record.module_name,
|
||||
"#{MyModule::ID_PREFIX}#{record.module_id}"
|
||||
]
|
||||
end
|
||||
|
||||
row_data.insert(10, *breadcrumbs_data)
|
||||
data << row_data
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength
|
||||
data
|
||||
end
|
||||
end
|
|
@ -1,16 +1,16 @@
|
|||
<div class="modal fade"
|
||||
id="exportRepositoryModal"
|
||||
id="exportRepositoryRowsModal"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-labelledby="modal-export-repository-label">
|
||||
<%= form_with(url: export_repository_team_path(repository),
|
||||
html: { id: 'form-export' },
|
||||
html: { id: 'form-repository-rows-export' },
|
||||
data: { remote: true }) do |f| %>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title"><%=t 'zip_export.modal_label' %></h4>
|
||||
<h4 class="modal-title"><%=t 'zip_export.repositories_modal_label' %></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div><%=t('zip_export.repository_header_html', repository: repository.name) %></div>
|
||||
|
@ -18,8 +18,8 @@
|
|||
<div><%=t 'zip_export.repository_footer_html' %></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type='button' class='btn btn-secondary' data-dismiss='modal' id='close-modal-export-repositories'><%= t('general.cancel')%></button>
|
||||
<%= f.submit t('my_modules.repository.export'), id: "export-repositories", class: "btn btn-success" %>
|
||||
<button type='button' class='btn btn-secondary' data-dismiss='modal' id='close-modal-export-repository-rows'><%= t('general.cancel')%></button>
|
||||
<%= f.submit t('my_modules.repository.export'), id: "export-repository-rows", class: "btn btn-success" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,26 @@
|
|||
<div class="modal fade"
|
||||
id="exportStockConsumptionModal"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-labelledby="modal-export-stock-consumption-label">
|
||||
<%= form_with(url: export_repository_stock_items_team_path(repository),
|
||||
html: { id: 'form-stock-consumption-export' },
|
||||
data: { remote: true }) do |f| %>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title"><%=t 'zip_export.consumption_modal_label' %></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div><%=t('zip_export.consumption_header_html', repository: repository.name) %></div>
|
||||
<div><%=t 'zip_export.consumption_footer_html' %></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type='button' class='btn btn-secondary' data-dismiss='modal' id='close-modal-export-stock-consumption'><%= t('general.cancel')%></button>
|
||||
<%= f.submit t('zip_export.consumption_generate'), id: "export-stock-consumption", class: "btn btn-success" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
|
@ -53,7 +53,7 @@
|
|||
</div>
|
||||
<div class="col-md-4 bar-code-container">
|
||||
<canvas id="bar-code-canvas" class="hidden" data-id="<%= @repository_row.code %>"></canvas>
|
||||
<img id="bar-code-image"></img>
|
||||
<img id="bar-code-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -113,7 +113,10 @@
|
|||
</div>
|
||||
<div class="modal-footer" data-assign-item-button="<%= @my_module.present? %>">
|
||||
<% if can_export_repository_stock?(@repository_row.repository) %>
|
||||
<button type="button" class="btn btn-secondary export-consumption-button" data-rows="[<%= @repository_row.id %>]">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary export-consumption-button"
|
||||
>
|
||||
<%= t('repository_row.modal_info.export_consumption_label') %>
|
||||
</button>
|
||||
<% end %>
|
||||
|
|
|
@ -69,7 +69,9 @@
|
|||
|
||||
|
||||
<%= render partial: "repositories/delete_record_modal" %>
|
||||
<%= render partial: 'repositories/export_repository_modal',
|
||||
<%= render partial: 'repositories/export_repository_rows_modal',
|
||||
locals: { repository: @repository } %>
|
||||
<%= render partial: 'repositories/export_stock_consumption_modal',
|
||||
locals: { repository: @repository } %>
|
||||
|
||||
<%= render partial: 'repository_columns/manage_column_modal', locals: { my_module_page: false } %>
|
||||
|
|
|
@ -2226,7 +2226,24 @@ en:
|
|||
reminder_at: "Reminder at"
|
||||
units_remaining: "%{unit} remaining."
|
||||
enter_ammount: "Enter an amount"
|
||||
|
||||
stock_export:
|
||||
headers:
|
||||
consumption_type: 'Consumption type'
|
||||
item_name: 'Item name'
|
||||
item_id: 'Item ID'
|
||||
consumed_amount: 'Consumed amount'
|
||||
consumed_amount_unit: 'Consumed amount unit'
|
||||
added_amount: 'Added amount'
|
||||
added_amount_unit: 'Added amount unit'
|
||||
consumed_by: 'Consumed by'
|
||||
consumed_on: 'Consumed on'
|
||||
team: 'Team'
|
||||
project: 'Project'
|
||||
experiment: 'Experiment'
|
||||
task: 'Task'
|
||||
task_id: 'Task ID'
|
||||
stock_amount_balance: 'Stock amount after update (balance)'
|
||||
stock_balance_unit: 'Stock unit after update'
|
||||
libraries:
|
||||
manange_modal_column_index:
|
||||
title: "Manage columns"
|
||||
|
@ -3505,7 +3522,7 @@ en:
|
|||
title: "Manage access for %{resource_name}"
|
||||
description: "Changing a role will take away any permissions inherited from the project or the experiment. New permissions will apply only to this specific task."
|
||||
zip_export:
|
||||
modal_label: 'Export inventory'
|
||||
repositories_modal_label: 'Export inventory'
|
||||
notification_title: 'Your requested export package is ready!'
|
||||
expired_title: 'Looks like your link has expired.'
|
||||
expired_description: 'Please export the data again in order to receive a new link.'
|
||||
|
@ -3515,7 +3532,10 @@ en:
|
|||
repository_footer_html: 'Inventory will be exported in a .csv file format. You will receive <strong>email with a link</strong> where you can download it.'
|
||||
export_request_success: "Export request received. Your export request is being processed."
|
||||
export_error: "Error when creating zip export."
|
||||
|
||||
consumption_modal_label: 'Consumption report'
|
||||
consumption_header_html: 'You are about to generate consumption report for selected items in inventory %{repository}.'
|
||||
consumption_footer_html: 'Consumption report will be exported in a .csv file format. You will receive <strong>email with a link</strong> where you can download it.'
|
||||
consumption_generate: 'Generate'
|
||||
webhooks:
|
||||
index:
|
||||
title: 'Webhooks'
|
||||
|
|
|
@ -219,6 +219,7 @@ Rails.application.routes.draw do
|
|||
post 'parse_sheet', defaults: { format: 'json' }
|
||||
post 'export_repository', to: 'repositories#export_repository'
|
||||
post 'export_repositories', to: 'repositories#export_repositories'
|
||||
post 'export_repository_stock_items', to: 'repositories#export_repository_stock_items'
|
||||
post 'export_projects'
|
||||
get 'sidebar'
|
||||
get 'export_projects_modal'
|
||||
|
|
64
spec/services/repository_stock_ledger_zip_export_spec.rb
Normal file
64
spec/services/repository_stock_ledger_zip_export_spec.rb
Normal file
|
@ -0,0 +1,64 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'zip'
|
||||
|
||||
describe RepositoryStockLedgerZipExport, type: :background_job do
|
||||
let(:user) { create :user }
|
||||
let(:team) { create :team, created_by: user }
|
||||
let!(:owner_role) { UserRole.find_by(name: I18n.t('user_roles.predefined.owner')) }
|
||||
let!(:team_assignment) { create_user_assignment(team, owner_role, user) }
|
||||
let(:repository) { create :repository, team: team, created_by: user }
|
||||
|
||||
before do
|
||||
2.times do |index|
|
||||
repository_row = create(
|
||||
:repository_row,
|
||||
name: "row #{index}",
|
||||
repository: repository,
|
||||
created_by: user,
|
||||
last_modified_by: user)
|
||||
repository_row.repository_stock_value = build(:repository_stock_value)
|
||||
repository_stock_unit_item =
|
||||
create :repository_stock_unit_item,
|
||||
repository_column: repository_row.repository_stock_value.repository_cell.repository_column
|
||||
repository_row.repository_stock_value.repository_cell.repository_column.reload
|
||||
repository_row.save
|
||||
[100, 1500].each do |amount|
|
||||
repository_row.repository_stock_value.update_data!(
|
||||
{ amount: amount, low_stock_threshold: '', unit_item_id: repository_stock_unit_item.id }, user
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#generate_zip/2' do
|
||||
it 'generates a new zip export object' do
|
||||
params = RepositoryRow.pluck(:id)
|
||||
ZipExport.skip_callback(:create, :after, :self_destruct)
|
||||
described_class.generate_zip(params, user.id)
|
||||
expect(ZipExport.count).to eq 1
|
||||
ZipExport.set_callback(:create, :after, :self_destruct)
|
||||
end
|
||||
|
||||
it 'generates a zip with csv file with exported rows' do
|
||||
ZipExport.skip_callback(:create, :after, :self_destruct)
|
||||
params = RepositoryRow.pluck(:id)
|
||||
described_class.generate_zip(params, user.id)
|
||||
csv_zip_file = ZipExport.last.zip_file
|
||||
file_path = ActiveStorage::Blob.service.public_send(:path_for, csv_zip_file.key)
|
||||
parsed_csv_content = Zip::File.open(file_path) do |zip_file|
|
||||
csv_file = zip_file.glob('*.csv').last
|
||||
csv_content = csv_file.get_input_stream.read
|
||||
CSV.parse(csv_content, headers: true)
|
||||
end
|
||||
|
||||
expect(
|
||||
parsed_csv_content.to_a[0]
|
||||
).to eq described_class::COLUMNS.map{ |col| I18n.t("repository_stock_values.stock_export.headers.#{col}") }
|
||||
expect(parsed_csv_content.length).to eq RepositoryLedgerRecord.count
|
||||
|
||||
ZipExport.set_callback(:create, :after, :self_destruct)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue