Merge pull request #303 from eReuse/features/transfer-3493-3492-3510

Features/transfer 3493 3492 3510
This commit is contained in:
cayop 2022-06-22 09:40:56 +02:00 committed by GitHub
commit 97653058c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 292 additions and 17 deletions

View file

@ -8,6 +8,9 @@ ml).
## master ## master
## testing ## 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 ## [2.2.0 rc1] - 2022-06-07
- [added] #212 Server side render parser Workbench Snapshots. - [added] #212 Server side render parser Workbench Snapshots.

View file

@ -1,6 +1,7 @@
from uuid import uuid4 from uuid import uuid4
from citext import CIText from citext import CIText
from flask import g
from sqlalchemy import Column, Integer from sqlalchemy import Column, Integer
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import backref, relationship from sqlalchemy.orm import backref, relationship
@ -43,6 +44,15 @@ class Transfer(Thing):
return False 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): class DeliveryNote(Thing):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)

View file

@ -33,6 +33,7 @@ from ereuse_devicehub.parser.models import SnapshotsLog
from ereuse_devicehub.resources.action.models import Trade from ereuse_devicehub.resources.action.models import Trade
from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow 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.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
@ -476,6 +477,8 @@ class ExportsView(View):
'metrics': self.metrics, 'metrics': self.metrics,
'devices': self.devices_list, 'devices': self.devices_list,
'certificates': self.erasure, 'certificates': self.erasure,
'lots': self.lots_export,
'devices_lots': self.devices_lots_export,
} }
if export_id not in export_ids: if export_id not in export_ids:
@ -577,6 +580,111 @@ class ExportsView(View):
} }
return flask.render_template('inventory/erasure.html', **params) 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): class SnapshotListView(GenericMixin):
template_name = 'inventory/snapshots_list.html' template_name = 'inventory/snapshots_list.html'

View file

@ -151,6 +151,16 @@ class Lot(Thing):
"""Gets the lots that are not under any other lot.""" """Gets the lots that are not under any other lot."""
return cls.query.join(cls.paths).filter(db.func.nlevel(Path.path) == 1) 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): def add_children(self, *children):
"""Add children lots to this lot. """Add children lots to this lot.

View file

@ -111,7 +111,7 @@ class LotView(View):
query = query.filter(Lot.name.ilike(args['search'] + '%')) query = query.filter(Lot.name.ilike(args['search'] + '%'))
lots = query.paginate(per_page=6 if args['search'] else query.count()) lots = query.paginate(per_page=6 if args['search'] else query.count())
return things_response( return things_response(
self.schema.dump(lots.items, many=True, nested=2), self.get_lots_dump(lots),
lots.page, lots.page,
lots.per_page, lots.per_page,
lots.total, lots.total,
@ -120,6 +120,17 @@ class LotView(View):
) )
return jsonify(ret) 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): def visibility_filter(self, query):
query = ( query = (
query.outerjoin(Trade) query.outerjoin(Trade)
@ -141,7 +152,9 @@ class LotView(View):
# temporary # temporary
if lot_type == "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": if lot_type == "incoming":
return query.filter( return query.filter(

View file

@ -681,17 +681,32 @@ async function processSelectedDevices() {
return lot; 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(""); 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(); lotsSearcher.enable();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>"); listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>");
} }
} }
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(`<li style="color: black; text-align: center">${ title }<hr /></li>`);
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
}

View file

@ -646,17 +646,30 @@ async function processSelectedDevices() {
return lot; 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(""); 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(); lotsSearcher.enable();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>"); listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>");
} }
} }
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(`<li style="color: black; text-align: center">${ title }<hr /></li>`);
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
}

View file

@ -44,6 +44,29 @@
</a> </a>
</li><!-- End Search Icon--> </li><!-- End Search Icon-->
<li class="nav-item dropdown pe-3">
<a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown">
<span class="d-none d-md-block dropdown-toggle ps-2 pb-3 pt-3">Reports</span>
</a><!-- End Profile Iamge Icon -->
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile">
<li class="dropdown-header">
<h6>Exports</h6>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item d-flex align-items-center" href="{{ url_for('inventory.export', export_id='lots') }}">
<i class="bi bi-file-spreadsheet"></i>
<span>Lots Spreadsheet</span>
</a>
</li>
</ul>
</li>
<li class="nav-item dropdown pe-3"> <li class="nav-item dropdown pe-3">
<a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown"> <a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown">

View file

@ -247,9 +247,9 @@
</a> </a>
</li> </li>
<li> <li>
<a href="javascript:export_file('metrics')" class="dropdown-item"> <a href="javascript:export_file('devices_lots')" class="dropdown-item">
<i class="bi bi-file-spreadsheet"></i> <i class="bi bi-file-spreadsheet"></i>
Metrics Spreadsheet Devices Incoming and Outgoing Lots Spreadsheet
</a> </a>
</li> </li>
<li> <li>

View file

@ -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;;;;;
1 DHID Lot Id Lot Name Lot Type Transfer Status Transfer Code Transfer Date Transfer Creation Date Transfer Update Date
2 O48N2 c43a0d06-0c77-4a74-9c95-086645fbc534 lot1 Temporary

2
tests/files/lots.csv Normal file
View file

@ -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;;;;;;;;
1 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
2 cca691c4-b221-4882-924c-30cd545c0182 lot1 Temporary 1 1 0

View file

@ -3,16 +3,20 @@ import datetime
import json import json
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from uuid import UUID
import pytest import pytest
from flask import g
from flask.testing import FlaskClient from flask.testing import FlaskClient
from flask_wtf.csrf import generate_csrf from flask_wtf.csrf import generate_csrf
from ereuse_devicehub.client import UserClient, UserClientFlask from ereuse_devicehub.client import UserClient, UserClientFlask
from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.action.models import Snapshot from ereuse_devicehub.resources.action.models import Snapshot
from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.user.models import User
from tests import conftest from tests import conftest
@ -1309,3 +1313,75 @@ def test_edit_notes_with_closed_transfer(user3: UserClientFlask):
body, status = user3.post(uri, data=data) body, status = user3.post(uri, data=data)
assert status == '200 OK' assert status == '200 OK'
assert 'Receiver Note updated error!' in body 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])