diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index e907e72b..5c671eb7 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -28,7 +28,7 @@ from wtforms import ( from wtforms.fields import FormField from ereuse_devicehub.db import db -from ereuse_devicehub.inventory.models import Transfer +from ereuse_devicehub.inventory.models import DeliveryNote, ReceiverNote, Transfer from ereuse_devicehub.parser.models import SnapshotsLog from ereuse_devicehub.parser.parser import ParseSnapshotLsHw from ereuse_devicehub.parser.schemas import Snapshot_lite @@ -1181,5 +1181,128 @@ class EditTransferForm(TransferForm): self.description.data = self._obj.description self.date.data = self._obj.date + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + date = self.date.data + if date and date > datetime.datetime.now().date(): + self.date.errors = ["You have to choose a date before today."] + is_valid = False + return is_valid + def set_obj(self, commit=True): self.populate_obj(self._obj) + + +class NotesForm(FlaskForm): + number = StringField( + 'Number', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="You can put a number for tracer of receiver or delivery", + ) + date = DateField( + 'Date', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="""Date when the transfer was do it""", + ) + units = IntegerField( + 'Units', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="Number of units", + ) + weight = IntegerField( + 'Weight', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="Weight expressed in Kg", + ) + + def __init__(self, *args, **kwargs): + self.type = kwargs.pop('type', None) + lot_id = kwargs.pop('lot_id', None) + self._tmp_lot = Lot.query.filter(Lot.id == lot_id).one() + self._obj = None + super().__init__(*args, **kwargs) + + if self._tmp_lot.transfer: + if self.type == 'Delivery': + self._obj = self._tmp_lot.transfer.delivery_note + if not self._obj: + self._obj = DeliveryNote(transfer_id=self._tmp_lot.transfer.id) + + self.date.description = """Date when the delivery was do it.""" + self.number.description = ( + """You can put a number for tracer of delivery note.""" + ) + + if self.type == 'Receiver': + self._obj = self._tmp_lot.transfer.receiver_note + if not self._obj: + self._obj = ReceiverNote(transfer_id=self._tmp_lot.transfer.id) + + self.date.description = """Date when the receipt was do it.""" + self.number.description = ( + """You can put a number for tracer of receiber note.""" + ) + + if self.is_editable(): + self.number.render_kw.pop('disabled', None) + self.date.render_kw.pop('disabled', None) + self.units.render_kw.pop('disabled', None) + self.weight.render_kw.pop('disabled', None) + else: + disabled = {'disabled': "disabled"} + self.number.render_kw.update(disabled) + self.date.render_kw.update(disabled) + self.units.render_kw.update(disabled) + self.weight.render_kw.update(disabled) + + if self._obj and not self.data['csrf_token']: + self.number.data = self._obj.number + self.date.data = self._obj.date + self.units.data = self._obj.units + self.weight.data = self._obj.weight + + def is_editable(self): + if not self._tmp_lot.transfer: + return False + + if self._tmp_lot.transfer.closed: + return False + + if self._tmp_lot.transfer.code: + return True + + if self._tmp_lot.transfer.user_from == g.user and self.type == 'Receiver': + return False + + if self._tmp_lot.transfer.user_to == g.user and self.type == 'Delivery': + return False + + return True + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + date = self.date.data + if date and date > datetime.datetime.now().date(): + self.date.errors = ["You have to choose a date before today."] + is_valid = False + + if not self.is_editable(): + is_valid = False + + return is_valid + + def save(self, commit=True): + if self._tmp_lot.transfer.closed: + return self._obj + + self.populate_obj(self._obj) + db.session.add(self._obj) + + if commit: + db.session.commit() + + return self._obj diff --git a/ereuse_devicehub/inventory/models.py b/ereuse_devicehub/inventory/models.py index f8aafe51..69414c10 100644 --- a/ereuse_devicehub/inventory/models.py +++ b/ereuse_devicehub/inventory/models.py @@ -1,7 +1,7 @@ from uuid import uuid4 from citext import CIText -from sqlalchemy import Column +from sqlalchemy import Column, Integer from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import backref, relationship from teal.db import CASCADE_OWN @@ -23,8 +23,8 @@ class Transfer(Thing): description = Column(CIText(), default='', nullable=True) lot_id = db.Column( UUID(as_uuid=True), - db.ForeignKey('lot.id', use_alter=True, name='lot_trade'), - nullable=True, + db.ForeignKey('lot.id', use_alter=True, name='lot_transfer'), + nullable=False, ) lot = relationship( 'Lot', @@ -42,3 +42,41 @@ class Transfer(Thing): return True return False + + +class DeliveryNote(Thing): + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) + number = Column(CIText(), default='', nullable=False) + date = Column(db.TIMESTAMP(timezone=True)) + units = Column(Integer, default=0) + weight = Column(Integer, default=0) + + transfer_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey('transfer.id'), + nullable=False, + ) + transfer = relationship( + 'Transfer', + backref=backref('delivery_note', lazy=True, uselist=False, cascade=CASCADE_OWN), + primaryjoin='DeliveryNote.transfer_id == Transfer.id', + ) + + +class ReceiverNote(Thing): + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) + number = Column(CIText(), default='', nullable=False) + date = Column(db.TIMESTAMP(timezone=True)) + units = Column(Integer, default=0) + weight = Column(Integer, default=0) + + transfer_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey('transfer.id'), + nullable=False, + ) + transfer = relationship( + 'Transfer', + backref=backref('receiver_note', lazy=True, uselist=False, cascade=CASCADE_OWN), + primaryjoin='ReceiverNote.transfer_id == Transfer.id', + ) diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 51cf36b7..b7a8298b 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -21,6 +21,7 @@ from ereuse_devicehub.inventory.forms import ( LotForm, NewActionForm, NewDeviceForm, + NotesForm, TagDeviceForm, TradeDocumentForm, TradeForm, @@ -52,11 +53,15 @@ class DeviceListMixin(GenericMixin): devices = form_filter.search() lot = None form_transfer = '' + form_delivery = '' + form_receiver = '' if lot_id: lot = lots.filter(Lot.id == lot_id).one() if not lot.is_temporary and lot.transfer: form_transfer = EditTransferForm(lot_id=lot.id) + form_delivery = NotesForm(lot_id=lot.id, type='Delivery') + form_receiver = NotesForm(lot_id=lot.id, type='Receiver') form_new_action = NewActionForm(lot=lot_id) self.context.update( @@ -67,6 +72,8 @@ class DeviceListMixin(GenericMixin): 'form_new_allocate': AllocateForm(lot=lot_id), 'form_new_datawipe': DataWipeForm(lot=lot_id), 'form_transfer': form_transfer, + 'form_delivery': form_delivery, + 'form_receiver': form_receiver, 'form_filter': form_filter, 'form_print_labels': PrintLabelsForm(), 'lot': lot, @@ -453,6 +460,10 @@ class EditTransferView(GenericMixin): return flask.redirect(next_url) messages.error('Transfer updated error!') + for k, v in form.errors.items(): + value = ';'.join(v) + key = form[k].label.text + messages.error('Error {key}: {value}!'.format(key=key, value=value)) return flask.redirect(next_url) @@ -631,6 +642,50 @@ class SnapshotDetailView(GenericMixin): ) +class DeliveryNoteView(GenericMixin): + methods = ['POST'] + form_class = NotesForm + + def dispatch_request(self, lot_id): + self.get_context() + form = self.form_class(request.form, lot_id=lot_id, type='Delivery') + next_url = url_for('inventory.lotdevicelist', lot_id=lot_id) + + if form.validate_on_submit(): + form.save() + messages.success('Delivery Note updated successfully!') + return flask.redirect(next_url) + + messages.error('Delivery Note updated error!') + for k, v in form.errors.items(): + value = ';'.join(v) + key = form[k].label.text + messages.error('Error {key}: {value}!'.format(key=key, value=value)) + return flask.redirect(next_url) + + +class ReceiverNoteView(GenericMixin): + methods = ['POST'] + form_class = NotesForm + + def dispatch_request(self, lot_id): + self.get_context() + form = self.form_class(request.form, lot_id=lot_id, type='Receiver') + next_url = url_for('inventory.lotdevicelist', lot_id=lot_id) + + if form.validate_on_submit(): + form.save() + messages.success('Receiver Note updated successfully!') + return flask.redirect(next_url) + + messages.error('Receiver Note updated error!') + for k, v in form.errors.items(): + value = ';'.join(v) + key = form[k].label.text + messages.error('Error {key}: {value}!'.format(key=key, value=value)) + return flask.redirect(next_url) + + devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add')) devices.add_url_rule('/action/trade/add/', view_func=NewTradeView.as_view('trade_add')) devices.add_url_rule( @@ -693,3 +748,11 @@ devices.add_url_rule( '/lot//transfer/', view_func=EditTransferView.as_view('edit_transfer'), ) +devices.add_url_rule( + '/lot//deliverynote/', + view_func=DeliveryNoteView.as_view('delivery_note'), +) +devices.add_url_rule( + '/lot//receivernote/', + view_func=ReceiverNoteView.as_view('receiver_note'), +) diff --git a/ereuse_devicehub/migrations/versions/dac62da1621a_transfer_notes.py b/ereuse_devicehub/migrations/versions/dac62da1621a_transfer_notes.py new file mode 100644 index 00000000..f09196eb --- /dev/null +++ b/ereuse_devicehub/migrations/versions/dac62da1621a_transfer_notes.py @@ -0,0 +1,158 @@ +"""transfer notes + +Revision ID: dac62da1621a +Revises: 054a3aea9f08 +Create Date: 2022-06-03 12:04:39.486276 + +""" +import citext +import sqlalchemy as sa +from alembic import context, op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'dac62da1621a' +down_revision = '054a3aea9f08' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + + +def upgrade(): + # creating delivery note table + op.create_table( + 'delivery_note', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('date', sa.TIMESTAMP(timezone=True), nullable=True), + sa.Column('number', citext.CIText(), nullable=True), + sa.Column('weight', sa.Integer(), nullable=True), + sa.Column('units', sa.Integer(), nullable=True), + sa.Column('transfer_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint(['transfer_id'], [f'{get_inv()}.transfer.id']), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + + # creating index + op.create_index( + op.f('ix_delivery_note_created'), + 'delivery_note', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_delivery_note_updated'), + 'delivery_note', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + 'ix_delivery_note_id', + 'delivery_note', + ['id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + + # creating receiver note table + op.create_table( + 'receiver_note', + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('date', sa.TIMESTAMP(timezone=True), nullable=True), + sa.Column('number', citext.CIText(), nullable=True), + sa.Column('weight', sa.Integer(), nullable=True), + sa.Column('units', sa.Integer(), nullable=True), + sa.Column('transfer_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.ForeignKeyConstraint(['transfer_id'], [f'{get_inv()}.transfer.id']), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + + # creating index + op.create_index( + op.f('ix_receiver_note_created'), + 'receiver_note', + ['created'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + op.f('ix_receiver_note_updated'), + 'receiver_note', + ['updated'], + unique=False, + schema=f'{get_inv()}', + ) + op.create_index( + 'ix_receiver_note_id', + 'receiver_note', + ['id'], + unique=False, + postgresql_using='hash', + schema=f'{get_inv()}', + ) + + +def downgrade(): + op.drop_index( + op.f('ix_delivery_note_created'), + table_name='delivery_note', + schema=f'{get_inv()}', + ) + op.drop_index( + op.f('ix_delivery_note_updated'), + table_name='delivery_note', + schema=f'{get_inv()}', + ) + op.drop_index( + op.f('ix_delivery_note_id'), table_name='delivery_note', schema=f'{get_inv()}' + ) + op.drop_table('delivery_note', schema=f'{get_inv()}') + + op.drop_index( + op.f('ix_receiver_note_created'), + table_name='receiver_note', + schema=f'{get_inv()}', + ) + op.drop_index( + op.f('ix_receiver_note_updated'), + table_name='receiver_note', + schema=f'{get_inv()}', + ) + op.drop_index( + op.f('ix_receiver_note_id'), table_name='receiver_note', schema=f'{get_inv()}' + ) + op.drop_table('receiver_note', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index d08bc413..38628a7c 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -90,6 +90,16 @@ Transfer ({% if lot.transfer.closed %}Closed{% else %}Open{% endif %}) + + {% endif %} @@ -480,6 +490,7 @@ * {% endif %} {{ field }} + {{ field.description }} {% if field.errors %}

{% for error in field.errors %} @@ -498,6 +509,70 @@ +

+
Delivery Note
+
+ {{ form_delivery.csrf_token }} + + {% for field in form_delivery %} + {% if field != form_delivery.csrf_token %} +
+ {% if field != form_delivery.type %} + {{ field.label(class_="form-label") }} + {{ field }} + {{ field.description }} + {% if field.errors %} +

+ {% for error in field.errors %} + {{ error }}
+ {% endfor %} +

+ {% endif %} + {% endif %} +
+ {% endif %} + {% endfor %} + + {% if lot.transfer and form_receiver.is_editable() %} +
+ Cancel + +
+ {% endif %} +
+
+
+
Receiver Note
+
+ {{ form_receiver.csrf_token }} + + {% for field in form_receiver %} + {% if field != form_receiver.csrf_token %} +
+ {% if field != form_receiver.type %} + {{ field.label(class_="form-label") }} + {{ field }} + {{ field.description }} + {% if field.errors %} +

+ {% for error in field.errors %} + {{ error }}
+ {% endfor %} +

+ {% endif %} + {% endif %} +
+ {% endif %} + {% endfor %} + + {% if lot.transfer and form_receiver.is_editable() %} +
+ Cancel + +
+ {% endif %} +
+
{% endif %} diff --git a/tests/test_basic.py b/tests/test_basic.py index e933dea7..bb9196a2 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -61,6 +61,8 @@ def test_api_docs(client: Client): '/inventory/lot/{id}/del/', '/inventory/lot/{lot_id}/device/', '/inventory/lot/{lot_id}/device/add/', + '/inventory/lot/{lot_id}/deliverynote/', + '/inventory/lot/{lot_id}/receivernote/', '/inventory/lot/{lot_id}/trade-document/add/', '/inventory/lot/{lot_id}/transfer/{type_id}/', '/inventory/lot/{lot_id}/transfer/', diff --git a/tests/test_render_2_0.py b/tests/test_render_2_0.py index 37c06004..90ae4b4c 100644 --- a/tests/test_render_2_0.py +++ b/tests/test_render_2_0.py @@ -1175,3 +1175,137 @@ def test_edit_transfer(user3: UserClientFlask): assert 'one one one' in body assert ' Delete Lot' not in body assert 'Transfer (Closed)' in body + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_edit_deliverynote(user3: UserClientFlask): + # create lot + 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() + lot_id = lot.id + + # create new incoming lot + uri = f'/inventory/lot/{lot_id}/transfer/incoming/' + data = {'csrf_token': generate_csrf(), 'code': 'AAA'} + user3.post(uri, data=data) + lot = Lot.query.filter()[1] + lot_id = lot.id + + # edit delivery with errors + uri = f'/inventory/lot/{lot_id}/deliverynote/' + data = { + 'csrf_token': generate_csrf(), + 'number': 'AAA', + 'units': 10, + 'weight': 50, + 'date': datetime.datetime.now().date() + datetime.timedelta(15), + } + body, status = user3.post(uri, data=data) + assert status == '200 OK' + assert 'Delivery Note updated error!' in body + + # # edit transfer successfully + data['date'] = datetime.datetime.now().date() - datetime.timedelta(15) + body, status = user3.post(uri, data=data) + assert status == '200 OK' + assert 'Delivery Note updated successfully!' in body + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_edit_receivernote(user3: UserClientFlask): + # create lot + 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() + lot_id = lot.id + + # create new incoming lot + uri = f'/inventory/lot/{lot_id}/transfer/incoming/' + data = {'csrf_token': generate_csrf(), 'code': 'AAA'} + user3.post(uri, data=data) + lot = Lot.query.filter()[1] + lot_id = lot.id + + # edit delivery with errors + uri = f'/inventory/lot/{lot_id}/receivernote/' + data = { + 'csrf_token': generate_csrf(), + 'number': 'AAA', + 'units': 10, + 'weight': 50, + 'date': datetime.datetime.now().date() + datetime.timedelta(15), + } + body, status = user3.post(uri, data=data) + assert status == '200 OK' + assert 'Receiver Note updated error!' in body + + # # edit transfer successfully + data['date'] = datetime.datetime.now().date() - datetime.timedelta(15) + body, status = user3.post(uri, data=data) + assert status == '200 OK' + assert 'Receiver Note updated successfully!' in body + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_edit_notes_with_closed_transfer(user3: UserClientFlask): + # create lot + 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() + lot_id = lot.id + + # create new incoming lot + uri = f'/inventory/lot/{lot_id}/transfer/incoming/' + data = {'csrf_token': generate_csrf(), 'code': 'AAA'} + user3.post(uri, data=data) + lot = Lot.query.filter()[1] + lot_id = lot.id + + # edit transfer adding date + uri = f'/inventory/lot/{lot_id}/transfer/' + data['date'] = datetime.datetime.now().date() - datetime.timedelta(15) + user3.post(uri, data=data) + assert lot.transfer.closed is True + + # edit delivery with errors + uri = f'/inventory/lot/{lot_id}/deliverynote/' + data = { + 'csrf_token': generate_csrf(), + 'number': 'AAA', + 'units': 10, + 'weight': 50, + } + body, status = user3.post(uri, data=data) + assert status == '200 OK' + assert 'Delivery Note updated error!' in body + + # edit receiver with errors + uri = f'/inventory/lot/{lot_id}/receivernote/' + data = { + 'csrf_token': generate_csrf(), + 'number': 'AAA', + 'units': 10, + 'weight': 50, + } + body, status = user3.post(uri, data=data) + assert status == '200 OK' + assert 'Receiver Note updated error!' in body