Merge pull request #292 from eReuse/feature/3416-transaction-notes

Feature/3416 transaction notes
This commit is contained in:
cayop 2022-06-06 13:57:35 +02:00 committed by GitHub
commit 1297766577
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 597 additions and 4 deletions

View File

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

View File

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

View File

@ -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/<string:lot_id>/transfer/',
view_func=EditTransferView.as_view('edit_transfer'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/deliverynote/',
view_func=DeliveryNoteView.as_view('delivery_note'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/receivernote/',
view_func=ReceiverNoteView.as_view('receiver_note'),
)

View File

@ -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()}')

View File

@ -90,6 +90,16 @@
Transfer ({% if lot.transfer.closed %}<span class="text-danger">Closed</span>{% else %}<span class="text-success">Open</span>{% endif %})
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-delivery-note">
Delivery Note
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#edit-receiver-note">
Receiver Note
</button>
</li>
{% endif %}
</ul>
@ -480,6 +490,7 @@
<span class="text-danger">*</span>
{% endif %}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
@ -498,6 +509,70 @@
</div>
</form>
</div>
<div id="edit-delivery-note" class="tab-pane fade edit-delivery-note">
<h5 class="card-title">Delivery Note</h5>
<form method="post" action="{{ url_for('inventory.delivery_note', lot_id=lot.id) }}" class="row g-3 needs-validation" novalidate>
{{ form_delivery.csrf_token }}
{% for field in form_delivery %}
{% if field != form_delivery.csrf_token %}
<div class="col-12">
{% if field != form_delivery.type %}
{{ field.label(class_="form-label") }}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
{% if lot.transfer and form_receiver.is_editable() %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
{% endif %}
</form>
</div>
<div id="edit-receiver-note" class="tab-pane fade edit-receiver-note">
<h5 class="card-title">Receiver Note</h5>
<form method="post" action="{{ url_for('inventory.receiver_note', lot_id=lot.id) }}" class="row g-3 needs-validation" novalidate>
{{ form_receiver.csrf_token }}
{% for field in form_receiver %}
{% if field != form_receiver.csrf_token %}
<div class="col-12">
{% if field != form_receiver.type %}
{{ field.label(class_="form-label") }}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
{% if lot.transfer and form_receiver.is_editable() %}
<div>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
{% endif %}
</form>
</div>
{% endif %}
</div><!-- End Bordered Tabs -->

View File

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

View File

@ -1175,3 +1175,137 @@ def test_edit_transfer(user3: UserClientFlask):
assert 'one one one' in body
assert '<i class="bi bi-trash"></i> Delete Lot' not in body
assert 'Transfer (<span class="text-danger">Closed</span>)' 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