diff --git a/CHANGELOG.md b/CHANGELOG.md index c6392e99..9bcbc503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ml). ## master ## testing +- [added] #303 Add export Lots. +- [added] #303 Add export relating lots with devices. +- [added] #303 To do possible add and remove one device in one lot transfer. ## [2.2.0 rc1] - 2022-06-07 - [added] #212 Server side render parser Workbench Snapshots. diff --git a/ereuse_devicehub/inventory/models.py b/ereuse_devicehub/inventory/models.py index 69414c10..45d25157 100644 --- a/ereuse_devicehub/inventory/models.py +++ b/ereuse_devicehub/inventory/models.py @@ -1,6 +1,7 @@ from uuid import uuid4 from citext import CIText +from flask import g from sqlalchemy import Column, Integer from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import backref, relationship @@ -43,6 +44,15 @@ class Transfer(Thing): return False + def type_transfer(self): + if self.user_from == g.user: + return 'Outgoing' + + if self.user_to == g.user: + return 'Incoming' + + return 'Temporary' + class DeliveryNote(Thing): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index b7a8298b..83b78343 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -33,6 +33,7 @@ from ereuse_devicehub.parser.models import SnapshotsLog from ereuse_devicehub.resources.action.models import Trade from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow +from ereuse_devicehub.resources.enums import SnapshotSoftware from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.tag.model import Tag @@ -476,6 +477,8 @@ class ExportsView(View): 'metrics': self.metrics, 'devices': self.devices_list, 'certificates': self.erasure, + 'lots': self.lots_export, + 'devices_lots': self.devices_lots_export, } if export_id not in export_ids: @@ -577,6 +580,111 @@ class ExportsView(View): } return flask.render_template('inventory/erasure.html', **params) + def lots_export(self): + data = StringIO() + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') + + cw.writerow( + [ + 'Lot Id', + 'Lot Name', + 'Lot Type', + 'Transfer Status', + 'Transfer Code', + 'Transfer Date', + 'Transfer Creation Date', + 'Transfer Update Date', + 'Transfer Description', + 'Devices Number', + 'Devices Snapshots', + 'Devices Placeholders', + 'Delivery Note Number', + 'Delivery Note Date', + 'Delivery Note Units', + 'Delivery Note Weight', + 'Receiver Note Number', + 'Receiver Note Date', + 'Receiver Note Units', + 'Receiver Note Weight', + ] + ) + + for lot in Lot.query.filter_by(owner=g.user): + delivery_note = lot.transfer and lot.transfer.delivery_note or '' + receiver_note = lot.transfer and lot.transfer.receiver_note or '' + wb_devs = 0 + placeholders = 0 + + for dev in lot.devices: + snapshots = [e for e in dev.actions if e.type == 'Snapshot'] + if not snapshots or snapshots[-1].software not in [ + SnapshotSoftware.Workbench + ]: + placeholders += 1 + elif snapshots[-1].software in [SnapshotSoftware.Workbench]: + wb_devs += 1 + + row = [ + lot.id, + lot.name, + lot.type_transfer(), + lot.transfer and (lot.transfer.closed and 'Closed' or 'Open') or '', + lot.transfer and lot.transfer.code or '', + lot.transfer and lot.transfer.date or '', + lot.transfer and lot.transfer.created or '', + lot.transfer and lot.transfer.updated or '', + lot.transfer and lot.transfer.description or '', + len(lot.devices), + wb_devs, + placeholders, + delivery_note and delivery_note.number or '', + delivery_note and delivery_note.date or '', + delivery_note and delivery_note.units or '', + delivery_note and delivery_note.weight or '', + receiver_note and receiver_note.number or '', + receiver_note and receiver_note.date or '', + receiver_note and receiver_note.units or '', + receiver_note and receiver_note.weight or '', + ] + cw.writerow(row) + + return self.response_csv(data, "lots_export.csv") + + def devices_lots_export(self): + data = StringIO() + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') + head = [ + 'DHID', + 'Lot Id', + 'Lot Name', + 'Lot Type', + 'Transfer Status', + 'Transfer Code', + 'Transfer Date', + 'Transfer Creation Date', + 'Transfer Update Date', + ] + cw.writerow(head) + + for dev in self.find_devices(): + for lot in dev.lots: + row = [ + dev.devicehub_id, + lot.id, + lot.name, + lot.type_transfer(), + lot.transfer and (lot.transfer.closed and 'Closed' or 'Open') or '', + lot.transfer and lot.transfer.code or '', + lot.transfer and lot.transfer.date or '', + lot.transfer and lot.transfer.created or '', + lot.transfer and lot.transfer.updated or '', + ] + cw.writerow(row) + + return self.response_csv( + data, "Devices_Incoming_and_Outgoing_Lots_Spreadsheet.csv" + ) + class SnapshotListView(GenericMixin): template_name = 'inventory/snapshots_list.html' diff --git a/ereuse_devicehub/resources/lot/models.py b/ereuse_devicehub/resources/lot/models.py index ef6577a8..c2309224 100644 --- a/ereuse_devicehub/resources/lot/models.py +++ b/ereuse_devicehub/resources/lot/models.py @@ -151,6 +151,16 @@ class Lot(Thing): """Gets the lots that are not under any other lot.""" return cls.query.join(cls.paths).filter(db.func.nlevel(Path.path) == 1) + def type_transfer(self): + # Used in reports lots_export.csv + if not self.transfer: + return 'Temporary' + if self.transfer.user_from == g.user: + return 'Outgoing' + if self.transfer.user_to == g.user: + return 'Incoming' + return '' + def add_children(self, *children): """Add children lots to this lot. diff --git a/ereuse_devicehub/resources/lot/views.py b/ereuse_devicehub/resources/lot/views.py index 117d3209..0f0752ab 100644 --- a/ereuse_devicehub/resources/lot/views.py +++ b/ereuse_devicehub/resources/lot/views.py @@ -111,7 +111,7 @@ class LotView(View): query = query.filter(Lot.name.ilike(args['search'] + '%')) lots = query.paginate(per_page=6 if args['search'] else query.count()) return things_response( - self.schema.dump(lots.items, many=True, nested=2), + self.get_lots_dump(lots), lots.page, lots.per_page, lots.total, @@ -120,6 +120,17 @@ class LotView(View): ) return jsonify(ret) + def get_lots_dump(self, lots): + lots_dump = self.schema.dump(lots.items, many=True, nested=2) + for lot in lots.items: + if not lot.transfer: + continue + for _lot in lots_dump: + if _lot['id'] == str(lot.id): + _lot['transfer'] = lot.type_transfer() + break + return lots_dump + def visibility_filter(self, query): query = ( query.outerjoin(Trade) @@ -141,7 +152,9 @@ class LotView(View): # temporary if lot_type == "temporary": - return query.filter(Lot.trade == None).filter(Lot.transfer == None) + return query.filter(Lot.trade == None).filter( + or_(Lot.transfer == None, Transfer.date == None) + ) if lot_type == "incoming": return query.filter( diff --git a/ereuse_devicehub/static/js/main_inventory.build.js b/ereuse_devicehub/static/js/main_inventory.build.js index 16bb720a..1e1b21fd 100644 --- a/ereuse_devicehub/static/js/main_inventory.build.js +++ b/ereuse_devicehub/static/js/main_inventory.build.js @@ -681,17 +681,32 @@ async function processSelectedDevices() { return lot; }); - let lotsList = []; - lotsList.push(lots.filter(lot => lot.state == "true").sort((a, b) => a.name.localeCompare(b.name))); - lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a, b) => a.name.localeCompare(b.name))); - lotsList.push(lots.filter(lot => lot.state == "false").sort((a, b) => a.name.localeCompare(b.name))); - lotsList = lotsList.flat(); // flat array listHTML.html(""); - lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions)); + const lot_temporary = lots.filter(lot => !lot.transfer); + appendMenu(lot_temporary, listHTML, templateLot, selectedDevices, actions, "Temporary"); + + const lot_incoming = lots.filter(lot => lot.transfer && lot.transfer == "Incoming"); + appendMenu(lot_incoming, listHTML, templateLot, selectedDevices, actions, "Incoming"); + + const lot_outgoing = lots.filter(lot => lot.transfer && lot.transfer == "Outgoing"); + appendMenu(lot_outgoing, listHTML, templateLot, selectedDevices, actions, "Outgoing"); + lotsSearcher.enable(); + } catch (error) { console.log(error); listHTML.html("
  • Error feching devices and lots
    (see console for more details)
  • "); } } + +function appendMenu(lots, listHTML, templateLot, selectedDevices, actions, title) { + let lotsList = []; + lotsList.push(lots.filter(lot => lot.state == "true").sort((a, b) => a.name.localeCompare(b.name))); + lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a, b) => a.name.localeCompare(b.name))); + lotsList.push(lots.filter(lot => lot.state == "false").sort((a, b) => a.name.localeCompare(b.name))); + lotsList = lotsList.flat(); // flat array + + listHTML.append(`
  • ${ title }
  • `); + lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions)); +} diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js index fd693d44..9c9b8d0a 100644 --- a/ereuse_devicehub/static/js/main_inventory.js +++ b/ereuse_devicehub/static/js/main_inventory.js @@ -646,17 +646,30 @@ async function processSelectedDevices() { return lot; }) - let lotsList = []; - lotsList.push(lots.filter(lot => lot.state == "true").sort((a, b) => a.name.localeCompare(b.name))); - lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a, b) => a.name.localeCompare(b.name))); - lotsList.push(lots.filter(lot => lot.state == "false").sort((a, b) => a.name.localeCompare(b.name))); - lotsList = lotsList.flat(); // flat array - listHTML.html(""); - lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions)); + const lot_temporary = lots.filter(lot => !lot.transfer); + appendMenu(lot_temporary, listHTML, templateLot, selectedDevices, actions, "Temporary"); + + const lot_incoming = lots.filter(lot => lot.transfer && lot.transfer == "Incoming"); + appendMenu(lot_incoming, listHTML, templateLot, selectedDevices, actions, "Incoming"); + + const lot_outgoing = lots.filter(lot => lot.transfer && lot.transfer == "Outgoing"); + appendMenu(lot_outgoing, listHTML, templateLot, selectedDevices, actions, "Outgoing"); + lotsSearcher.enable(); } catch (error) { console.log(error); listHTML.html("
  • Error feching devices and lots
    (see console for more details)
  • "); } } + +function appendMenu(lots, listHTML, templateLot, selectedDevices, actions, title) { + let lotsList = []; + lotsList.push(lots.filter(lot => lot.state == "true").sort((a, b) => a.name.localeCompare(b.name))); + lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a, b) => a.name.localeCompare(b.name))); + lotsList.push(lots.filter(lot => lot.state == "false").sort((a, b) => a.name.localeCompare(b.name))); + lotsList = lotsList.flat(); // flat array + + listHTML.append(`
  • ${ title }
  • `); + lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions)); +} diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index f86e31ae..658c2345 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -44,6 +44,29 @@ + +
  • - + - Metrics Spreadsheet + Devices Incoming and Outgoing Lots Spreadsheet
  • diff --git a/tests/files/devices_lots.csv b/tests/files/devices_lots.csv new file mode 100644 index 00000000..5de2cc9f --- /dev/null +++ b/tests/files/devices_lots.csv @@ -0,0 +1,2 @@ +DHID;Lot Id;Lot Name;Lot Type;Transfer Status;Transfer Code;Transfer Date;Transfer Creation Date;Transfer Update Date +O48N2;c43a0d06-0c77-4a74-9c95-086645fbc534;lot1;Temporary;;;;; diff --git a/tests/files/lots.csv b/tests/files/lots.csv new file mode 100644 index 00000000..9c2d7c5b --- /dev/null +++ b/tests/files/lots.csv @@ -0,0 +1,2 @@ +Lot Id;Lot Name;Lot Type;Transfer Status;Transfer Code;Transfer Date;Transfer Creation Date;Transfer Update Date;Transfer Description;Devices Number;Devices Snapshots;Devices Placeholders;Delivery Note Number;Delivery Note Date;Delivery Note Units;Delivery Note Weight;Receiver Note Number;Receiver Note Date;Receiver Note Units;Receiver Note Weight +cca691c4-b221-4882-924c-30cd545c0182;lot1;Temporary;;;;;;;1;1;0;;;;;;;; diff --git a/tests/test_render_2_0.py b/tests/test_render_2_0.py index 90ae4b4c..94aa9d7f 100644 --- a/tests/test_render_2_0.py +++ b/tests/test_render_2_0.py @@ -3,16 +3,20 @@ import datetime import json from io import BytesIO from pathlib import Path +from uuid import UUID import pytest +from flask import g from flask.testing import FlaskClient from flask_wtf.csrf import generate_csrf from ereuse_devicehub.client import UserClient, UserClientFlask +from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.action.models import Snapshot from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.resources.user.models import User from tests import conftest @@ -1309,3 +1313,75 @@ def test_edit_notes_with_closed_transfer(user3: UserClientFlask): body, status = user3.post(uri, data=data) assert status == '200 OK' assert 'Receiver Note updated error!' in body + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_export_devices_lots(user3: UserClientFlask): + snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json') + user3.get('/inventory/lot/add/') + lot_name = 'lot1' + data = { + 'name': lot_name, + 'csrf_token': generate_csrf(), + } + user3.post('/inventory/lot/add/', data=data) + lot = Lot.query.filter_by(name=lot_name).one() + + device = snap.device + g.user = User.query.one() + device.lots.update({lot}) + db.session.commit() + + uri = "/inventory/export/devices_lots/?ids={id}".format(id=snap.device.devicehub_id) + + body, status = user3.get(uri) + assert status == '200 OK' + + export_csv = [line.split(";") for line in body.split("\n")] + + with Path(__file__).parent.joinpath('files').joinpath( + 'devices_lots.csv' + ).open() as csv_file: + obj_csv = csv.reader(csv_file, delimiter=';', quotechar='"') + fixture_csv = list(obj_csv) + + assert fixture_csv[0] == export_csv[0], 'Headers are not equal' + assert fixture_csv[1][2:] == export_csv[1][2:], 'Computer information are not equal' + UUID(export_csv[1][1]) + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_export_lots(user3: UserClientFlask): + snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json') + user3.get('/inventory/lot/add/') + lot_name = 'lot1' + data = { + 'name': lot_name, + 'csrf_token': generate_csrf(), + } + user3.post('/inventory/lot/add/', data=data) + lot = Lot.query.filter_by(name=lot_name).one() + + device = snap.device + g.user = User.query.one() + device.lots.update({lot}) + db.session.commit() + + uri = "/inventory/export/lots/" + + body, status = user3.get(uri) + assert status == '200 OK' + + export_csv = [line.split(";") for line in body.split("\n")] + + with Path(__file__).parent.joinpath('files').joinpath( + 'lots.csv' + ).open() as csv_file: + obj_csv = csv.reader(csv_file, delimiter=';', quotechar='"') + fixture_csv = list(obj_csv) + + assert fixture_csv[0] == export_csv[0], 'Headers are not equal' + assert fixture_csv[1][1:] == export_csv[1][1:], 'Computer information are not equal' + UUID(export_csv[1][0])