Merge pull request #303 from eReuse/features/transfer-3493-3492-3510
Features/transfer 3493 3492 3510
This commit is contained in:
commit
97653058c6
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -681,17 +681,32 @@ async function processSelectedDevices() {
|
|||
|
||||
return lot;
|
||||
});
|
||||
|
||||
listHTML.html("");
|
||||
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("<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.html("");
|
||||
listHTML.append(`<li style="color: black; text-align: center">${ title }<hr /></li>`);
|
||||
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
|
||||
lotsSearcher.enable();
|
||||
} catch (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>");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("<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));
|
||||
}
|
||||
|
|
|
@ -44,6 +44,29 @@
|
|||
</a>
|
||||
</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">
|
||||
|
||||
<a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown">
|
||||
|
|
|
@ -247,9 +247,9 @@
|
|||
</a>
|
||||
</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>
|
||||
Metrics Spreadsheet
|
||||
Devices Incoming and Outgoing Lots Spreadsheet
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
|
2
tests/files/devices_lots.csv
Normal file
2
tests/files/devices_lots.csv
Normal 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;;;;;
|
|
2
tests/files/lots.csv
Normal file
2
tests/files/lots.csv
Normal 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;;;;;;;;
|
|
|
@ -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])
|
||||
|
|
Reference in a new issue