def check_tender_status_on_active_qualification_stand_still(request): tender = request.validated["tender"] config = getAdapter(tender, IContentConfigurator) now = get_now() active_lots = [ lot.id for lot in tender.lots if lot.status == "active" ] if tender.lots else [None] if not ( tender.awardPeriod and tender.awardPeriod.endDate <= now and not any( [ i.status in tender.block_complaint_status for a in tender.awards for i in a.complaints if a.lotID in active_lots ] ) ): return statuses = set() if tender.lots: for lot in tender.lots: if lot.status != "active": # pragma: no cover statuses.add(lot.status) continue active_lot_awards = [i for i in tender.awards if i.lotID == lot.id and i.status == "active"] if len(active_lot_awards) < config.min_bids_number: LOGGER.info( "Switched lot {} of tender {} to {}".format(lot.id, tender.id, "unsuccessful"), extra=context_unpack(request, {"MESSAGE_ID": "switched_lot_unsuccessful"}, {"LOT_ID": lot.id}), ) lot.status = "unsuccessful" statuses.add(lot.status) continue statuses.add(lot.status) else: # pragma: no cover active_awards = [i for i in tender.awards if i.status == "active"] if len(active_awards) <= config.min_bids_count: statuses.add("unsuccessful") else: statuses.add("active.awarded") if statuses == {"cancelled"}: # pragma: no cover LOGGER.info( "Switched tender {} to {}".format(tender.id, "cancelled"), extra=context_unpack(request, {"MESSAGE_ID": "switched_tender_cancelled"}), ) tender.status = "cancelled" elif not statuses.difference({"unsuccessful", "cancelled"}): # pragma: no cover LOGGER.info( "Switched tender {} to {}".format(tender.id, "unsuccessful"), extra=context_unpack(request, {"MESSAGE_ID": "switched_tender_unsuccessful"}), ) tender.status = "unsuccessful" else: LOGGER.info( "Switched tender {} to {}".format(tender.id, "active.awarded"), extra=context_unpack(request, {"MESSAGE_ID": "switched_tender_active_awarded"}), ) tender.status = "active.awarded" clarif_date = calculate_tender_business_date(now, config.clarifications_until_period, tender, False) tender.contractPeriod = { "startDate": now, "clarificationsUntil": clarif_date } lots = [l for l in tender.get("lots", []) if l.status == "active"] if lots: for lot in lots: agreement_data = generate_agreement_data(request, tender, lot) agreement = type(tender).agreements.model_class(agreement_data) agreement.__parent__ = tender tender.agreements.append(agreement) else: # pragma: no cover agreement_data = generate_agreement_data(request, tender) agreement = type(tender).agreements.model_class(agreement_data) agreement.__parent__ = tender tender.agreements.append(agreement)
def patch_eu(self): """Tender Edit (partial) For example here is how procuring entity can change number of items to be procured and total Value of a tender: .. sourcecode:: http PATCH /tenders/4879d3f8ee2443169b5fbbc9f89fa607 HTTP/1.1 Host: example.com Accept: application/json { "data": { "value": { "amount": 600 }, "itemsToBeProcured": [ { "quantity": 6 } ] } } And here is the response to be expected: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "data": { "id": "4879d3f8ee2443169b5fbbc9f89fa607", "tenderID": "UA-64e93250be76435397e8c992ed4214d1", "dateModified": "2014-10-27T08:12:34.956Z", "value": { "amount": 600 }, "itemsToBeProcured": [ { "quantity": 6 } ] } } """ tender = self.context data = self.request.validated["data"] if ( self.request.authenticated_role == "tender_owner" and self.request.validated["tender_status"] == "active.tendering" ): if "tenderPeriod" in data and "endDate" in data["tenderPeriod"]: self.request.validated["tender"].tenderPeriod.import_data(data["tenderPeriod"]) validate_tender_period_extension(self.request) self.request.registry.notify(TenderInitializeEvent(self.request.validated["tender"])) self.request.validated["data"]["enquiryPeriod"] = self.request.validated["tender"].enquiryPeriod.serialize() apply_patch(self.request, save=False, src=self.request.validated["tender_src"]) if self.request.authenticated_role == "chronograph": check_status(self.request) elif self.request.authenticated_role == "tender_owner" and tender.status == "active.tendering": tender.invalidate_bids_data() elif ( self.request.authenticated_role == "tender_owner" and self.request.validated["tender_status"] == "active.pre-qualification" and tender.status == "active.pre-qualification.stand-still" ): if all_bids_are_reviewed(self.request): tender.qualificationPeriod.endDate = calculate_tender_business_date( get_now(), COMPLAINT_STAND_STILL, self.request.validated["tender"] ) tender.check_auction_time() else: raise_operation_error( self.request, "Can't switch to 'active.pre-qualification.stand-still' while not all bids are qualified" ) elif ( self.request.authenticated_role == "tender_owner" and self.request.validated["tender_status"] == "active.pre-qualification" and tender.status != "active.pre-qualification.stand-still" ): raise_operation_error(self.request, "Can't update tender status") save_tender(self.request) self.LOGGER.info( "Updated tender {}".format(tender.id), extra=context_unpack(self.request, {"MESSAGE_ID": "tender_patch"}) ) return {"data": tender.serialize(tender.status)}
def next_check(self): now = get_now() checks = [] if self.status == "active.enquiries" and self.tenderPeriod.startDate: checks.append(self.tenderPeriod.startDate.astimezone(TZ)) elif self.status == "active.enquiries" and self.enquiryPeriod.endDate: checks.append(self.enquiryPeriod.endDate.astimezone(TZ)) elif self.status == "active.tendering" and self.tenderPeriod.endDate: checks.append(self.tenderPeriod.endDate.astimezone(TZ)) elif (not self.lots and self.status == "active.auction" and self.auctionPeriod and self.auctionPeriod.startDate and not self.auctionPeriod.endDate): if now < self.auctionPeriod.startDate: checks.append(self.auctionPeriod.startDate.astimezone(TZ)) elif now < calc_auction_end_time( self.numberOfBids, self.auctionPeriod.startDate).astimezone(TZ): checks.append( calc_auction_end_time( self.numberOfBids, self.auctionPeriod.startDate).astimezone(TZ)) elif self.lots and self.status == "active.auction": for lot in self.lots: if (lot.status != "active" or not lot.auctionPeriod or not lot.auctionPeriod.startDate or lot.auctionPeriod.endDate): continue if now < lot.auctionPeriod.startDate: checks.append(lot.auctionPeriod.startDate.astimezone(TZ)) elif now < calc_auction_end_time( lot.numberOfBids, lot.auctionPeriod.startDate).astimezone(TZ): checks.append( calc_auction_end_time( lot.numberOfBids, lot.auctionPeriod.startDate).astimezone(TZ)) elif (not self.lots and self.status == "active.awarded" and not any( [i.status in self.block_complaint_status for i in self.complaints]) and not any([ i.status in self.block_complaint_status for a in self.awards for i in a.complaints ])): standStillEnds = [ a.complaintPeriod.endDate.astimezone(TZ) for a in self.awards if a.complaintPeriod.endDate ] last_award_status = self.awards[-1].status if self.awards else "" if standStillEnds and last_award_status == "unsuccessful": checks.append(max(standStillEnds)) elif (self.lots and self.status in ["active.qualification", "active.awarded"] and not any([ i.status in self.block_complaint_status and i.relatedLot is None for i in self.complaints ])): for lot in self.lots: if lot["status"] != "active": continue lot_awards = [i for i in self.awards if i.lotID == lot.id] pending_complaints = any([ i["status"] in self.block_complaint_status and i.relatedLot == lot.id for i in self.complaints ]) pending_awards_complaints = any([ i.status in self.block_complaint_status for a in lot_awards for i in a.complaints ]) standStillEnds = [ a.complaintPeriod.endDate.astimezone(TZ) for a in lot_awards if a.complaintPeriod.endDate ] last_award_status = lot_awards[-1].status if lot_awards else "" if (not pending_complaints and not pending_awards_complaints and standStillEnds and last_award_status == "unsuccessful"): checks.append(max(standStillEnds)) if self.status.startswith("active"): from openprocurement.tender.core.utils import calculate_tender_business_date for complaint in self.complaints: if complaint.status == "answered" and complaint.dateAnswered: checks.append( calculate_tender_business_date( complaint.dateAnswered, COMPLAINT_STAND_STILL_TIME, self)) elif complaint.status == "pending": checks.append(self.dateModified) for award in self.awards: if award.status == "active" and not any( [i.awardID == award.id for i in self.contracts]): checks.append(award.date) for complaint in award.complaints: if complaint.status == "answered" and complaint.dateAnswered: checks.append( calculate_tender_business_date( complaint.dateAnswered, COMPLAINT_STAND_STILL_TIME, self)) elif complaint.status == "pending": checks.append(self.dateModified) return min(checks).isoformat() if checks else None
def patch(self): """Tender Edit (partial) For example here is how procuring entity can change number of items to be procured and total Value of a tender: .. sourcecode:: http PATCH /tenders/4879d3f8ee2443169b5fbbc9f89fa607 HTTP/1.1 Host: example.com Accept: application/json { "data": { "value": { "amount": 600 }, "itemsToBeProcured": [ { "quantity": 6 } ] } } And here is the response to be expected: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "data": { "id": "4879d3f8ee2443169b5fbbc9f89fa607", "tenderID": "UA-64e93250be76435397e8c992ed4214d1", "dateModified": "2014-10-27T08:12:34.956Z", "value": { "amount": 600 }, "itemsToBeProcured": [ { "quantity": 6 } ] } } """ tender = self.context if self.request.authenticated_role == "chronograph": apply_patch(self.request, save=False, src=self.request.validated["tender_src"]) check_status(self.request) elif self.request.authenticated_role == "agreement_selection": apply_patch(self.request, save=False, src=self.request.validated["tender_src"]) if self.request.tender.status == "active.enquiries": check_agreement(self.request, tender) if tender.status == "active.enquiries": tender.enquiryPeriod.startDate = get_now() tender.enquiryPeriod.endDate = calculate_tender_business_date( tender.enquiryPeriod.startDate, self.request.content_configurator.enquiry_period, tender ) tender.tenderPeriod.startDate = tender.enquiryPeriod.endDate tender.tenderPeriod.endDate = calculate_tender_business_date( tender.tenderPeriod.startDate, self.request.content_configurator.tender_period, tender ) calculate_agreement_contracts_value_amount(self.request, tender) tender.lots[0].minimalStep = deepcopy(tender.lots[0].value) tender.lots[0].minimalStep.amount = round( self.request.content_configurator.minimal_step_percentage * tender.lots[0].value.amount, 2 ) calculate_tender_features(self.request, tender) else: self.LOGGER.info( "Switched tender {} to {}".format(tender.id, "draft.unsuccessful"), extra=context_unpack( self.request, {"MESSAGE_ID": "switched_tender_draft.unsuccessful"}, {"CAUSE": AGREEMENT_NOT_FOUND}, ), ) tender.unsuccessfulReason = [AGREEMENT_NOT_FOUND] elif self.request.authenticated_role == "tender_owner" and tender.status == "active.enquiries": validate_json_data_in_active_enquiries(self.request) apply_patch(self.request, save=False, data=self.request.validated["data"]) if "items" in self.request.validated["json_data"]: calculate_agreement_contracts_value_amount(self.request, tender) else: default_status = type(tender).fields["status"].default tender_status = tender.status apply_patch(self.request, save=False, src=self.request.validated["tender_src"]) if tender_status == default_status and tender.status == "draft.pending": if not tender.agreements or not tender.items: raise_operation_error( self.request, "Can't switch tender to (draft.pending) status without agreements or items." ) if tender_status == default_status and tender.status not in ("draft.pending", default_status): raise_operation_error( self.request, "Can't switch tender from ({}) to ({}) status.".format(default_status, tender.status) ) save_tender(self.request) self.LOGGER.info( "Updated tender {}".format(tender.id), extra=context_unpack(self.request, {"MESSAGE_ID": "tender_patch"}) ) return {"data": tender.serialize(tender.status)}
def validate_tenderPeriod(self, data, period): # data['_rev'] is None when tender was created just now if not data["_rev"] and calculate_tender_business_date(get_now(), -timedelta(minutes=10)) >= period.startDate: raise ValidationError(u"tenderPeriod.startDate should be in greater than current date") if period and calculate_tender_business_date(period.startDate, TENDER_PERIOD, data) > period.endDate: raise ValidationError(u"tenderPeriod should be greater than 15 days")
def test_set_auctionPeriod_nextday(self): now = datetime.now(TZ) response = self.api.patch_json( self.app.app.registry.api_url + 'tenders/' + self.tender_id, { 'data': { 'enquiryPeriod': { 'startDate': calculate_tender_business_date( now, timedelta(days=-15), tender=test_tender_data_quick, working_days=True).isoformat(), 'endDate': calculate_tender_business_date( now, timedelta(days=-9), tender=test_tender_data_quick, working_days=True).isoformat() }, 'tenderPeriod': { 'startDate': now.isoformat(), 'endDate': (now + timedelta(days=14 - now.weekday())).replace( hour=13).isoformat() } } }) response = self.app.get('/recheck/' + self.tender_id) self.assertEqual(response.status, '200 OK') self.assertNotEqual(response.json, None) response = self.api.get(self.app.app.registry.api_url + 'tenders/' + self.tender_id) tender = response.json['data'] self.assertEqual(tender['status'], 'active.tendering') response = self.app.get('/resync/' + self.tender_id) self.assertEqual(response.status, '200 OK') self.assertEqual(response.json, None) response = self.api.get(self.app.app.registry.api_url + 'tenders/' + self.tender_id) tender = response.json['data'] self.assertEqual(tender['status'], 'active.tendering') if self.initial_lots: self.assertIn('auctionPeriod', tender['lots'][0]) self.assertEqual( parse_date(tender['lots'][0]['auctionPeriod']['startDate'], TZ).weekday(), 1) else: self.assertIn('auctionPeriod', tender) self.assertEqual( parse_date(tender['auctionPeriod']['startDate'], TZ).weekday(), 1) response = self.app.get('/recheck/' + self.tender_id) self.assertEqual(response.status, '200 OK') self.assertNotEqual(response.json, None) self.app.app.registry.scheduler.start() response = self.app.get('/') self.assertEqual(response.status, '200 OK') self.assertIn('jobs', response.json) self.assertIn('recheck_{}'.format(self.tender_id), response.json['jobs']) self.assertGreaterEqual( parse_date(response.json['jobs']['recheck_{}'.format( self.tender_id)]).utctimetuple(), parse_date(tender['tenderPeriod']['endDate']).utctimetuple()) self.assertLessEqual( parse_date(response.json['jobs']['recheck_{}'.format( self.tender_id)]).utctimetuple(), (parse_date(tender['tenderPeriod']['endDate']) + timedelta(minutes=5)).utctimetuple())
def test_set_auctionPeriod_jobs(self): now = datetime.now(TZ) self.api.patch_json( self.app.app.registry.api_url + 'tenders/' + self.tender_id, { 'data': { 'enquiryPeriod': { 'startDate': calculate_tender_business_date( now, timedelta(days=-15), tender=test_tender_data_quick, working_days=True).isoformat(), 'endDate': calculate_tender_business_date( now, timedelta(days=-9), tender=test_tender_data_quick, working_days=True).isoformat() }, 'tenderPeriod': { 'startDate': calculate_tender_business_date( now, timedelta(days=-9), tender=test_tender_data_quick, working_days=True).isoformat(), 'endDate': calculate_tender_business_date( now, timedelta(days=1), tender=test_tender_data_quick, working_days=True).isoformat() } } }) for _ in range(100): self.app.app.registry.scheduler.start() response = self.app.get('/resync_all') self.assertEqual(response.status, '200 OK') self.assertNotEqual(response.json, None) response = self.app.get('/') self.app.app.registry.scheduler.shutdown() self.assertEqual(response.status, '200 OK') self.assertIn('jobs', response.json) self.assertEqual(len(response.json['jobs']), 2) if 'recheck_{}'.format(self.tender_id) in response.json['jobs']: break self.assertIn('recheck_{}'.format(self.tender_id), response.json['jobs']) response = self.app.get('/recheck/' + self.tender_id) self.assertEqual(response.status, '200 OK') self.assertNotEqual(response.json, None) for _ in range(10): self.app.app.registry.scheduler.start() self.app.get('/resync_all') self.app.app.registry.scheduler.shutdown() response = self.api.get(self.app.app.registry.api_url + 'tenders/' + self.tender_id) tender = response.json['data'] self.assertEqual(tender['status'], 'active.tendering') if self.initial_lots: self.assertIn('auctionPeriod', tender['lots'][0]) if 'startDate' in tender['lots'][0]['auctionPeriod']: break else: self.assertIn('auctionPeriod', tender) if 'startDate' in tender['auctionPeriod']: break else: response = self.app.get('/resync/' + self.tender_id) self.assertEqual(response.status, '200 OK') self.assertEqual(response.json, None) response = self.api.get(self.app.app.registry.api_url + 'tenders/' + self.tender_id) tender = response.json['data'] self.assertEqual(tender['status'], 'active.tendering') if self.initial_lots: self.assertIn('startDate', tender['lots'][0]['auctionPeriod']) else: self.assertIn('startDate', tender['auctionPeriod'])
def wrapper(): return PeriodStartEndRequired( {"startDate": get_now(), "endDate": calculate_tender_business_date(get_now(), tendering_duration)} )
def test_24hours_milestone(self): self.app.authorization = ("Basic", ("broker", "")) # try upload documents response = self.app.get("/tenders/{}".format(self.tender_id)) context = response.json["data"]["{}s".format(self.context_name)][0] bid_id = context.get("bid_id") or context.get( "bidID") # awards and qualifications developed on different days winner_token = self.initial_bids_tokens[bid_id] upload_allowed_by_default = response.json["data"][ "procurementMethodType"] == "aboveThresholdUA.defense" self.assert_upload_docs_status(bid_id, winner_token, success=upload_allowed_by_default) # invalid creation response = self.app.post_json("/tenders/{}/{}s/{}/milestones".format( self.tender_id, self.context_name, self.context_id), {"data": {}}, status=403) self.assertEqual( response.json, { "status": "error", "errors": [{ "location": "url", "name": "permission", "description": "Forbidden" }] }) response = self.app.post_json( "/tenders/{}/{}s/{}/milestones?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token), {"data": { "code": "alp" }}, status=403) if get_now() > RELEASE_2020_04_19: self.assertEqual( response.json, { "status": "error", "errors": [{ "description": "The only allowed milestone code is '24h'", "location": "body", "name": "data" }] }) else: self.assertEqual( response.json, { "status": "error", "errors": [{ "location": "body", "name": "data", "description": "Forbidden" }] }) return # valid creation request_data = { "code": "24h", "description": "One ring to bring them all and in the darkness bind them", "dueDate": (get_now() + timedelta(days=10)).isoformat() } response = self.app.post_json( "/tenders/{}/{}s/{}/milestones?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token), {"data": request_data}, ) self.assertEqual(response.status, "201 Created") created_milestone = response.json["data"] # get milestone from tender response = self.app.get("/tenders/{}".format(self.tender_id)) tender_data = response.json["data"] context = tender_data["{}s".format(self.context_name)][0] public_milestone = context["milestones"][0] self.assertEqual(created_milestone, public_milestone) self.assertEqual(set(created_milestone.keys()), { "id", "date", "code", "description", "dueDate", }) self.assertEqual(created_milestone["code"], request_data["code"]) self.assertEqual(created_milestone["description"], request_data["description"]) self.assertNotEqual(created_milestone["dueDate"], request_data["dueDate"]) expected_date = calculate_tender_business_date( parse_date(created_milestone["date"]), timedelta(hours=24), tender_data) self.assertEqual(created_milestone["dueDate"], expected_date.isoformat()) # get milestone by its direct link response = self.app.get("/tenders/{}/{}s/{}/milestones/{}".format( self.tender_id, self.context_name, self.context_id, created_milestone["id"])) direct_milestone = response.json["data"] self.assertEqual(created_milestone, direct_milestone) # can't post another response = self.app.post_json( "/tenders/{}/{}s/{}/milestones?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token), {"data": request_data}, status=422) self.assertEqual( response.json, { "status": "error", "errors": [{ "description": [{ "milestones": ["There can be only one '24h' milestone"] }], "location": "body", "name": "{}s".format(self.context_name) }] }) # can't update status of context until dueDate activation_data = { "status": "active", "qualified": True, "eligible": True } response = self.app.patch_json( "/tenders/{}/{}s/{}?acc_token={}".format(self.tender_id, self.context_name, self.context_id, self.tender_token), {"data": activation_data}, status=403) self.assertEqual( response.json, { "status": "error", "errors": [{ "description": "Can't change status to 'active' " "until milestone.dueDate: {}".format( created_milestone["dueDate"]), "location": "body", "name": "data" }] }) # try upload documents self.assert_upload_docs_status(bid_id, winner_token) # wait until milestone dueDate ends with patch("openprocurement.tender.core.validation.get_now", lambda: get_now() + timedelta(hours=24)): self.assert_upload_docs_status(bid_id, winner_token, success=upload_allowed_by_default) response = self.app.patch_json( "/tenders/{}/{}s/{}?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token), {"data": activation_data}, status=200) self.assertEqual(response.json["data"]["status"], "active") # check appending milestone at active qualification status # remove milestone to skip "only one" validator tender = self.db.get(self.tender_id) context = tender["{}s".format(self.context_name)][0] context["milestones"] = [] self.db.save(tender) response = self.app.post_json( "/tenders/{}/{}s/{}/milestones?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token), {"data": request_data}, status=403) self.assertEqual( response.json, { "status": "error", "errors": [{ "description": "Not allowed in current 'active' {} status".format( self.context_name), "location": "body", "name": "data" }] })