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:
wandji 2023-09-07 12:12:27 +01:00 committed by GitHub
parent d2c0e3dca2
commit a59f94c5a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 302 additions and 32 deletions

View file

@ -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();

View file

@ -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;

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View 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

View file

@ -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>

View file

@ -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>

View file

@ -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 %>

View file

@ -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 } %>

View file

@ -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'

View file

@ -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'

View 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