class Complaint(BaseComplaint): author = ModelType(Organization, required=True) documents = ListType(ModelType(Document), default=list())
class Complaint(Model): class Options: roles = { 'create': whitelist('author', 'title', 'description', 'status', 'relatedLot'), 'draft': whitelist('author', 'title', 'description', 'status'), 'cancellation': whitelist('cancellationReason', 'status'), 'satisfy': whitelist('satisfied', 'status'), 'answer': whitelist('resolution', 'resolutionType', 'status', 'tendererAction'), 'action': whitelist('tendererAction'), 'review': whitelist('decision', 'status'), 'view': view_bid_role, 'view_claim': (blacklist('author') + view_bid_role), 'active.enquiries': view_bid_role, 'active.tendering': view_bid_role, 'active.auction': view_bid_role, 'active.qualification': view_bid_role, 'active.awarded': view_bid_role, 'complete': view_bid_role, 'unsuccessful': view_bid_role, 'cancelled': view_bid_role, } # system id = MD5Type(required=True, default=lambda: uuid4().hex) complaintID = StringType() date = IsoDateTimeType(default=get_now) # autogenerated date of posting status = StringType(choices=[ 'draft', 'claim', 'answered', 'pending', 'invalid', 'resolved', 'declined', 'cancelled' ], default='draft') documents = ListType(ModelType(Document), default=list()) type = StringType( choices=['claim', 'complaint'], default='claim' ) # 'complaint' if status in ['pending'] or 'claim' if status in ['draft', 'claim', 'answered'] owner_token = StringType() owner = StringType() relatedLot = MD5Type() # complainant author = ModelType(Organization, required=True) # author of claim title = StringType(required=True) # title of the claim description = StringType() # description of the claim dateSubmitted = IsoDateTimeType() # tender owner resolution = StringType() resolutionType = StringType(choices=['invalid', 'resolved', 'declined']) dateAnswered = IsoDateTimeType() tendererAction = StringType() tendererActionDate = IsoDateTimeType() # complainant satisfied = BooleanType() dateEscalated = IsoDateTimeType() # reviewer decision = StringType() dateDecision = IsoDateTimeType() # complainant cancellationReason = StringType() dateCanceled = IsoDateTimeType() def serialize(self, role=None, context=None): if role == 'view' and self.type == 'claim' and get_tender( self).status in ['active.enquiries', 'active.tendering']: role = 'view_claim' return super(Complaint, self).serialize(role=role, context=context) def get_role(self): root = self.__parent__ while root.__parent__ is not None: root = root.__parent__ request = root.request data = request.json_body['data'] if request.authenticated_role == 'complaint_owner' and data.get( 'status', self.status) == 'cancelled': role = 'cancellation' elif request.authenticated_role == 'complaint_owner' and self.status == 'draft': role = 'draft' elif request.authenticated_role == 'tender_owner' and self.status == 'claim': role = 'answer' elif request.authenticated_role == 'tender_owner' and self.status == 'pending': role = 'action' elif request.authenticated_role == 'complaint_owner' and self.status == 'answered': role = 'satisfy' elif request.authenticated_role == 'reviewers' and self.status == 'pending': role = 'review' else: role = 'invalid' return role def __local_roles__(self): return dict([('{}_{}'.format(self.owner, self.owner_token), 'complaint_owner')]) def __acl__(self): return [ (Allow, 'g:reviewers', 'edit_complaint'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_complaint'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'upload_complaint_documents'), ] def validate_resolutionType(self, data, resolutionType): if not resolutionType and data.get('status') == 'answered': raise ValidationError(u'This field is required.') def validate_cancellationReason(self, data, cancellationReason): if not cancellationReason and data.get('status') == 'cancelled': raise ValidationError(u'This field is required.') def validate_relatedLot(self, data, relatedLot): if relatedLot and isinstance( data['__parent__'], Model) and relatedLot not in [ i.id for i in get_tender(data['__parent__']).lots ]: raise ValidationError(u"relatedLot should be one of lots")
class Tender(BaseTender): """Data regarding tender process - publicly inviting prospective contractors to submit bids for evaluation and selecting a winner or winners. """ class Options: roles = { 'plain': plain_role, 'create': create_role, 'edit': edit_role, 'edit_draft': draft_role, 'edit_active': edit_role, 'edit_active.awarded': whitelist(), 'edit_complete': whitelist(), 'edit_unsuccessful': whitelist(), 'edit_cancelled': whitelist(), 'view': view_role, 'listing': listing_role, 'draft': enquiries_role, 'active': enquiries_role, 'active.awarded': view_role, 'complete': view_role, 'unsuccessful': view_role, 'cancelled': view_role, 'Administrator': Administrator_role, 'chronograph': chronograph_role, # remove after chronograph fix 'chronograph_view': chronograph_view_role, # remove after chronograph fix 'default': schematics_default_role, 'contracting': whitelist('doc_id', 'owner'), } items = ListType(ModelType(Item), required=True, min_size=1, validators=[validate_cpv_group, validate_items_uniq]) # The goods and services to be purchased, broken into line items wherever possible. Items should not be duplicated, but a quantity of 2 specified instead. value = ModelType(Value, required=True) # The total estimated value of the procurement. procurementMethod = StringType(choices=['open', 'selective', 'limited'], default='limited') # Specify tendering method as per GPA definitions of Open, Selective, Limited (http://www.wto.org/english/docs_e/legal_e/rev-gpr-94_01_e.htm) procurementMethodType = StringType(default="reporting") procuringEntity = ModelType(ProcuringEntity, required=True) # The entity managing the procurement, which may be different from the buyer who is paying / using the items being procured. awards = ListType(ModelType(Award), default=list()) contracts = ListType(ModelType(Contract), default=list()) status = StringType(choices=['draft', 'active', 'complete', 'cancelled', 'unsuccessful'], default='active') mode = StringType(choices=['test']) cancellations = ListType(ModelType(Cancellation), default=list()) create_accreditation = 1 edit_accreditation = 2 procuring_entity_kinds = ['general', 'special', 'defense', 'other'] block_complaint_status = [] # BBB No complaints in reporting __parent__ = None def get_role(self): root = self.__parent__ request = root.request if request.authenticated_role == 'Administrator': role = 'Administrator' elif request.authenticated_role == 'chronograph': role = 'chronograph' elif request.authenticated_role == 'contracting': role = 'contracting' else: role = 'edit_{}'.format(request.context.status) return role def __acl__(self): return [ (Allow, 'g:brokers', 'create_award_complaint'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_tender'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'upload_tender_documents'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_complaint'), ] def validate_value(self, data, value): if value.valueAddedTaxIncluded is not False: raise ValidationError(u"Currently, only procedures with VAT excluded are supported")
class Award(BaseTenderEU.awards.model_class): items = ListType(ModelType(ItemStage2EU))
class Contract(BaseTenderUA.contracts.model_class): items = ListType(ModelType(ItemStage2UA))
class Tender(BaseTender): """ OpenEU tender model """ class Options: namespace = "Tender" _parent_roles = BaseTender.Options.roles _edit_role = _parent_roles["edit"] - whitelist("enquiryPeriod") _read_fields = whitelist("qualifications", "qualificationPeriod", "complaintPeriod") _tendering_role = _parent_roles["active.tendering"] + _read_fields + whitelist("tender_enquiryPeriod") _view_role = _parent_roles["view"] + _read_fields _pre_qualifications_role = _view_role _all_forbidden = whitelist() roles = { "create": _parent_roles["create"] - whitelist("enquiryPeriod"), "edit": _edit_role, "edit_draft": _edit_role + whitelist("status"), "edit_active.tendering": _edit_role, "edit_active.pre-qualification": whitelist("status"), "edit_active.pre-qualification.stand-still": _all_forbidden, "edit_active.auction": _all_forbidden, "edit_active.qualification": _all_forbidden, "edit_active.awarded": _all_forbidden, "edit_complete": _all_forbidden, "edit_unsuccessful": _all_forbidden, "edit_cancelled": _all_forbidden, "active.pre-qualification": _pre_qualifications_role, "active.pre-qualification.stand-still": _pre_qualifications_role, "active.auction": _pre_qualifications_role, "draft": _tendering_role, "active.tendering": _tendering_role, "active.qualification": _view_role, "active.awarded": _view_role, "complete": _view_role, "unsuccessful": _view_role, "cancelled": _view_role, "view": _view_role, "auction_view": _parent_roles["auction_view"], "auction_post": _parent_roles["auction_post"], "auction_patch": _parent_roles["auction_patch"], "chronograph": _parent_roles["chronograph"], "chronograph_view": _parent_roles["chronograph_view"], "Administrator": _parent_roles["Administrator"], "default": _parent_roles["default"], "plain": _parent_roles["plain"], "contracting": _parent_roles["contracting"], "listing": _parent_roles["listing"], } procurementMethodType = StringType(default="aboveThresholdEU") title_en = StringType(required=True, min_length=1) enquiryPeriod = ModelType(EnquiryPeriod, required=False) tenderPeriod = ModelType(PeriodStartEndRequired, required=True) auctionPeriod = ModelType(TenderAuctionPeriod, default={}) documents = ListType( ModelType(EUDocument, required=True), default=list() ) # All documents and attachments related to the tender. items = ListType( ModelType(Item, required=True), required=True, min_size=1, validators=[validate_cpv_group, validate_items_uniq, validate_classification_id], ) # The goods and services to be purchased, broken into line items wherever possible. Items should not be duplicated, but a quantity of 2 specified instead. complaints = ListType(ComplaintModelType(Complaint, required=True), default=list()) contracts = ListType(ModelType(Contract, required=True), default=list()) cancellations = ListType(ModelType(Cancellation, required=True), default=list()) awards = ListType(ModelType(Award, required=True), default=list()) procuringEntity = ModelType( ProcuringEntity, required=True ) # The entity managing the procurement, which may be different from the buyer who is paying / using the items being procured. bids = SifterListType( BidModelType(Bid), default=list(), filter_by="status", filter_in_values=["invalid", "invalid.pre-qualification", "deleted"], ) # A list of all the companies who entered submissions for the tender. qualifications = ListType(ModelType(Qualification, required=True), default=list()) qualificationPeriod = ModelType(Period) lots = ListType(ModelType(Lot, required=True), default=list(), validators=[validate_lots_uniq]) status = StringType( choices=[ "draft", "active.tendering", "active.pre-qualification", "active.pre-qualification.stand-still", "active.auction", "active.qualification", "active.awarded", "complete", "cancelled", "unsuccessful", ], default="active.tendering", ) create_accreditations = (ACCR_3, ACCR_5) central_accreditations = (ACCR_5,) edit_accreditations = (ACCR_4,) procuring_entity_kinds = ["authority", "central", "defense", "general", "social", "special"] block_tender_complaint_status = OpenUATender.block_tender_complaint_status block_complaint_status = OpenUATender.block_complaint_status def __acl__(self): acl = [ (Allow, "{}_{}".format(i.owner, i.owner_token), "create_qualification_complaint") for i in self.bids if i.status in ["active", "unsuccessful"] ] acl.extend( [ (Allow, "{}_{}".format(i.owner, i.owner_token), "create_award_complaint") for i in self.bids if i.status == "active" ] ) acl.extend( [ (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_complaint"), (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_contract"), (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_contract_documents"), (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_qualification_documents"), ] ) self._acl_cancellation_complaint(acl) return acl def validate_enquiryPeriod(self, data, period): # for deactivate validation to enquiryPeriod from parent class return @serializable(serialized_name="enquiryPeriod", type=ModelType(EnquiryPeriod)) def tender_enquiryPeriod(self): end_date = calculate_tender_business_date(self.tenderPeriod.endDate, -QUESTIONS_STAND_STILL, self) clarifications_until = calculate_clarif_business_date(end_date, ENQUIRY_STAND_STILL_TIME, self, True) return EnquiryPeriod( dict( startDate=self.tenderPeriod.startDate, endDate=end_date, invalidationDate=self.enquiryPeriod and self.enquiryPeriod.invalidationDate, clarificationsUntil=clarifications_until, ) ) @serializable(type=ModelType(Period)) def complaintPeriod(self): end_date = calculate_complaint_business_date(self.tenderPeriod.endDate, -COMPLAINT_SUBMIT_TIME, self) return Period(dict(startDate=self.tenderPeriod.startDate, endDate=end_date)) @serializable(serialize_when_none=False) def next_check(self): now = get_now() checks = [] extend_next_check_by_complaint_period_ends(self, checks) if cancellation_block_tender(self): return min(checks).isoformat() if checks else None if ( self.status == "active.tendering" and self.tenderPeriod.endDate and not has_unanswered_complaints(self) and not has_unanswered_questions(self) ): checks.append(self.tenderPeriod.endDate.astimezone(TZ)) elif ( self.status == "active.pre-qualification.stand-still" and self.qualificationPeriod and self.qualificationPeriod.endDate ): active_lots = [lot.id for lot in self.lots if lot.status == "active"] if self.lots else [None] if not any( [ i.status in self.block_complaint_status for q in self.qualifications for i in q.complaints if q.lotID in active_lots ] ): checks.append(self.qualificationPeriod.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)) else: auction_end_time = calc_auction_end_time( self.numberOfBids, self.auctionPeriod.startDate ).astimezone(TZ) if now < auction_end_time: checks.append(auction_end_time) 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)) else: auction_end_time = calc_auction_end_time( lot.numberOfBids, lot.auctionPeriod.startDate ).astimezone(TZ) if now < auction_end_time: checks.append(auction_end_time) 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 and 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 and 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 and self.status.startswith("active"): 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) return min(checks).isoformat() if checks else None def validate_tenderPeriod(self, data, period): if is_new_created(data): _validate_tender_period_start_date(data, period) _validate_tender_period_duration(data, period, TENDERING_DURATION) @serializable def numberOfBids(self): """A property that is serialized by schematics exports.""" return len([bid for bid in self.bids if bid.status in ("active", "pending")]) def check_auction_time(self): if check_auction_period(self.auctionPeriod, self): self.auctionPeriod.startDate = None for lot in self.lots: if check_auction_period(lot.auctionPeriod, self): lot.auctionPeriod.startDate = None def invalidate_bids_data(self): self.check_auction_time() self.enquiryPeriod.invalidationDate = get_now() for bid in self.bids: if bid.status not in ["deleted", "draft"]: bid.status = "invalid"
class Feature(BaseFeature): enum = ListType(ModelType(FeatureValue), default=list(), min_size=1, validators=[validate_values_uniq])
class Organization(BaseOrganization): identifier = ModelType(Identifier, required=True) additionalIdentifiers = ListType(ModelType(Identifier))
class ProcuringEntity(BaseProcuringEntity): identifier = ModelType(Identifier, required=True) additionalIdentifiers = ListType(ModelType(Identifier))
class Auction(BaseAuction): """Data regarding auction process - publicly inviting prospective contractors to submit bids for evaluation and selecting a winner or winners.""" class Options: roles = { 'create': create_role, 'edit_active.tendering': (blacklist('enquiryPeriod', 'tenderPeriod', 'auction_value', 'auction_minimalStep', 'auction_guarantee', 'eligibilityCriteria', 'eligibilityCriteria_en', 'eligibilityCriteria_ru', 'title', 'title_ru', 'title_en', 'dgfID', 'tenderAttempts', 'minNumberOfQualifiedBids') + edit_role), 'Administrator': (whitelist('value', 'minimalStep', 'guarantee') + Administrator_role), } awards = ListType(ModelType(Award), default=list()) bids = ListType(ModelType(Bid), default=list( )) # A list of all the companies who entered submissions for the auction. cancellations = ListType(ModelType(Cancellation), default=list()) complaints = ListType(ModelType(Complaint), default=list()) contracts = ListType(ModelType(Contract), default=list()) dgfID = StringType() documents = ListType(ModelType(Document), default=list( )) # All documents and attachments related to the auction. enquiryPeriod = ModelType( Period ) # The period during which enquiries may be made and will be answered. tenderPeriod = ModelType( Period ) # The period when the auction is open for submissions. The end date is the closing date for auction submissions. tenderAttempts = IntType(choices=[1, 2, 3, 4]) auctionPeriod = ModelType(AuctionAuctionPeriod, required=True, default={}) procurementMethodType = StringType(default="dgfOtherAssets") procuringEntity = ModelType(ProcuringEntity, required=True) status = StringType(choices=[ 'draft', 'active.tendering', 'active.auction', 'active.qualification', 'active.awarded', 'complete', 'cancelled', 'unsuccessful' ], default='active.tendering') questions = ListType(ModelType(Question), default=list()) features = ListType( ModelType(Feature), validators=[validate_features_uniq, validate_not_available]) lots = ListType(ModelType(Lot), default=list(), validators=[validate_lots_uniq, validate_not_available]) items = ListType(ModelType(Item), required=True, min_size=1, validators=[validate_items_uniq]) minNumberOfQualifiedBids = IntType(choices=[1, 2]) def initialize(self): if not self.enquiryPeriod: self.enquiryPeriod = type(self).enquiryPeriod.model_class() if not self.tenderPeriod: self.tenderPeriod = type(self).tenderPeriod.model_class() now = get_now() self.tenderPeriod.startDate = self.enquiryPeriod.startDate = now pause_between_periods = self.auctionPeriod.startDate - ( self.auctionPeriod.startDate.replace( hour=20, minute=0, second=0, microsecond=0) - timedelta(days=1)) self.enquiryPeriod.endDate = self.tenderPeriod.endDate = calculate_business_date( self.auctionPeriod.startDate, -pause_between_periods, self) self.auctionPeriod.startDate = None self.auctionPeriod.endDate = None self.date = now if self.lots: for lot in self.lots: lot.date = now def validate_tenderPeriod(self, data, period): pass def validate_value(self, data, value): if value.currency != u'UAH': raise ValidationError(u"currency should be only UAH") def validate_dgfID(self, data, dgfID): if not dgfID: if (data.get('revisions')[0].date if data.get('revisions') else get_now()) > DGF_ID_REQUIRED_FROM: raise ValidationError(u'This field is required.') @serializable(serialize_when_none=False) def next_check(self): now = get_now() checks = [] if self.status == 'active.tendering' and self.tenderPeriod 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.api.utils import calculate_business_date for complaint in self.complaints: if complaint.status == 'claim' and complaint.dateSubmitted: checks.append( calculate_business_date(complaint.dateSubmitted, COMPLAINT_STAND_STILL_TIME, self)) elif complaint.status == 'answered' and complaint.dateAnswered: checks.append( calculate_business_date(complaint.dateAnswered, COMPLAINT_STAND_STILL_TIME, self)) for award in self.awards: for complaint in award.complaints: if complaint.status == 'claim' and complaint.dateSubmitted: checks.append( calculate_business_date( complaint.dateSubmitted, COMPLAINT_STAND_STILL_TIME, self)) elif complaint.status == 'answered' and complaint.dateAnswered: checks.append( calculate_business_date( complaint.dateAnswered, COMPLAINT_STAND_STILL_TIME, self)) return min(checks).isoformat() if checks else None
class FinantialOrganization(BaseOrganization): identifier = ModelType(Identifier, required=True) additionalIdentifiers = ListType(ModelType(Identifier), required=True, validators=[validate_ua_fin])
class Award(BaseAward): suppliers = ListType(ModelType(Organization), min_size=1, max_size=1) complaints = ListType(ModelType(Complaint), default=list()) documents = ListType(ModelType(Document), default=list()) items = ListType(ModelType(Item))
class Contract(BaseContract): items = ListType(ModelType(Item)) suppliers = ListType(ModelType(Organization), min_size=1, max_size=1) complaints = ListType(ModelType(Complaint), default=list()) documents = ListType(ModelType(Document), default=list())
class Cancellation(BaseCancellation): documents = ListType(ModelType(Document), default=list())
class CFASelectionUATender(BaseTender): """Data regarding tender process - publicly inviting prospective contractors to submit bids for evaluation and selecting a winner or winners. """ class Options: namespace = 'Tender' roles = RolesFromCsv('Tender.csv', relative_to=__file__) items = ListType( ModelType(Item), min_size=1, validators=[validate_items_uniq] ) # The goods and services to be purchased, broken into line items wherever possible. Items should not be duplicated, but a quantity of 2 specified instead. value = ModelType(Value) # The total estimated value of the procurement. enquiryPeriod = ModelType( PeriodEndRequired, required=False ) # The period during which enquiries may be made and will be answered. tenderPeriod = ModelType( PeriodEndRequired, required=False ) # The period when the tender is open for submissions. The end date is the closing date for tender submissions. hasEnquiries = BooleanType( ) # A Yes/No field as to whether enquiries were part of tender process. awardPeriod = ModelType( Period ) # The date or period on which an award is anticipated to be made. numberOfBidders = IntType( ) # The number of unique tenderers who participated in the tender bids = ListType(ModelType(Bid), default=list( )) # A list of all the companies who entered submissions for the tender. procuringEntity = ModelType( ProcuringEntity, required=True ) # The entity managing the procurement, which may be different from the buyer who is paying / using the items being procured. awards = ListType(ModelType(Award), default=list()) contracts = ListType(ModelType(Contract), default=list()) auctionPeriod = ModelType(TenderAuctionPeriod, default={}) minimalStep = ModelType(Value, required=False) auctionUrl = URLType() cancellations = ListType(ModelType(Cancellation), default=list()) features = ListType(ModelType(Feature), validators=[validate_features_uniq]) lots = ListType(ModelType(Lot), default=list(), validators=[validate_lots_uniq], min_size=1, max_size=1) guarantee = ModelType(Guarantee) status = StringType(choices=[ 'draft', 'draft.pending', 'draft.unsuccessful', 'active.enquiries', 'active.tendering', 'active.auction', 'active.qualification', 'active.awarded', 'complete', 'cancelled', 'unsuccessful' ], default='draft') # TODO Refactoring status agreements = ListType(ModelType(Agreement), default=list(), min_size=1, max_size=1) procurementMethod = StringType(choices=['open', 'selective', 'limited'], default='selective') procurementMethodType = StringType( default="closeFrameworkAgreementSelectionUA") unsuccessfulReason = ListType(StringType, serialize_when_none=False) procuring_entity_kinds = ['general', 'special', 'defense', 'other'] def get_role(self): root = self.__parent__ request = root.request if request.authenticated_role == 'Administrator': role = 'Administrator' elif request.authenticated_role == 'chronograph': role = 'chronograph' elif request.authenticated_role == 'auction': role = 'auction_{}'.format(request.method.lower()) elif request.authenticated_role == 'contracting': role = 'contracting' elif request.authenticated_role == 'agreement_selection': role = 'edit_{}'.format(request.authenticated_role) else: role = 'edit_{}'.format(request.context.status) return role def __acl__(self): acl = [(Allow, '{}_{}'.format(i.owner, i.owner_token), 'create_award_complaint') for i in self.bids] acl.extend([ (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_tender'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'upload_tender_documents'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_complaint'), (Allow, 'g:agreement_selection', 'edit_agreement_selection'), (Allow, 'g:agreement_selection', 'edit_tender'), ]) return acl def __local_roles__(self): roles = dict([('{}_{}'.format(self.owner, self.owner_token), 'tender_owner')]) for i in self.bids: roles['{}_{}'.format(i.owner, i.owner_token)] = 'bid_owner' return roles
class Award(BaseEUAward): """ESCO award model""" value = ModelType(BaseESCOValue) items = ListType(ModelType(Item, required=True))
class Bid(BidResponsesMixin, BaseBid): class Options: roles = { "Administrator": Administrator_bid_role, "embedded": view_bid_role, "view": view_bid_role, "create": whitelist( "value", "tenderers", "parameters", "lotValues", "status", "selfQualified", "selfEligible", "subcontractingDetails", "documents", "financialDocuments", "eligibilityDocuments", "qualificationDocuments", "requirementResponses", ), "edit": whitelist( "value", "tenderers", "parameters", "lotValues", "status", "subcontractingDetails", "requirementResponses", ), "auction_view": whitelist("value", "lotValues", "id", "date", "parameters", "participationUrl", "status"), "auction_post": whitelist("value", "lotValues", "id", "date"), "auction_patch": whitelist("id", "lotValues", "participationUrl"), "active.enquiries": whitelist(), "active.tendering": whitelist(), "active.pre-qualification": whitelist( "id", "status", "documents", "eligibilityDocuments", "tenderers", "requirementResponses"), "active.pre-qualification.stand-still": whitelist( "id", "status", "documents", "eligibilityDocuments", "tenderers", "requirementResponses"), "active.auction": whitelist("id", "status", "documents", "eligibilityDocuments", "tenderers"), "active.qualification": view_bid_role, "active.awarded": view_bid_role, "complete": view_bid_role, "unsuccessful": view_bid_role, "bid.unsuccessful": whitelist( "id", "status", "tenderers", "documents", "eligibilityDocuments", "parameters", "selfQualified", "selfEligible", "subcontractingDetails", "requirementResponses", ), "cancelled": view_bid_role, "invalid": whitelist("id", "status"), "invalid.pre-qualification": whitelist( "id", "status", "documents", "eligibilityDocuments", "tenderers", "requirementResponses"), "deleted": whitelist("id", "status"), } documents = ListType(ConfidentialDocumentModelType(EUConfidentialDocument, required=True), default=list()) financialDocuments = ListType(ConfidentialDocumentModelType(EUConfidentialDocument, required=True), default=list()) eligibilityDocuments = ListType(ConfidentialDocumentModelType(EUConfidentialDocument, required=True), default=list()) qualificationDocuments = ListType(ConfidentialDocumentModelType(EUConfidentialDocument, required=True), default=list()) lotValues = ListType(ModelType(LotValue, required=True), default=list()) selfQualified = BooleanType(required=True, choices=[True]) selfEligible = BooleanType(choices=[True]) subcontractingDetails = StringType() parameters = ListType(ModelType(Parameter, required=True), default=list(), validators=[validate_parameters_uniq]) status = StringType( choices=["draft", "pending", "active", "invalid", "invalid.pre-qualification", "unsuccessful", "deleted"], default="pending", ) def serialize(self, role=None): if role and role != "create" and self.status in ["invalid", "invalid.pre-qualification", "deleted"]: role = self.status elif role and role != "create" and self.status == "unsuccessful": role = "bid.unsuccessful" return super(Bid, self).serialize(role) @serializable(serialized_name="status") def serialize_status(self): if self.status in [ "draft", "invalid", "invalid.pre-qualification", "unsuccessful", "deleted", ] or self.__parent__.status in ["active.tendering", "cancelled"]: return self.status if self.__parent__.lots: active_lots = [lot.id for lot in self.__parent__.lots if lot.status in ("active", "complete")] if not self.lotValues: return "invalid" elif [i.relatedLot for i in self.lotValues if i.status == "pending" and i.relatedLot in active_lots]: return "pending" elif [i.relatedLot for i in self.lotValues if i.status == "active" and i.relatedLot in active_lots]: return "active" else: return "unsuccessful" return self.status @bids_validation_wrapper def validate_value(self, data, value): BaseBid._validator_functions["value"](self, data, value) @bids_validation_wrapper def validate_lotValues(self, data, lotValues): BaseBid._validator_functions["lotValues"](self, data, lotValues) @bids_validation_wrapper def validate_participationUrl(self, data, participationUrl): BaseBid._validator_functions["participationUrl"](self, data, participationUrl) @bids_validation_wrapper def validate_parameters(self, data, parameters): BaseBid._validator_functions["parameters"](self, data, parameters)
class Tender(BaseTender): """ ESCO Tender model """ class Options: namespace = "Tender" _parent_roles = BaseTender.Options.roles _all_forbidden = whitelist() _serializable_fields = whitelist( "tender_enquiryPeriod", "complaintPeriod", "next_check", "tender_minValue", "tender_guarantee", "tender_minimalStepPercentage", "tender_yearlyPaymentsPercentageRange", "tender_noticePublicationDate", ) _edit_fields = _serializable_fields + whitelist( "procuringEntity", "tenderPeriod", "NBUdiscountRate", "items", "features", "yearlyPaymentsPercentageRange", "fundingKind", # fields below are not covered "hasEnquiries", "numberOfBidders", "minimalStepPercentage", ) _read_only_fields = whitelist( "awards", "lots", "contracts", "auctionPeriod", "complaints", # fields below are not covered "auctionUrl", "awardPeriod", "questions", "cancellations", "qualifications", "qualificationPeriod", ) _tendering_role = _parent_roles[ "view"] + _edit_fields + _read_only_fields _view_role = _tendering_role + whitelist("bids", "numberOfBids") _pre_qualifications_role = _view_role _esco_edit_forbidden = whitelist( "minValue", "tender_minValue", "minimalStep", "tender_minimalStep", "noticePublicationDate", "tender_noticePublicationDate", ) _edit_role = _parent_roles["edit"] + _edit_fields - _esco_edit_forbidden roles = { "create": _parent_roles["create"] + _edit_fields + whitelist("lots") - _esco_edit_forbidden, "edit": _edit_role, "edit_draft": _edit_role, "edit_active.tendering": _edit_role, "edit_active.pre-qualification": whitelist("status"), "edit_active.pre-qualification.stand-still": _all_forbidden, "edit_active.auction": _all_forbidden, "edit_active.qualification": _all_forbidden, "edit_active.awarded": _all_forbidden, "edit_complete": _all_forbidden, "edit_unsuccessful": _all_forbidden, "edit_cancelled": _all_forbidden, "draft": _tendering_role, "active.tendering": _tendering_role, "active.qualification": _view_role, "active.awarded": _view_role, "complete": _view_role, "unsuccessful": _view_role, "cancelled": _view_role, "view": _view_role, "active.pre-qualification": _pre_qualifications_role, "active.pre-qualification.stand-still": _pre_qualifications_role, "active.auction": _pre_qualifications_role, "auction_view": _parent_roles["auction_view"] + whitelist( "NBUdiscountRate", "minimalStepPercentage", "yearlyPaymentsPercentageRange", "fundingKind", "procurementMethodType", "noticePublicationDate", ), "auction_post": _parent_roles["auction_post"], "auction_patch": _parent_roles["auction_patch"], "chronograph": _parent_roles["chronograph"], "chronograph_view": _parent_roles["chronograph_view"], "Administrator": _parent_roles["Administrator"] + whitelist("yearlyPaymentsPercentageRange"), "plain": _parent_roles["plain"], "listing": _parent_roles["listing"], "contracting": _parent_roles["contracting"], "default": _parent_roles["default"], } procurementMethodType = StringType(default="esco") title_en = StringType(required=True, min_length=1) items = ListType( ModelType(Item, required=True), required=True, min_size=1, validators=[validate_cpv_group, validate_items_uniq] ) # The goods and services to be purchased, broken into line items wherever possible. Items should not be duplicated, but a quantity of 2 specified instead. minValue = ModelType(Value, required=False, default={ "amount": 0, "currency": "UAH", "valueAddedTaxIncluded": True }) # The total estimated value of the procurement. enquiryPeriod = ModelType(EnquiryPeriod, required=False) tenderPeriod = ModelType(PeriodStartEndRequired, required=True) auctionPeriod = ModelType(TenderAuctionPeriod, default={}) hasEnquiries = BooleanType( ) # A Yes/No field as to whether enquiries were part of tender process. awardCriteria = StringType(default="ratedCriteria") awardPeriod = ModelType( Period ) # The date or period on which an award is anticipated to be made. numberOfBidders = IntType( ) # The number of unique tenderers who participated in the tender bids = SifterListType( BidModelType(Bid), default=list(), filter_by="status", filter_in_values=["invalid", "invalid.pre-qualification", "deleted"], ) # A list of all the companies who entered submissions for the tender. procuringEntity = ModelType( ProcuringEntity, required=True ) # The entity managing the procurement, which may be different from the buyer who is paying / using the items being procured. awards = ListType(ModelType(Award, required=True), default=list()) contracts = ListType(ModelType(Contract, required=True), default=list()) minimalStep = ModelType( Value, required=False ) # Not required, blocked for create/edit, since we have minimalStepPercentage in esco minimalStepPercentage = DecimalType(required=True, min_value=Decimal("0.005"), max_value=Decimal("0.03"), precision=-5) questions = ListType(ModelType(Question, required=True), default=list()) complaints = ListType(ComplaintModelType(Complaint, required=True), default=list()) auctionUrl = URLType() cancellations = ListType(ModelType(Cancellation, required=True), default=list()) features = ListType(ModelType(Feature, required=True), validators=[validate_features_uniq]) lots = ListType(ModelType(Lot, required=True), default=list(), validators=[validate_lots_uniq]) guarantee = ModelType(Guarantee) documents = ListType( ModelType(Document, required=True), default=list()) # All documents and attachments related to the tender. qualifications = ListType(ModelType(Qualification, required=True), default=list()) qualificationPeriod = ModelType(Period) status = StringType( choices=[ "draft", "active.tendering", "active.pre-qualification", "active.pre-qualification.stand-still", "active.auction", "active.qualification", "active.awarded", "complete", "cancelled", "unsuccessful", ], default="active.tendering", ) NBUdiscountRate = DecimalType(required=True, min_value=Decimal("0"), max_value=Decimal("0.99"), precision=-5) fundingKind = StringType(choices=["budget", "other"], required=True, default="other") yearlyPaymentsPercentageRange = DecimalType(required=True, default=Decimal("0.8"), min_value=Decimal("0"), max_value=Decimal("1"), precision=-5) noticePublicationDate = IsoDateTimeType() create_accreditations = (ACCR_3, ACCR_5) central_accreditations = (ACCR_5, ) edit_accreditations = (ACCR_4, ) special_fields = ["fundingKind", "yearlyPaymentsPercentageRange"] procuring_entity_kinds = ["general", "special", "defense", "central"] block_tender_complaint_status = OpenUATender.block_tender_complaint_status block_complaint_status = OpenUATender.block_complaint_status def import_data(self, raw_data, **kw): """ Converts and imports the raw data into the instance of the model according to the fields in the model. :param raw_data: The data to be imported. """ data = self.convert(raw_data, **kw) del_keys = [ k for k in data.keys() if data[k] == self.__class__.fields[k].default or data[k] == getattr(self, k) ] for k in del_keys: if k in self.special_fields: # skip special fields :) continue del data[k] self._data.update(data) return self def __local_roles__(self): roles = dict([("{}_{}".format(self.owner, self.owner_token), "tender_owner")]) for i in self.bids: roles["{}_{}".format(i.owner, i.owner_token)] = "bid_owner" return roles def __acl__(self): acl = [(Allow, "{}_{}".format(i.owner, i.owner_token), "create_qualification_complaint") for i in self.bids if i.status in ["active", "unsuccessful"]] acl.extend([(Allow, "{}_{}".format(i.owner, i.owner_token), "create_award_complaint") for i in self.bids if i.status == "active"]) acl.extend([ (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_complaint"), ]) self._acl_cancellation_complaint(acl) return acl @serializable(serialized_name="enquiryPeriod", type=ModelType(EnquiryPeriod)) def tender_enquiryPeriod(self): endDate = calculate_tender_business_date(self.tenderPeriod.endDate, -QUESTIONS_STAND_STILL, self) clarificationsUntil = calculate_clarifications_business_date( endDate, ENQUIRY_STAND_STILL_TIME, self, True) return EnquiryPeriod( dict( startDate=self.tenderPeriod.startDate, endDate=endDate, invalidationDate=self.enquiryPeriod and self.enquiryPeriod.invalidationDate, clarificationsUntil=clarificationsUntil, )) @serializable(type=ModelType(Period)) def complaintPeriod(self): endDate = calculate_complaint_business_date(self.tenderPeriod.endDate, -COMPLAINT_SUBMIT_TIME, self) return Period( dict(startDate=self.tenderPeriod.startDate, endDate=endDate)) @serializable(serialize_when_none=False) def next_check(self): now = get_now() checks = [] if (self.status == "active.tendering" and self.tenderPeriod.endDate and not has_unanswered_complaints(self) and not has_unanswered_questions(self)): checks.append(self.tenderPeriod.endDate.astimezone(TZ)) elif (self.status == "active.pre-qualification.stand-still" and self.qualificationPeriod and self.qualificationPeriod.endDate): active_lots = [ lot.id for lot in self.lots if lot.status == "active" ] if self.lots else [None] if not any([ i.status in self.block_complaint_status for q in self.qualifications for i in q.complaints if q.lotID in active_lots ]): checks.append(self.qualificationPeriod.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"): 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) extend_next_check_by_complaint_period_ends(self, checks) return min(checks).isoformat() if checks else None @serializable def numberOfBids(self): """A property that is serialized by schematics exports.""" return len( [bid for bid in self.bids if bid.status in ("active", "pending")]) @serializable(serialized_name="minValue", type=ModelType(Value)) def tender_minValue(self): return (Value( dict( amount=sum([i.minValue.amount for i in self.lots]), currency=self.minValue.currency, valueAddedTaxIncluded=self.minValue.valueAddedTaxIncluded, )) if self.lots else self.minValue) @serializable(serialized_name="guarantee", serialize_when_none=False, type=ModelType(Guarantee)) def tender_guarantee(self): if self.lots: lots_amount = [ i.guarantee.amount for i in self.lots if i.guarantee ] if not lots_amount: return self.guarantee guarantee = {"amount": sum(lots_amount)} lots_currency = [ i.guarantee.currency for i in self.lots if i.guarantee ] guarantee["currency"] = lots_currency[0] if lots_currency else None if self.guarantee: guarantee["currency"] = self.guarantee.currency return Guarantee(guarantee) else: return self.guarantee @serializable(serialized_name="minimalStep", type=ModelType(Value), serialize_when_none=False) def tender_minimalStep(self): pass @serializable(serialized_name="minimalStepPercentage") def tender_minimalStepPercentage(self): return min([i.minimalStepPercentage for i in self.lots ]) if self.lots else self.minimalStepPercentage @serializable(serialized_name="yearlyPaymentsPercentageRange") def tender_yearlyPaymentsPercentageRange(self): return (min([i.yearlyPaymentsPercentageRange for i in self.lots]) if self.lots else self.yearlyPaymentsPercentageRange) @serializable(serialized_name="noticePublicationDate", serialize_when_none=False, type=IsoDateTimeType()) def tender_noticePublicationDate(self): if not self.noticePublicationDate and self.status == "active.tendering": return self.get_root().request.now else: return self.noticePublicationDate def validate_items(self, data, items): cpv_336_group = items[ 0].classification.id[:3] == "336" if items else False if (not cpv_336_group and (data.get("revisions")[0].date if data.get("revisions") else get_now()) > CPV_ITEMS_CLASS_FROM and items and len(set([i.classification.id[:4] for i in items])) != 1): raise ValidationError(u"CPV class of items should be identical") else: validate_cpv_group(items) def validate_features(self, data, features): if (features and data["lots"] and any([ round( vnmax([ i for i in features if i.featureOf == "tenderer" or i.featureOf == "lot" and i.relatedItem == lot["id"] or i.featureOf == "item" and i.relatedItem in [ j.id for j in data["items"] if j.relatedLot == lot["id"] ] ]), 15, ) > 0.25 for lot in data["lots"] ])): raise ValidationError( u"Sum of max value of all features for lot should be less then or equal to 25%" ) elif features and not data["lots"] and round(vnmax(features), 15) > 0.25: raise ValidationError( u"Sum of max value of all features should be less then or equal to 25%" ) def validate_auctionUrl(self, data, url): if url and data["lots"]: raise ValidationError(u"url should be posted for each lot") def validate_minimalStep(self, data, value): pass def validate_tenderPeriod(self, data, period): # if 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, TENDERING_DURATION, data) > period.endDate: raise ValidationError( u"tenderPeriod should be greater than {} days".format( TENDERING_DAYS)) def validate_awardPeriod(self, data, period): if (period and period.startDate and data.get("auctionPeriod") and data.get("auctionPeriod").endDate and period.startDate < data.get("auctionPeriod").endDate): raise ValidationError(u"period should begin after auctionPeriod") if (period and period.startDate and data.get("tenderPeriod") and data.get("tenderPeriod").endDate and period.startDate < data.get("tenderPeriod").endDate): raise ValidationError(u"period should begin after tenderPeriod") def validate_lots(self, data, value): if len(set([lot.guarantee.currency for lot in value if lot.guarantee])) > 1: raise ValidationError( u"lot guarantee currency should be identical to tender guarantee currency" ) if len(set([lot.fundingKind for lot in value])) > 1: raise ValidationError( u"lot funding kind should be identical to tender funding kind") def validate_yearlyPaymentsPercentageRange(self, data, value): if data["fundingKind"] == "other" and value != Decimal("0.8"): raise ValidationError( "when fundingKind is other, yearlyPaymentsPercentageRange should be equal 0.8" ) if data["fundingKind"] == "budget" and (value > Decimal("0.8") or value < Decimal("0")): raise ValidationError( "when fundingKind is budget, yearlyPaymentsPercentageRange should be less or equal 0.8, and more or equal 0" ) def check_auction_time(self): if (self.auctionPeriod and self.auctionPeriod.startDate and self.auctionPeriod.shouldStartAfter and self.auctionPeriod.startDate > calculate_tender_business_date( parse_date(self.auctionPeriod.shouldStartAfter), AUCTION_PERIOD_TIME, self, True)): self.auctionPeriod.startDate = None for lot in self.lots: if (lot.auctionPeriod and lot.auctionPeriod.startDate and lot.auctionPeriod.shouldStartAfter and lot.auctionPeriod.startDate > calculate_tender_business_date( parse_date(lot.auctionPeriod.shouldStartAfter), AUCTION_PERIOD_TIME, self, True)): lot.auctionPeriod.startDate = None def invalidate_bids_data(self): self.check_auction_time() self.enquiryPeriod.invalidationDate = get_now() for bid in self.bids: if bid.status not in ["deleted", "draft"]: bid.status = "invalid" # Not required milestones def validate_milestones(self, data, value): pass
class Tender(BaseTenderEU): procurementMethodType = StringType(default=CD_EU_TYPE) status = StringType(choices=[ 'draft', 'active.tendering', 'active.pre-qualification', 'active.pre-qualification.stand-still', 'active.stage2.pending', 'active.stage2.waiting', 'complete', 'cancelled', 'unsuccessful' ], default='active.tendering') # A list of all the companies who entered submissions for the tender. bids = SifterListType(ModelType(Bid), default=list(), filter_by='status', filter_in_values=['invalid', 'deleted']) TenderID = StringType(required=False) stage2TenderID = StringType(required=False) features = ListType(ModelType(Feature), validators=[validate_features_uniq]) lots = ListType(ModelType(Lot), default=list(), validators=[validate_lots_uniq]) items = ListType(ModelType(BaseEUItem), required=True, min_size=1, validators=[validate_cpv_group, validate_items_uniq]) class Options: roles = roles.copy() def get_role(self): root = self.__parent__ request = root.request if request.authenticated_role == 'Administrator': role = 'Administrator' elif request.authenticated_role == 'chronograph': role = 'chronograph' elif request.authenticated_role == 'competitive_dialogue': role = 'competitive_dialogue' else: role = 'edit_{}'.format(request.context.status) return role def __acl__(self): acl = [(Allow, '{}_{}'.format(i.owner, i.owner_token), 'create_qualification_complaint') for i in self.bids if i.status in ['active', 'unsuccessful']] acl.extend([(Allow, '{}_{}'.format(i.owner, i.owner_token), 'create_award_complaint') for i in self.bids if i.status == 'active']) acl.extend([ (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_tender'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'upload_tender_documents'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_complaint'), (Allow, 'g:competitive_dialogue', 'extract_credentials'), (Allow, 'g:competitive_dialogue', 'edit_tender'), ]) return acl def validate_features(self, data, features): validate_features_custom_weight(self, data, features, FEATURES_MAX_SUM)
class Cancellation(BaseCancellation): class Options: roles = RolesFromCsv("Cancellation.csv", relative_to=__file__) documents = ListType(ModelType(EUDocument, required=True), default=list())
class Firms(Model): identifier = ModelType(Identifier, required=True) name = StringType(required=True) lots = ListType(ModelType(LotId), default=list())
class Bid(BaseBid): class Options: roles = { 'Administrator': Administrator_bid_role, 'embedded': view_bid_role, 'view': view_bid_role, 'create': whitelist('value', 'tenderers', 'parameters', 'lotValues', 'status'), 'edit': whitelist('value', 'tenderers', 'parameters', 'lotValues', 'status'), 'auction_view': whitelist('value', 'lotValues', 'id', 'date', 'parameters', 'participationUrl', 'status'), 'auction_post': whitelist('value', 'lotValues', 'id', 'date'), 'auction_patch': whitelist('id', 'lotValues', 'participationUrl'), 'active.enquiries': whitelist(), 'active.tendering': whitelist(), 'active.pre-qualification': whitelist('id', 'status', 'documents', 'tenderers'), 'active.pre-qualification.stand-still': whitelist('id', 'status', 'documents', 'tenderers'), 'active.auction': whitelist('id', 'status', 'documents', 'tenderers'), 'active.qualification': view_bid_role, 'active.awarded': view_bid_role, 'complete': view_bid_role, 'unsuccessful': view_bid_role, 'bid.unsuccessful': whitelist('id', 'status', 'tenderers', 'documents', 'parameters'), 'cancelled': view_bid_role, 'invalid': whitelist('id', 'status'), 'invalid.pre-qualification': whitelist('id', 'status', 'documents', 'tenderers'), 'deleted': whitelist('id', 'status'), } documents = ListType(ModelType(Document), default=list()) financialDocuments = ListType(ModelType(Document), default=list()) lotValues = ListType(ModelType(LotValue), default=list()) parameters = ListType(ModelType(Parameter), default=list(), validators=[validate_parameters_uniq]) status = StringType(choices=[ 'draft', 'pending', 'active', 'invalid', 'invalid.pre-qualification', 'unsuccessful', 'deleted' ], default='pending') def serialize(self, role=None): if role and role != 'create' and self.status in [ 'invalid', 'invalid.pre-qualification', 'deleted' ]: role = self.status elif role and role != 'create' and self.status == 'unsuccessful': role = 'bid.unsuccessful' return super(Bid, self).serialize(role) @serializable(serialized_name="status") def serialize_status(self): if self.status in [ 'draft', 'invalid', 'deleted' ] or self.__parent__.status in ['active.tendering', 'cancelled']: return self.status if self.__parent__.lots: active_lots = [ lot.id for lot in self.__parent__.lots if lot.status in ( 'active', 'complete', ) ] if not self.lotValues: return 'invalid' elif [ i.relatedLot for i in self.lotValues if i.status == 'pending' and i.relatedLot in active_lots ]: return 'pending' elif [ i.relatedLot for i in self.lotValues if i.status == 'active' and i.relatedLot in active_lots ]: return 'active' else: return 'unsuccessful' return self.status @bids_validation_wrapper def validate_value(self, data, value): BaseBid._validator_functions['value'](self, data, value) @bids_validation_wrapper def validate_lotValues(self, data, lotValues): BaseBid._validator_functions['lotValues'](self, data, lotValues) @bids_validation_wrapper def validate_participationUrl(self, data, participationUrl): BaseBid._validator_functions['participationUrl'](self, data, participationUrl) @bids_validation_wrapper def validate_parameters(self, data, parameters): BaseBid._validator_functions['parameters'](self, data, parameters)
class Award(BaseTenderUA.awards.model_class): items = ListType(ModelType(ItemStage2UA))
class Tender(BaseTender): """ Open two stage tender model """ class Options: roles = { 'plain': plain_role, 'create': create_role_ts, 'edit': edit_role_ts, 'edit_draft': edit_role_ts, 'edit_active.tendering': edit_role_ts, 'edit_active.pre-qualification': whitelist('status'), 'edit_active.pre-qualification.stand-still': whitelist(), 'edit_active.auction': whitelist(), 'edit_active.qualification': whitelist(), 'edit_active.awarded': whitelist(), 'edit_complete': whitelist(), 'edit_unsuccessful': whitelist(), 'edit_cancelled': whitelist(), 'view': view_role, 'listing': listing_role, 'auction_view': auction_view_role, 'auction_post': auction_post_role, 'auction_patch': auction_patch_role, 'draft': enquiries_role, 'active.tendering': enquiries_role, 'active.pre-qualification': pre_qualifications_role, 'active.pre-qualification.stand-still': pre_qualifications_role, 'active.auction': pre_qualifications_role, 'active.qualification': view_role, 'active.awarded': view_role, 'complete': view_role, 'unsuccessful': view_role, 'cancelled': view_role, 'chronograph': chronograph_role, 'chronograph_view': chronograph_view_role, 'Administrator': Administrator_role, 'default': schematics_default_role, 'contracting': whitelist('doc_id', 'owner'), } procurementMethodType = StringType(default="aboveThresholdTS") title_en = StringType() enquiryPeriod = ModelType(EnquiryPeriod, required=False) tenderPeriod = ModelType(PeriodStartEndRequired, required=True) auctionPeriod = ModelType(TenderAuctionPeriod, default={}) documents = ListType(ModelType(Document), default=list()) # All items = ListType( ModelType(Item), required=True, min_size=1, validators=[validate_cpv_group, validate_items_uniq] ) # The goods and services to be purchased, broken into line items wherever possible. Items should not be duplicated, but a quantity of 2 specified instead. complaints = ListType(ComplaintModelType(Complaint), default=list()) contracts = ListType(ModelType(Contract), default=list()) cancellations = ListType(ModelType(Cancellation), default=list()) awards = ListType(ModelType(Award), default=list()) procuringEntity = ModelType( ProcuringEntity, required=True ) # The entity managing the procurement, which may be different from the buyer who is paying / using the items being procured. bids = SifterListType( BidModelType(Bid), default=list(), filter_by='status', filter_in_values=['invalid', 'invalid.pre-qualification', 'deleted'] ) # A list of all the companies who entered submissions for the tender. qualifications = ListType(ModelType(Qualification), default=list()) qualificationPeriod = ModelType(Period) lots = ListType(ModelType(Lot), default=list(), validators=[validate_lots_uniq]) status = StringType(choices=[ 'draft', 'active.tendering', 'active.pre-qualification', 'active.pre-qualification.stand-still', 'active.auction', 'active.qualification', 'active.awarded', 'complete', 'cancelled', 'unsuccessful' ], default='active.tendering') create_accreditation = 3 edit_accreditation = 4 procuring_entity_kinds = ['general', 'special', 'defense'] block_tender_complaint_status = OpenUATender.block_tender_complaint_status #block_complaint_status = OpenUATender.block_complaint_status def __acl__(self): acl = [(Allow, '{}_{}'.format(i.owner, i.owner_token), 'create_qualification_complaint') for i in self.bids if i.status in ['active', 'unsuccessful']] acl.extend([(Allow, '{}_{}'.format(i.owner, i.owner_token), 'create_award_complaint') for i in self.bids if i.status == 'active']) acl.extend([ (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_tender'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'upload_tender_documents'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_complaint'), ]) return acl def initialize(self): self.tenderPeriod.startDate = get_now() endDate = calculate_business_date(self.tenderPeriod.endDate, -QUESTIONS_STAND_STILL, self) self.enquiryPeriod = EnquiryPeriod( dict(startDate=self.tenderPeriod.startDate, endDate=endDate, invalidationDate=self.enquiryPeriod and self.enquiryPeriod.invalidationDate, clarificationsUntil=endDate)) now = get_now() self.date = now if self.lots: for lot in self.lots: lot.date = now @serializable(serialized_name="enquiryPeriod", type=ModelType(EnquiryPeriod)) def tender_enquiryPeriod(self): endDate = calculate_business_date(self.tenderPeriod.endDate, -QUESTIONS_STAND_STILL, self) return EnquiryPeriod( dict(startDate=self.tenderPeriod.startDate, endDate=endDate, invalidationDate=self.enquiryPeriod and self.enquiryPeriod.invalidationDate, clarificationsUntil=endDate)) @serializable(type=ModelType(Period)) def complaintPeriod(self): return Period( dict(startDate=self.tenderPeriod.startDate, endDate=calculate_business_date(self.tenderPeriod.startDate, COMPLAINT_SUBMIT_TIME, self))) @serializable(serialize_when_none=False) def next_check(self): now = get_now() checks = [] if self.status == 'active.tendering' and self.tenderPeriod.endDate: checks.append(self.tenderPeriod.endDate.astimezone(TZ)) elif self.status == 'active.pre-qualification.stand-still' and self.qualificationPeriod and self.qualificationPeriod.endDate and not any( [ i.status in self.block_complaint_status for q in self.qualifications for i in q.complaints ]): checks.append(self.qualificationPeriod.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)) return min(checks).isoformat() if checks else None def validate_tenderPeriod(self, data, period): # if data['_rev'] is None when tender was created just now if not data['_rev'] and calculate_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_business_date( period.startDate, TENDERING_DURATION, data) > period.endDate: raise ValidationError( u"tenderPeriod should be greater than {} days".format( TENDERING_DAYS)) @serializable def numberOfBids(self): """A property that is serialized by schematics exports.""" return len([ bid for bid in self.bids if bid.status in ( "active", "pending", ) ]) def check_auction_time(self): if self.auctionPeriod and self.auctionPeriod.startDate and self.auctionPeriod.shouldStartAfter \ and self.auctionPeriod.startDate > calculate_business_date(parse_date(self.auctionPeriod.shouldStartAfter), AUCTION_PERIOD_TIME, self, True): self.auctionPeriod.startDate = None for lot in self.lots: if lot.auctionPeriod and lot.auctionPeriod.startDate and lot.auctionPeriod.shouldStartAfter \ and lot.auctionPeriod.startDate > calculate_business_date(parse_date(lot.auctionPeriod.shouldStartAfter), AUCTION_PERIOD_TIME, self, True): lot.auctionPeriod.startDate = None def invalidate_bids_data(self): self.check_auction_time() self.enquiryPeriod.invalidationDate = get_now() for bid in self.bids: if bid.status not in ["deleted", "draft"]: bid.status = "invalid"
class Bid(Model): class Options: roles = { 'Administrator': Administrator_bid_role, 'embedded': view_bid_role, 'view': view_bid_role, 'create': whitelist('value', 'status', 'tenderers', 'parameters', 'lotValues', 'documents'), 'edit': whitelist('value', 'status', 'tenderers', 'parameters', 'lotValues'), 'auction_view': whitelist('value', 'lotValues', 'id', 'date', 'parameters', 'participationUrl'), 'auction_post': whitelist('value', 'lotValues', 'id', 'date'), 'auction_patch': whitelist('id', 'lotValues', 'participationUrl'), 'active.enquiries': whitelist(), 'active.tendering': whitelist(), 'active.auction': whitelist(), 'active.qualification': view_bid_role, 'active.awarded': view_bid_role, 'complete': view_bid_role, 'unsuccessful': view_bid_role, 'cancelled': view_bid_role, } def __local_roles__(self): return dict([('{}_{}'.format(self.owner, self.owner_token), 'bid_owner')]) tenderers = ListType(ModelType(Organization), required=True, min_size=1, max_size=1) parameters = ListType(ModelType(Parameter), default=list(), validators=[validate_parameters_uniq]) lotValues = ListType(ModelType(LotValue), default=list()) date = IsoDateTimeType(default=get_now) id = MD5Type(required=True, default=lambda: uuid4().hex) status = StringType(choices=['active', 'draft'], default='active') value = ModelType(Value) documents = ListType(ModelType(Document), default=list()) participationUrl = URLType() owner_token = StringType() owner = StringType() __name__ = '' def import_data(self, raw_data, **kw): """ Converts and imports the raw data into the instance of the model according to the fields in the model. :param raw_data: The data to be imported. """ data = self.convert(raw_data, **kw) del_keys = [k for k in data.keys() if k != "value" and data[k] is None] for k in del_keys: del data[k] self._data.update(data) return self def __acl__(self): return [ (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_bid'), ] def validate_participationUrl(self, data, url): if url and isinstance(data['__parent__'], Model) and get_tender( data['__parent__']).lots: raise ValidationError(u"url should be posted for each lot of bid") def validate_lotValues(self, data, values): if isinstance(data['__parent__'], Model): tender = data['__parent__'] if tender.lots and not values: raise ValidationError(u'This field is required.') if tender.get('revisions') and tender['revisions'][ 0].date > BID_LOTVALUES_VALIDATION_FROM and values: lots = [i.relatedLot for i in values] if len(lots) != len(set(lots)): raise ValidationError( u'bids don\'t allow duplicated proposals') def validate_value(self, data, value): if isinstance(data['__parent__'], Model): tender = data['__parent__'] if tender.lots: if value: raise ValidationError( u"value should be posted for each lot of bid") else: if not value: raise ValidationError(u'This field is required.') if tender.value.amount < value.amount: raise ValidationError( u"value of bid should be less than value of tender") if tender.get('value').currency != value.currency: raise ValidationError( u"currency of bid should be identical to currency of value of tender" ) if tender.get( 'value' ).valueAddedTaxIncluded != value.valueAddedTaxIncluded: raise ValidationError( u"valueAddedTaxIncluded of bid should be identical to valueAddedTaxIncluded of value of tender" ) def validate_parameters(self, data, parameters): if isinstance(data['__parent__'], Model): tender = data['__parent__'] if tender.lots: lots = [i.relatedLot for i in data['lotValues']] items = [i.id for i in tender.items if i.relatedLot in lots] codes = dict([ (i.code, [x.value for x in i.enum]) for i in (tender.features or []) if i.featureOf == 'tenderer' or i.featureOf == 'lot' and i.relatedItem in lots or i.featureOf == 'item' and i.relatedItem in items ]) if set([i['code'] for i in parameters]) != set(codes): raise ValidationError( u"All features parameters is required.") elif not parameters and tender.features: raise ValidationError(u'This field is required.') elif set([i['code'] for i in parameters]) != set( [i.code for i in (tender.features or [])]): raise ValidationError(u"All features parameters is required.")
class ProcuringEntity(BaseProcuringEntity): contactPoint = ModelType(ContactPoint, required=True) additionalContactPoints = ListType(ModelType(ContactPoint, required=True), required=False)
class BaseTender(SchematicsDocument, Model): title = StringType(required=True) title_en = StringType() title_ru = StringType() documents = ListType( ModelType(Document), default=list()) # All documents and attachments related to the tender. description = StringType() description_en = StringType() description_ru = StringType() date = IsoDateTimeType() dateModified = IsoDateTimeType() tenderID = StringType( ) # TenderID should always be the same as the OCID. It is included to make the flattened data structure more convenient. owner = StringType() owner_token = StringType() mode = StringType(choices=['test']) procurementMethodRationale = StringType( ) # Justification of procurement method, especially in the case of Limited tendering. procurementMethodRationale_en = StringType() procurementMethodRationale_ru = StringType() if SANDBOX_MODE: procurementMethodDetails = StringType() _attachments = DictType(DictType(BaseType), default=dict()) # couchdb attachments revisions = ListType(ModelType(Revision), default=list()) def __repr__(self): return '<%s:%r@%r>' % (type(self).__name__, self.id, self.rev) def __local_roles__(self): roles = dict([('{}_{}'.format(self.owner, self.owner_token), 'tender_owner')]) return roles @serializable(serialized_name='id') def doc_id(self): """A property that is serialized by schematics exports.""" return self._id def import_data(self, raw_data, **kw): """ Converts and imports the raw data into the instance of the model according to the fields in the model. :param raw_data: The data to be imported. """ data = self.convert(raw_data, **kw) del_keys = [ k for k in data.keys() if data[k] == self.__class__.fields[k].default or data[k] == getattr(self, k) ] for k in del_keys: del data[k] self._data.update(data) return self def validate_procurementMethodDetails(self, *args, **kw): if self.mode and self.mode == 'test' and self.procurementMethodDetails and self.procurementMethodDetails != '': raise ValidationError( u"procurementMethodDetails should be used with mode test")
class Tender(BaseTender): """Data regarding tender process - publicly inviting prospective contractors to submit bids for evaluation and selecting a winner or winners. """ class Options: roles = { 'plain': plain_role, 'create': create_role, 'edit': edit_role, 'edit_draft': draft_role, 'edit_active.enquiries': edit_role, 'edit_active.tendering': whitelist(), 'edit_active.auction': whitelist(), 'edit_active.qualification': whitelist(), 'edit_active.awarded': whitelist(), 'edit_complete': whitelist(), 'edit_unsuccessful': whitelist(), 'edit_cancelled': whitelist(), 'view': view_role, 'listing': listing_role, 'auction_view': auction_view_role, 'auction_post': auction_post_role, 'auction_patch': auction_patch_role, 'draft': enquiries_role, 'active.enquiries': enquiries_role, 'active.tendering': enquiries_role, 'active.auction': auction_role, 'active.qualification': view_role, 'active.awarded': view_role, 'complete': view_role, 'unsuccessful': view_role, 'cancelled': view_role, 'chronograph': chronograph_role, 'chronograph_view': chronograph_view_role, 'Administrator': Administrator_role, 'default': schematics_default_role, 'contracting': whitelist('doc_id', 'owner'), } items = ListType( ModelType(Item), required=True, min_size=1, validators=[validate_items_uniq] ) # The goods and services to be purchased, broken into line items wherever possible. Items should not be duplicated, but a quantity of 2 specified instead. value = ModelType( Value, required=True) # The total estimated value of the procurement. enquiryPeriod = ModelType( PeriodEndRequired, required=True ) # The period during which enquiries may be made and will be answered. tenderPeriod = ModelType( PeriodEndRequired, required=True ) # The period when the tender is open for submissions. The end date is the closing date for tender submissions. hasEnquiries = BooleanType( ) # A Yes/No field as to whether enquiries were part of tender process. awardPeriod = ModelType( Period ) # The date or period on which an award is anticipated to be made. numberOfBidders = IntType( ) # The number of unique tenderers who participated in the tender bids = ListType(ModelType(Bid), default=list( )) # A list of all the companies who entered submissions for the tender. procuringEntity = ModelType( ProcuringEntity, required=True ) # The entity managing the procurement, which may be different from the buyer who is paying / using the items being procured. awards = ListType(ModelType(Award), default=list()) contracts = ListType(ModelType(Contract), default=list()) auctionPeriod = ModelType(TenderAuctionPeriod, default={}) minimalStep = ModelType(Value, required=True) questions = ListType(ModelType(Question), default=list()) complaints = ListType(ComplaintModelType(Complaint), default=list()) auctionUrl = URLType() cancellations = ListType(ModelType(Cancellation), default=list()) features = ListType(ModelType(Feature), validators=[validate_features_uniq]) lots = ListType(ModelType(Lot), default=list(), validators=[validate_lots_uniq]) guarantee = ModelType(Guarantee) procurementMethodType = StringType(default="belowThreshold") procuring_entity_kinds = ['general', 'special', 'defense', 'other'] block_complaint_status = ['answered', 'pending'] def __local_roles__(self): roles = dict([('{}_{}'.format(self.owner, self.owner_token), 'tender_owner')]) for i in self.bids: roles['{}_{}'.format(i.owner, i.owner_token)] = 'bid_owner' return roles @serializable(serialize_when_none=False) 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_business_date for complaint in self.complaints: if complaint.status == 'answered' and complaint.dateAnswered: checks.append( calculate_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_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 @serializable def numberOfBids(self): """A property that is serialized by schematics exports.""" return len(self.bids) @serializable(serialized_name="value", type=ModelType(Value)) def tender_value(self): return Value( dict(amount=sum([i.value.amount for i in self.lots]), currency=self.value.currency, valueAddedTaxIncluded=self.value.valueAddedTaxIncluded) ) if self.lots else self.value @serializable(serialized_name="guarantee", serialize_when_none=False, type=ModelType(Guarantee)) def tender_guarantee(self): if self.lots: lots_amount = [ i.guarantee.amount for i in self.lots if i.guarantee ] if not lots_amount: return self.guarantee guarantee = {'amount': sum(lots_amount)} lots_currency = [ i.guarantee.currency for i in self.lots if i.guarantee ] guarantee['currency'] = lots_currency[0] if lots_currency else None if self.guarantee: guarantee['currency'] = self.guarantee.currency return Guarantee(guarantee) else: return self.guarantee @serializable(serialized_name="minimalStep", type=ModelType(Value)) def tender_minimalStep(self): return Value( dict(amount=min([i.minimalStep.amount for i in self.lots]), currency=self.minimalStep.currency, valueAddedTaxIncluded=self.minimalStep.valueAddedTaxIncluded) ) if self.lots else self.minimalStep def validate_items(self, data, items): cpv_336_group = items[ 0].classification.id[:3] == '336' if items else False if not cpv_336_group and ( data.get('revisions')[0].date if data.get('revisions') else get_now()) > CPV_ITEMS_CLASS_FROM and items and len( set([i.classification.id[:4] for i in items])) != 1: raise ValidationError(u"CPV class of items should be identical") else: validate_cpv_group(items) def validate_features(self, data, features): if features and data['lots'] and any([ round( vnmax([ i for i in features if i.featureOf == 'tenderer' or i.featureOf == 'lot' and i.relatedItem == lot['id'] or i.featureOf == 'item' and i.relatedItem in [ j.id for j in data['items'] if j.relatedLot == lot['id'] ] ]), 15) > 0.3 for lot in data['lots'] ]): raise ValidationError( u"Sum of max value of all features for lot should be less then or equal to 30%" ) elif features and not data['lots'] and round(vnmax(features), 15) > 0.3: raise ValidationError( u"Sum of max value of all features should be less then or equal to 30%" ) def validate_auctionUrl(self, data, url): if url and data['lots']: raise ValidationError(u"url should be posted for each lot") def validate_minimalStep(self, data, value): if value and value.amount and data.get('value'): if data.get('value').amount < value.amount: raise ValidationError( u"value should be less than value of tender") if data.get('value').currency != value.currency: raise ValidationError( u"currency should be identical to currency of value of tender" ) if data.get('value' ).valueAddedTaxIncluded != value.valueAddedTaxIncluded: raise ValidationError( u"valueAddedTaxIncluded should be identical to valueAddedTaxIncluded of value of tender" ) def validate_tenderPeriod(self, data, period): if period and period.startDate and data.get( 'enquiryPeriod') and data.get( 'enquiryPeriod').endDate and period.startDate < data.get( 'enquiryPeriod').endDate: raise ValidationError(u"period should begin after enquiryPeriod") def validate_awardPeriod(self, data, period): if period and period.startDate and data.get( 'auctionPeriod') and data.get( 'auctionPeriod').endDate and period.startDate < data.get( 'auctionPeriod').endDate: raise ValidationError(u"period should begin after auctionPeriod") if period and period.startDate and data.get( 'tenderPeriod') and data.get( 'tenderPeriod').endDate and period.startDate < data.get( 'tenderPeriod').endDate: raise ValidationError(u"period should begin after tenderPeriod") def validate_lots(self, data, value): if len(set([lot.guarantee.currency for lot in value if lot.guarantee])) > 1: raise ValidationError( u"lot guarantee currency should be identical to tender guarantee currency" )
class Contract(BaseContract): documents = ListType(ModelType(EUDocument, required=True), default=list()) items = ListType(ModelType(Item, required=True))
class ElectronicCatalogueFramework(Framework): class Options: namespace = "Framework" _status_view_role = blacklist( "doc_type", "successful", "transfer_token", "owner_token", "revisions", "_id", "_rev", "__parent__", ) _edit_role = _status_view_role + blacklist( "frameworkType", "prettyID", "period", "enquiryPeriod", "dateModified", "date", "doc_id") _create_role = _edit_role + blacklist("status") roles = { "create": _create_role, "edit_draft": _edit_role + blacklist("owner", "old_date"), "edit_active": whitelist("status", "procuringEntity", "qualificationPeriod", "description", "description_en", "description_ru", "documents", "frameworkDetails"), "draft": _status_view_role, "active": _status_view_role, "complete": _status_view_role, "unsuccessful": _status_view_role, "view": _edit_role + whitelist( "date", "period", "enquiryPeriod", "prettyID", "documents", "doc_id", "dateModified", "status", "owner", "next_check", ), "chronograph": whitelist("next_check"), "chronograph_view": _status_view_role, "Administrator": whitelist("status", "mode"), "default": blacklist("doc_id", "__parent__"), # obj.store() use default role "plain": blacklist( # is used for getting patches "_attachments", "revisions", "dateModified", "_id", "_rev", "doc_type", "__parent__"), "listing": whitelist("dateModified", "doc_id"), "embedded": blacklist("_id", "_rev", "doc_type", "__parent__"), } status = StringType( choices=[ "draft", "active", "deleted", "complete", "unsuccessful", ], default="draft", ) period = ModelType(BasePeriodEndRequired) qualificationPeriod = ModelType(BasePeriodEndRequired, required=True) enquiryPeriod = ModelType(BasePeriodEndRequired) frameworkType = StringType(default="electronicCatalogue") procuringEntity = ModelType(CentralProcuringEntity, required=True) classification = ModelType(DKClassification, required=True) additionalClassifications = ListType(ModelType(BaseClassification)) documents = ListType(ModelType(Document, required=True), default=list()) successful = BooleanType(required=True, default=False) procuring_entity_kinds = ["central"] central_accreditations = (ACCR_5, ) edit_accreditations = (ACCR_5, ) @serializable(serialize_when_none=False) def next_check(self): checks = [] if self.status == "active": if not self.successful: unsuccessful_status_check = get_framework_unsuccessful_status_check_date( self) if unsuccessful_status_check: checks.append(unsuccessful_status_check) checks.append(self.qualificationPeriod.endDate) return min(checks).isoformat() if checks else None def __acl__(self): acl = super(ElectronicCatalogueFramework, self).__acl__() acl.append((Allow, "{}_{}".format(self.owner, self.owner_token), "upload_framework_documents")) return acl