Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/a183705985788614_contract_item_u…
Browse files Browse the repository at this point in the history
…nit_value' into dev
  • Loading branch information
vmaksymiv committed Nov 22, 2016
2 parents 9931e9d + 329e56c commit 4cd8352
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 50 deletions.
90 changes: 87 additions & 3 deletions openprocurement/tender/limited/models.py
Expand Up @@ -13,25 +13,73 @@
embedded_lot_role, ListType, default_lot_role, validate_lots_uniq,
)
from openprocurement.api.models import (
Value, IsoDateTimeType, Document, Organization, SchematicsDocument,
IsoDateTimeType, Document, Organization, SchematicsDocument,
Model, Revision, Period, view_bid_role,
)
from openprocurement.api.models import validate_cpv_group, validate_items_uniq
from openprocurement.api.models import get_now
from openprocurement.api.models import Cancellation as BaseCancellation
from openprocurement.api.models import ITender
from openprocurement.api.models import Contract as BaseContract
from openprocurement.api.models import Value as BaseValue
from openprocurement.api.models import Unit as BaseUnit
from openprocurement.api.models import ProcuringEntity as BaseProcuringEntity
from openprocurement.tender.openua.models import Complaint as BaseComplaint
from openprocurement.tender.openua.models import Item as BaseItem
from openprocurement.tender.openua.models import Item
from openprocurement.tender.openua.models import Tender as OpenUATender


class Value(BaseValue):
currency = StringType(max_length=3, min_length=3)
valueAddedTaxIncluded = BooleanType()

@serializable(serialized_name="currency", serialize_when_none=False)
def unit_currency(self):
if self.currency is not None:
return self.currency

context = self.__parent__ if isinstance(self.__parent__, Model) else {}
while isinstance(context.__parent__, Model):
context = context.__parent__
if isinstance(context, BaseContract):
break

value = context.get("value", {})
return value.get("currency", None)

@serializable(serialized_name="valueAddedTaxIncluded", serialize_when_none=False)
def unit_valueAddedTaxIncluded(self):
if self.valueAddedTaxIncluded is not None:
return self.valueAddedTaxIncluded

context = self.__parent__ if isinstance(self.__parent__, Model) else {}
while isinstance(context.__parent__, Model):
context = context.__parent__
if isinstance(context, BaseContract):
break

value = context.get("value", {})
return value.get("valueAddedTaxIncluded", None)

class Unit(BaseUnit):
value = ModelType(Value)


class BaseItem(Item):
unit = ModelType(Unit)

class Options:
roles = {
'edit_contract': whitelist('unit')
}

class Item(BaseItem):

def validate_relatedLot(self, data, relatedLot):
if relatedLot and isinstance(data['__parent__'], Model):
raise ValidationError(u"This option is not available")


class Complaint(BaseComplaint):
class Options:
roles = {
Expand All @@ -42,10 +90,30 @@ class Options:
class Contract(BaseContract):
items = ListType(ModelType(Item))

class Options:
roles = {
'edit': whitelist(),
'edit_contract': blacklist('id', 'documents', 'date', 'awardID', 'suppliers', 'contractID'),
}

def validate_dateSigned(self, data, value):
if value and value > get_now():
raise ValidationError(u"Contract signature date can't be in the future")

def get_role(self):
root = self.__parent__
while root.__parent__ is not None:
root = root.__parent__
request = root.request
role = 'edit'
if request.authenticated_role == 'tender_owner':
role = 'edit_contract'
return role


Unit = BaseUnit
Value = BaseValue


award_edit_role = blacklist('id', 'items', 'date', 'documents', 'complaints', 'complaintPeriod')
award_create_role = blacklist('id', 'status', 'items', 'date', 'documents', 'complaints', 'complaintPeriod')
Expand Down Expand Up @@ -236,10 +304,11 @@ def import_data(self, raw_data, **kw):
self._data.update(data)
return self

ReportingTender = Tender

ReportingTender = Tender
Item = BaseItem


class Award(ReportingAward):

lotID = MD5Type()
Expand Down Expand Up @@ -294,6 +363,21 @@ def lot_value(self):
class Contract(BaseContract):
items = ListType(ModelType(Item))

class Options:
roles = {
'edit': whitelist(),
'edit_contract': blacklist('id', 'documents', 'date', 'awardID', 'suppliers', 'contractID'),
}

def get_role(self):
root = self.__parent__
while root.__parent__ is not None:
root = root.__parent__
request = root.request
role = 'edit'
if request.authenticated_role == 'tender_owner':
role = 'edit_contract'
return role

@implementer(ITender)
class Tender(ReportingTender):
Expand Down
153 changes: 107 additions & 46 deletions openprocurement/tender/limited/tests/contract.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import unittest
import time
from copy import deepcopy
from iso8601 import parse_date
from datetime import timedelta

Expand Down Expand Up @@ -106,52 +107,6 @@ def test_create_tender_contract_invalid(self):
{u'description': [u'awardID should be one of awards'], u'location': u'body', u'name': u'awardID'}
])

def test_create_tender_contract_with_token(self):
# This can not be, but just in case check
self.app.authorization = ('Basic', ('token', ''))
response = self.app.post_json('/tenders/{}/contracts'.format(self.tender_id),
{'data': {'title': 'contract title',
'description': 'contract description',
'awardID': self.award_id}})
self.assertEqual(response.status, '201 Created')
self.assertEqual(response.content_type, 'application/json')
contract = response.json['data']
self.assertIn('id', contract)
self.assertIn(contract['id'], response.headers['Location'])

response = self.app.patch_json('/tenders/{}/contracts/{}'.format(self.tender_id, contract['id']),
{"data": {"status": "terminated"}})
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['data']["status"], "terminated")

response = self.app.patch_json('/tenders/{}/contracts/{}'.format(self.tender_id, contract['id']),
{"data": {"status": "pending"}}, status=403)
self.assertEqual(response.status, '403 Forbidden')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['errors'][0]["description"], "Can't update contract status")

tender = self.db.get(self.tender_id)
for i in tender.get('awards', []):
if i.get('complaintPeriod', {}): # works for negotiation tender
i['complaintPeriod']['endDate'] = i['complaintPeriod']['startDate']
self.db.save(tender)

response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, contract['id'], self.tender_token), {"data": {"status": "active"}})
self.assertEqual(response.status, '200 OK')

response = self.app.get('/tenders/{}'.format(self.tender_id))
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.json['data']['status'], 'complete')

response = self.app.post_json('/tenders/{}/contracts'.format(self.tender_id),
{'data': {'title': 'contract title',
'description': 'contract description',
'awardID': self.award_id}},
status=403)
self.assertEqual(response.status, '403 Forbidden')

def test_create_tender_contract(self):
response = self.app.get('/tenders/{}/contracts'.format(self.tender_id))
self.contract_id = response.json['data'][0]['id']
Expand Down Expand Up @@ -243,6 +198,50 @@ def test_patch_tender_contract(self):
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.json['data']['value']['amount'], 238)

# check contract.items modification (contract.item.unit.value.amount
# modification should be allowed)
response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{"data": {"items": [{'unit': {'value': {'amount': 12}}}]}})
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['data']["items"][0]['unit']['value']['amount'], 12)
self.assertEqual(response.json['data']["items"][0]['unit']['value']['currency'], response.json['data']['value']['currency'])
self.assertEqual(response.json['data']["items"][0]['unit']['value']['valueAddedTaxIncluded'], response.json['data']['value']['valueAddedTaxIncluded'])

response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{"data": {"items": [{'unit': {'value': {'currency': "USD", 'valueAddedTaxIncluded': False}}}]}})
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.json['data']["items"][0]['unit']['value']['currency'], "USD")
self.assertEqual(response.json['data']["items"][0]['unit']['value']['valueAddedTaxIncluded'], False)

response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{'data': {'items': [{
'description': u"custom item descriptionX",
'quantity': 5,
'deliveryLocation': {'latitude': "12.123", 'longitude': "170.123"}
}]}})
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'application/json')
self.assertNotEqual(response.json['data']['items'][0]['description'], u"custom item descriptionX")
self.assertNotEqual(response.json['data']['items'][0]['quantity'], 99999)
self.assertNotIn('deliveryLocation', response.json['data']['items'][0])
self.assertEqual(response.json['data']["items"][0]['unit']['value']['amount'], 12)
self.assertEqual(response.json['data']["items"][0]['unit']['value']['currency'], "USD")
self.assertEqual(response.json['data']["items"][0]['unit']['value']['valueAddedTaxIncluded'], False)

# try to add/delete contract item
item = deepcopy(response.json['data']["items"][0])
response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{'data': {'items': [{}, item]}},
status=403)
self.assertEqual(response.status, '403 Forbidden')
self.assertEqual(response.json['errors'][0]["description"],
"Can't change items count")

response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{"data": {"status": "active"}})
Expand Down Expand Up @@ -489,6 +488,68 @@ def test_patch_tender_contract(self):
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.json['data']['value']['amount'], 238)

# check contract.items modification (contract.item.unit.value.amount
# modification should be allowed)
response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{"data": {"items": [{'unit': {'value': {'amount': 12}}}]}})
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['data']["items"][0]['unit']['value']['amount'], 12)
self.assertEqual(response.json['data']["items"][0]['unit']['value']['currency'], response.json['data']['value']['currency'])
self.assertEqual(response.json['data']["items"][0]['unit']['value']['valueAddedTaxIncluded'], response.json['data']['value']['valueAddedTaxIncluded'])

response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{"data": {"items": [{'unit': {'value': {'currency': "USD", 'valueAddedTaxIncluded': False}}}]}})
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.json['data']["items"][0]['unit']['value']['currency'], "USD")
self.assertEqual(response.json['data']["items"][0]['unit']['value']['valueAddedTaxIncluded'], False)

response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{'data': {'items': [{
'description': u"custom item descriptionX",
'quantity': 5,
'deliveryLocation': {'latitude': "12.123", 'longitude': "170.123"}
}]}})
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'application/json')
self.assertNotEqual(response.json['data']['items'][0]['description'], u"custom item descriptionX")
self.assertNotEqual(response.json['data']['items'][0]['quantity'], 99999)
self.assertNotIn('deliveryLocation', response.json['data']['items'][0])
self.assertEqual(response.json['data']["items"][0]['unit']['value']['amount'], 12)
self.assertEqual(response.json['data']["items"][0]['unit']['value']['currency'], "USD")
self.assertEqual(response.json['data']["items"][0]['unit']['value']['valueAddedTaxIncluded'], False)

# try to add contract item
item = deepcopy(response.json['data']["items"][0])
response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{'data': {'items': [{}, item]}},
status=403)
self.assertEqual(response.status, '403 Forbidden')
self.assertEqual(response.json['errors'][0]["description"],
"Can't change items count")

# add second item to contract. the quickest way
tender = self.db.get(self.tender_id)
item = tender['contracts'][0]['items'][0]
tender['contracts'][0]['items'] = [item, item]
self.db.save(tender)

response = self.app.get('/tenders/{}/contracts/{}'.format(self.tender_id, self.contract_id))
self.assertEqual(len(response.json['data']["items"]), 2)

# try to delete contract item
response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token),
{'data': {'items': [{}]}},
status=403)
self.assertEqual(response.status, '403 Forbidden')
self.assertEqual(response.json['errors'][0]["description"],
"Can't change items count")

response = self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(
self.tender_id, self.contract_id, self.tender_token), {"data": {"status": "active"}})
self.assertEqual(response.status, '200 OK')
Expand Down
25 changes: 25 additions & 0 deletions openprocurement/tender/limited/tests/tender.py
Expand Up @@ -773,6 +773,31 @@ def test_patch_tender(self):
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['errors'][0]["description"], "Can't update tender in current (complete) status")

def test_tender_items(self):
response = self.app.get('/tenders')
self.assertEqual(response.status, '200 OK')
self.assertEqual(len(response.json['data']), 0)
data = deepcopy(self.initial_data)

response = self.app.post_json('/tenders', {'data': self.initial_data})
self.assertEqual(response.status, '201 Created')
tender = response.json['data']
owner_token = response.json['access']['token']
dateModified = tender.pop('dateModified')

response = self.app.patch_json('/tenders/{}?acc_token={}'.format(
tender['id'], owner_token), {'data': {'items': [{
'description': u"custom item descriptionX",
'quantity': 15,
'deliveryLocation': {'latitude': "12.123", 'longitude': "170.123"}
}]}})
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['data']['items'][0]['description'], u"custom item descriptionX")
self.assertEqual(response.json['data']['items'][0]['quantity'], 15)
self.assertEqual(response.json['data']['items'][0]['deliveryLocation']['latitude'], "12.123")
self.assertEqual(response.json['data']['items'][0]['deliveryLocation']['longitude'], "170.123")

def test_dateModified_tender(self):
response = self.app.get('/tenders')
self.assertEqual(response.status, '200 OK')
Expand Down
14 changes: 14 additions & 0 deletions openprocurement/tender/limited/views/contract.py
Expand Up @@ -111,6 +111,13 @@ def patch(self):
self.request.errors.status = 403
return

if data.get('items') and len(data['items']) != len(self.request.context['items']):
# as it is alowed to set/change contract.item.unit.value we need to
# ensure that nobody is able to add or delete contract.item
self.request.errors.add('body', 'data', 'Can\'t change items count')
self.request.errors.status = 403
return

contract_status = self.request.context.status
apply_patch(self.request, save=False, src=self.request.context.serialize())
self.request.context.date = get_now()
Expand Down Expand Up @@ -179,6 +186,13 @@ def patch(self):
self.request.errors.status = 403
return

if data.get('items') and len(data['items']) != len(self.request.context['items']):
# as it is alowed to set/change contract.item.unit.value we need to
# ensure that nobody is able to add or delete contract.item
self.request.errors.add('body', 'data', 'Can\'t change items count')
self.request.errors.status = 403
return

contract_status = self.request.context.status
apply_patch(self.request, save=False, src=self.request.context.serialize())
self.request.context.date = get_now()
Expand Down
2 changes: 1 addition & 1 deletion setup.py
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
import os

version = '2.3.18'
version = '2.3.19'

requires = [
'setuptools',
Expand Down

0 comments on commit 4cd8352

Please sign in to comment.