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 wtforms.fields import FormField
from ereuse_devicehub.db import db 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.models import SnapshotsLog
from ereuse_devicehub.parser.parser import ParseSnapshotLsHw from ereuse_devicehub.parser.parser import ParseSnapshotLsHw
from ereuse_devicehub.parser.schemas import Snapshot_lite from ereuse_devicehub.parser.schemas import Snapshot_lite
@ -1181,5 +1181,128 @@ class EditTransferForm(TransferForm):
self.description.data = self._obj.description self.description.data = self._obj.description
self.date.data = self._obj.date 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): def set_obj(self, commit=True):
self.populate_obj(self._obj) 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 uuid import uuid4
from citext import CIText from citext import CIText
from sqlalchemy import Column 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
from teal.db import CASCADE_OWN from teal.db import CASCADE_OWN
@ -23,8 +23,8 @@ class Transfer(Thing):
description = Column(CIText(), default='', nullable=True) description = Column(CIText(), default='', nullable=True)
lot_id = db.Column( lot_id = db.Column(
UUID(as_uuid=True), UUID(as_uuid=True),
db.ForeignKey('lot.id', use_alter=True, name='lot_trade'), db.ForeignKey('lot.id', use_alter=True, name='lot_transfer'),
nullable=True, nullable=False,
) )
lot = relationship( lot = relationship(
'Lot', 'Lot',
@ -42,3 +42,41 @@ class Transfer(Thing):
return True return True
return False 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, LotForm,
NewActionForm, NewActionForm,
NewDeviceForm, NewDeviceForm,
NotesForm,
TagDeviceForm, TagDeviceForm,
TradeDocumentForm, TradeDocumentForm,
TradeForm, TradeForm,
@ -52,11 +53,15 @@ class DeviceListMixin(GenericMixin):
devices = form_filter.search() devices = form_filter.search()
lot = None lot = None
form_transfer = '' form_transfer = ''
form_delivery = ''
form_receiver = ''
if lot_id: if lot_id:
lot = lots.filter(Lot.id == lot_id).one() lot = lots.filter(Lot.id == lot_id).one()
if not lot.is_temporary and lot.transfer: if not lot.is_temporary and lot.transfer:
form_transfer = EditTransferForm(lot_id=lot.id) 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) form_new_action = NewActionForm(lot=lot_id)
self.context.update( self.context.update(
@ -67,6 +72,8 @@ class DeviceListMixin(GenericMixin):
'form_new_allocate': AllocateForm(lot=lot_id), 'form_new_allocate': AllocateForm(lot=lot_id),
'form_new_datawipe': DataWipeForm(lot=lot_id), 'form_new_datawipe': DataWipeForm(lot=lot_id),
'form_transfer': form_transfer, 'form_transfer': form_transfer,
'form_delivery': form_delivery,
'form_receiver': form_receiver,
'form_filter': form_filter, 'form_filter': form_filter,
'form_print_labels': PrintLabelsForm(), 'form_print_labels': PrintLabelsForm(),
'lot': lot, 'lot': lot,
@ -453,6 +460,10 @@ class EditTransferView(GenericMixin):
return flask.redirect(next_url) return flask.redirect(next_url)
messages.error('Transfer updated error!') 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) 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/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('/action/trade/add/', view_func=NewTradeView.as_view('trade_add'))
devices.add_url_rule( devices.add_url_rule(
@ -693,3 +748,11 @@ devices.add_url_rule(
'/lot/<string:lot_id>/transfer/', '/lot/<string:lot_id>/transfer/',
view_func=EditTransferView.as_view('edit_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 %}) Transfer ({% if lot.transfer.closed %}<span class="text-danger">Closed</span>{% else %}<span class="text-success">Open</span>{% endif %})
</button> </button>
</li> </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 %} {% endif %}
</ul> </ul>
@ -480,6 +490,7 @@
<span class="text-danger">*</span> <span class="text-danger">*</span>
{% endif %} {% endif %}
{{ field }} {{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %} {% if field.errors %}
<p class="text-danger"> <p class="text-danger">
{% for error in field.errors %} {% for error in field.errors %}
@ -498,6 +509,70 @@
</div> </div>
</form> </form>
</div> </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 %} {% endif %}
</div><!-- End Bordered Tabs --> </div><!-- End Bordered Tabs -->

View File

@ -61,6 +61,8 @@ def test_api_docs(client: Client):
'/inventory/lot/{id}/del/', '/inventory/lot/{id}/del/',
'/inventory/lot/{lot_id}/device/', '/inventory/lot/{lot_id}/device/',
'/inventory/lot/{lot_id}/device/add/', '/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}/trade-document/add/',
'/inventory/lot/{lot_id}/transfer/{type_id}/', '/inventory/lot/{lot_id}/transfer/{type_id}/',
'/inventory/lot/{lot_id}/transfer/', '/inventory/lot/{lot_id}/transfer/',

View File

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