class UnitPriceModification(Model): class Options: roles = RolesFromCsv("UnitPriceModification.csv", relative_to=__file__) itemId = StringType() factor = DecimalType(required=False, precision=-4, min_value=Decimal("0.0")) addend = DecimalType(required=False, precision=-2)
class Contract(BaseContract): """ ESCO Contract """ contractType = StringType(default='esco') fundingKind = StringType(choices=['budget', 'other'], required=True) milestones = SifterListType(ModelType(Milestone), default=list(), filter_by='status', filter_in_values=[ 'scheduled', 'pending', 'met', 'notMet', 'partiallyMet' ]) minValue = ModelType(Value, required=False, default={ 'amount': 0, 'currency': 'UAH', 'valueAddedTaxIncluded': True }) NBUdiscountRate = DecimalType(required=True, min_value=Decimal('0'), max_value=Decimal('0.99'), precision=-5) noticePublicationDate = IsoDateTimeType() value = ModelType(ESCOValue) amountPaid = ModelType(Value) yearlyPaymentsPercentageRange = DecimalType(required=True) documents = ListType(ModelType(Document), default=list()) class Options: roles = { 'plain': plain_role, 'create': contract_create_role, 'edit_active': contract_edit_role, 'edit_terminated': whitelist(), 'view': contract_view_role + whitelist('NBUdiscountRate', 'contractType', 'milestones'), 'Administrator': contract_administrator_role, 'default': schematics_default_role, } @serializable(serialized_name='amountPaid', serialize_when_none=False, type=ModelType(Value)) def contract_amountPaid(self): amount = sum([ milestone.amountPaid.amount for milestone in self.milestones if milestone.status != 'spare' ]) return Value( dict(amount=amount, currency=self.value.currency, valueAddedTaxIncluded=self.value.valueAddedTaxIncluded))
class ESCOValue(BaseESCOValue): class Options: roles = { 'embedded': view_value_role_esco, 'view': view_value_role_esco, 'create': whitelist('amount', 'amount_escp', 'amountPerformance', 'amountPerformance_npv', 'yearlyPaymentsPercentage', 'annualCostsReduction', 'contractDuration', 'currency', 'valueAddedTaxIncluded'), 'edit': whitelist('amount', 'amount_escp', 'amountPerformance', 'amountPerformance_npv', 'yearlyPaymentsPercentage', 'annualCostsReduction', 'contractDuration', 'currency', 'valueAddedTaxIncluded'), 'auction_view': whitelist('amountPerformance', 'yearlyPaymentsPercentage', 'annualCostsReduction', 'contractDuration', 'currency', 'valueAddedTaxIncluded'), 'auction_post': whitelist('amount_escp', 'amountPerformance_npv', 'yearlyPaymentsPercentage', 'contractDuration'), 'active.qualification': view_value_role_esco, 'active.awarded': view_value_role_esco, 'complete': view_value_role_esco, 'unsuccessful': view_value_role_esco, 'cancelled': view_value_role_esco, } def validate_yearlyPaymentsPercentage(self, data, value): pass @serializable(serialized_name='amountPerformance', type=DecimalType(precision=-2)) def amountPerformance_npv(self): """ Calculated energy service contract performance indicator """ return to_decimal( npv(self.contractDuration.years, self.contractDuration.days, self.yearlyPaymentsPercentage, self.annualCostsReduction, self.__parent__.noticePublicationDate, self.__parent__.NBUdiscountRate)) @serializable(serialized_name='amount', type=DecimalType(precision=-2)) def amount_escp(self): return sum([ milestone.value.amount for milestone in self.__parent__.milestones if milestone.status != 'spare' ])
class ESCOValue(BaseESCOValue): @serializable(serialized_name="amountPerformance", type=DecimalType(precision=-2)) def amountPerformance_npv(self): """ Calculated energy service contract performance indicator """ return to_decimal( npv( self.contractDuration.years, self.contractDuration.days, self.yearlyPaymentsPercentage, self.annualCostsReduction, get_tender(self).noticePublicationDate, get_tender(self).NBUdiscountRate, ) ) @serializable(serialized_name="amount", type=DecimalType(precision=-2)) def amount_escp(self): return to_decimal( escp( self.contractDuration.years, self.contractDuration.days, self.yearlyPaymentsPercentage, self.annualCostsReduction, get_tender(self).noticePublicationDate, ) ) def validate_annualCostsReduction(self, data, value): if len(value) != 21: raise ValidationError("annual costs reduction should be set for 21 period") @bids_validation_wrapper def validate_yearlyPaymentsPercentage(self, data, value): parent = data["__parent__"] tender = get_tender(parent) if tender.fundingKind == "other" and value < Decimal("0.8"): raise ValidationError("yearlyPaymentsPercentage should be greater than 0.8 and less than 1") if tender.fundingKind == "budget": if tender.lots: lots = [i for i in tender.lots if i.id == parent["relatedLot"]] if lots and value > lots[0].yearlyPaymentsPercentageRange: raise ValidationError( "yearlyPaymentsPercentage should be greater than 0 and less than {}".format( lots[0].yearlyPaymentsPercentageRange ) ) else: if value > tender.yearlyPaymentsPercentageRange: raise ValidationError( "yearlyPaymentsPercentage should be greater than 0 and less than {}".format( tender.yearlyPaymentsPercentageRange ) )
class UnitPriceModification(Model): class Options: roles = { "edit": blacklist("id", "__parent__"), "default": blacklist("__parent__"), "create": blacklist("id", "__parent__"), "embedded": blacklist("__parent__"), "view": blacklist("__parent__"), } itemId = StringType() factor = DecimalType(required=False, precision=-4, min_value=Decimal("0.0")) addend = DecimalType(required=False, precision=-2)
class BaseESCOValue(Value): class Options: roles = { 'embedded': view_value_role_esco, 'view': view_value_role_esco, 'create': whitelist('amount', 'amount_escp', 'amountPerformance', 'amountPerformance_npv', 'yearlyPaymentsPercentage', 'annualCostsReduction', 'contractDuration', 'currency', 'valueAddedTaxIncluded'), 'edit': whitelist('amount', 'amount_escp', 'amountPerformance', 'amountPerformance_npv', 'yearlyPaymentsPercentage', 'annualCostsReduction', 'contractDuration', 'currency', 'valueAddedTaxIncluded'), 'auction_view': whitelist('amountPerformance', 'yearlyPaymentsPercentage', 'annualCostsReduction', 'contractDuration', 'currency', 'valueAddedTaxIncluded'), 'auction_post': whitelist('amount_escp', 'amountPerformance_npv', 'yearlyPaymentsPercentage', 'contractDuration'), 'active.qualification': view_value_role_esco, 'active.awarded': view_value_role_esco, 'complete': view_value_role_esco, 'unsuccessful': view_value_role_esco, 'cancelled': view_value_role_esco, } amount = DecimalType( min_value=Decimal('0'), required=False, precision=-2) # Calculated energy service contract value. amountPerformance = DecimalType( required=False, precision=-2 ) # Calculated energy service contract performance indicator yearlyPaymentsPercentage = DecimalType( required=True, precision=-5, min_value=Decimal('0'), max_value=Decimal( '1')) # The percentage of annual payments in favor of Bidder annualCostsReduction = ListType( DecimalType(), required=True) # Buyer's annual costs reduction contractDuration = ModelType(ContractDuration, required=True)
class BaseESCOValue(Value): class Options: roles = { "embedded": view_value_role_esco, "view": view_value_role_esco, "create": create_value_role_esco, "edit": edit_value_role_esco, "auction_view": whitelist( "amountPerformance", "yearlyPaymentsPercentage", "annualCostsReduction", "contractDuration", "currency", "valueAddedTaxIncluded", ), "auction_post": whitelist("amount_escp", "amountPerformance_npv", "yearlyPaymentsPercentage", "contractDuration"), "active.qualification": view_value_role_esco, "active.awarded": view_value_role_esco, "complete": view_value_role_esco, "unsuccessful": view_value_role_esco, "cancelled": view_value_role_esco, } amount = DecimalType( min_value=Decimal("0"), required=False, precision=-2) # Calculated energy service contract value. amountPerformance = DecimalType( required=False, precision=-2 ) # Calculated energy service contract performance indicator yearlyPaymentsPercentage = DecimalType( required=True, precision=-5, min_value=Decimal("0"), max_value=Decimal( "1")) # The percentage of annual payments in favor of Bidder annualCostsReduction = ListType( DecimalType(), required=True) # Buyer's annual costs reduction contractDuration = ModelType(ContractDuration, required=True)
class FeatureValue(Model): value = DecimalType(required=True, min_value=Decimal("0.0"), max_value=Decimal("0.3")) title = StringType(required=True, min_length=1) title_en = StringType() title_ru = StringType() description = StringType() description_en = StringType() description_ru = StringType()
class ContractESCOValue(BaseESCOValue): class Options: roles = { "view": (view_value_role_esco + whitelist("amountNet")), "create": (create_value_role_esco + whitelist("amountNet")), "edit": (edit_value_role_esco + whitelist("amountNet")), "active.awarded": (view_value_role_esco + whitelist("amountNet")), "complete": (view_value_role_esco + whitelist("amountNet")), "unsuccessful": (view_value_role_esco + whitelist("amountNet")), "cancelled": (view_value_role_esco + whitelist("amountNet")), } amountNet = DecimalType(min_value=Decimal("0"), precision=-2)
class ESCOValue(BaseESCOValue): @serializable(serialized_name='amountPerformance', type=DecimalType(precision=-2)) def amountPerformance_npv(self): """ Calculated energy service contract performance indicator """ return to_decimal( npv(self.contractDuration.years, self.contractDuration.days, self.yearlyPaymentsPercentage, self.annualCostsReduction, get_tender(self).noticePublicationDate, get_tender(self).NBUdiscountRate)) @serializable(serialized_name='amount', type=DecimalType(precision=-2)) def amount_escp(self): return to_decimal( escp(self.contractDuration.years, self.contractDuration.days, self.yearlyPaymentsPercentage, self.annualCostsReduction, get_tender(self).noticePublicationDate)) def validate_annualCostsReduction(self, data, value): if len(value) != 21: raise ValidationError( 'annual costs reduction should be set for 21 period') def validate_yearlyPaymentsPercentage(self, data, value): if get_tender(data['__parent__']).fundingKind == 'other' and ( value < Decimal('0.8') or value > Decimal('1')): raise ValidationError( 'yearlyPaymentsPercentage should be greater than 0.8 and less than 1' ) if get_tender(data['__parent__']).fundingKind == 'budget' and ( value < Decimal('0') or value > get_tender( data['__parent__']).yearlyPaymentsPercentageRange): raise ValidationError( 'yearlyPaymentsPercentage should be greater than 0 and less than {}' .format( get_tender( data['__parent__']).yearlyPaymentsPercentageRange))
class Parameter(Model): class Options: serialize_when_none = False roles = RolesFromCsv('Parameter.csv', relative_to=__file__) code = StringType(required=True) value = DecimalType(required=True) def validate_code(self, data, code): if isinstance(data['__parent__']['__parent__'], Model) and \ code not in [i.code for i in (data['__parent__']['__parent__'].features or [])]: raise ValidationError(u"code should be one of feature code.") def validate_value(self, data, value): if isinstance(data['__parent__']['__parent__'], Model): tender = data['__parent__']['__parent__'] codes = dict([(i.code, [x.value for x in i.enum]) for i in (tender.features or [])]) if data['code'] in codes and value not in codes[data['code']]: raise ValidationError(u"value should be one of feature value.")
class Parameter(Model): class Options: serialize_when_none = False roles = RolesFromCsv("Parameter.csv", relative_to=__file__) code = StringType(required=True) value = DecimalType(required=True) def validate_code(self, data, code): if isinstance(data["__parent__"], Model) and code not in [ i.code for i in (get_agreement(data["__parent__"]).features or []) ]: raise ValidationError(u"code should be one of feature code.") def validate_value(self, data, value): if isinstance(data["__parent__"], Model): agreement = get_agreement(data["__parent__"]) codes = dict([(i.code, [x.value for x in i.enum]) for i in (agreement.features or [])]) if data["code"] in codes and value not in codes[data["code"]]: raise ValidationError(u"value should be one of feature value.")
class Value(BaseValue): amount = DecimalType(precision=-2, min_value=Decimal("0.0")) def validate_amount(self, data, amount): pass
class Lot(BaseLot): class Options: roles = { "create": whitelist( "id", "title", "title_en", "title_ru", "description", "description_en", "description_ru", "guarantee", "minimalStepPercentage", "fundingKind", "yearlyPaymentsPercentageRange", ), "edit": whitelist( "title", "title_en", "title_ru", "description", "description_en", "description_ru", "guarantee", "minimalStepPercentage", "fundingKind", "yearlyPaymentsPercentageRange", ), "embedded": embedded_lot_role, "view": default_lot_role, "default": default_lot_role, "auction_view": default_lot_role, "auction_patch": whitelist("id", "auctionUrl"), "chronograph": whitelist("id", "auctionPeriod"), "chronograph_view": whitelist("id", "auctionPeriod", "numberOfBids", "status"), "Administrator": whitelist("auctionPeriod", "yearlyPaymentsPercentageRange"), } minValue = ModelType(Value, required=False, default={"amount": 0, "currency": "UAH", "valueAddedTaxIncluded": True}) 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 ) auctionPeriod = ModelType(LotAuctionPeriod, default={}) auctionUrl = URLType() guarantee = ModelType(Guarantee) 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 ) @serializable def numberOfBids(self): """A property that is serialized by schematics exports.""" bids = [ bid for bid in self.__parent__.bids if self.id in [i.relatedLot for i in bid.lotValues if i.status in ["active", "pending"]] and bid.status in ["active", "pending"] ] return len(bids) @serializable(serialized_name="guarantee", serialize_when_none=False, type=ModelType(Guarantee)) def lot_guarantee(self): if self.guarantee: currency = self.__parent__.guarantee.currency if self.__parent__.guarantee else self.guarantee.currency return Guarantee(dict(amount=self.guarantee.amount, currency=currency)) @serializable(serialized_name="fundingKind") def lot_fundingKind(self): return self.__parent__.fundingKind # if self.__parent__.fundingKind else self.fundingKind @serializable(serialized_name="minimalStep", type=ModelType(Value), serialize_when_none=False) def lot_minimalStep(self): pass # return Value(dict(amount=self.minimalStep.amount, # currency=self.__parent__.minimalStep.currency, # valueAddedTaxIncluded=self.__parent__.minimalStep.valueAddedTaxIncluded)) @serializable(serialized_name="minValue", type=ModelType(Value)) def lot_minValue(self): return Value( dict( amount=self.minValue.amount, currency=self.__parent__.minValue.currency, valueAddedTaxIncluded=self.__parent__.minValue.valueAddedTaxIncluded, ) ) def validate_yearlyPaymentsPercentageRange(self, data, value): parent = data["__parent__"] if parent["fundingKind"] == "other" and value != Decimal("0.8"): raise ValidationError("when tender fundingKind is other, yearlyPaymentsPercentageRange should be equal 0.8") if parent["fundingKind"] == "budget" and (value > Decimal("0.8") or value < Decimal("0")): raise ValidationError( "when tender fundingKind is budget, yearlyPaymentsPercentageRange should be less or equal 0.8, and more or equal 0" )
class Value(BaseValue): amount = DecimalType(required=True, precision=-2, min_value=Decimal("0.0"))
class Parameter(BaseParameter): class Options: roles = RolesFromCsv("Parameter.csv", relative_to=__file__) value = DecimalType(required=True, precision=-2)
class FeatureValue(BaseFeatureValue): value = DecimalType(required=True, precision=-2, min_value=Decimal("0.0"), max_value=Decimal("0.3"))
class Tender(BaseTender): """ ESCO Tender model """ class Options: roles = { 'plain': plain_role, 'create': create_role_eu + blacklist('minValue', 'tender_minValue', 'minimalStep', 'tender_minimalStep', 'noticePublicationDate', 'tender_noticePublicationDate'), 'edit': edit_role_eu + blacklist('minValue', 'tender_minValue', 'minimalStep', 'tender_minimalStep', 'noticePublicationDate', 'tender_noticePublicationDate'), 'edit_draft': edit_role_eu + blacklist('minValue', 'tender_minValue', 'minimalStep', 'tender_minimalStep', 'noticePublicationDate', 'tender_noticePublicationDate'), 'edit_active.tendering': edit_role_eu + blacklist('minValue', 'tender_minValue', 'minimalStep', 'tender_minimalStep', 'noticePublicationDate', 'tender_noticePublicationDate'), '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 + whitelist('NBUdiscountRate', 'minimalStepPercentage', 'yearlyPaymentsPercentageRange', 'fundingKind', 'procurementMethodType', 'noticePublicationDate'), '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 + whitelist('yearlyPaymentsPercentageRange'), 'default': schematics_default_role, 'contracting': whitelist('doc_id', 'owner'), } procurementMethodType = StringType(default="esco") title_en = StringType(required=True, min_length=1) 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. 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), default=list()) contracts = ListType(ModelType(Contract), 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), 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) documents = ListType( ModelType(Document), default=list()) # All documents and attachments related to the tender. qualifications = ListType(ModelType(Qualification), 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_accreditation = 3 edit_accreditation = 4 special_fields = ['fundingKind', 'yearlyPaymentsPercentageRange'] procuring_entity_kinds = ['general', 'special', 'defense'] 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_tender'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'upload_tender_documents'), (Allow, '{}_{}'.format(self.owner, self.owner_token), 'edit_complaint'), ]) return acl @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=calculate_business_date( endDate, ENQUIRY_STAND_STILL_TIME, self, True))) @serializable(type=ModelType(Period)) def complaintPeriod(self): normalized_end = calculate_normalized_date(self.tenderPeriod.endDate, self) return Period( dict(startDate=self.tenderPeriod.startDate, endDate=calculate_business_date(normalized_end, -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 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) 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 # 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 @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.__parent__.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 # if value and value.amount and data.get('minValue'): # if data.get('minValue').currency != value.currency: # raise ValidationError(u"currency should be identical to currency of minValue of tender") # if data.get('minValue').valueAddedTaxIncluded != value.valueAddedTaxIncluded: # raise ValidationError(u"valueAddedTaxIncluded should be identical to valueAddedTaxIncluded of minValue of tender") 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)) 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_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 Lot(BaseLot): class Options: roles = { 'create': whitelist('id', 'title', 'title_en', 'title_ru', 'description', 'description_en', 'description_ru', 'guarantee', 'minimalStepPercentage', 'fundingKind', 'yearlyPaymentsPercentageRange'), 'edit': whitelist('title', 'title_en', 'title_ru', 'description', 'description_en', 'description_ru', 'guarantee', 'minimalStepPercentage', 'fundingKind', 'yearlyPaymentsPercentageRange'), 'embedded': embedded_lot_role, 'view': default_lot_role, 'default': default_lot_role, 'auction_view': default_lot_role, 'auction_patch': whitelist('id', 'auctionUrl'), 'chronograph': whitelist('id', 'auctionPeriod'), 'chronograph_view': whitelist('id', 'auctionPeriod', 'numberOfBids', 'status'), 'Administrator': whitelist('auctionPeriod', 'yearlyPaymentsPercentageRange'), } minValue = ModelType(Value, required=False, default={ 'amount': 0, 'currency': 'UAH', 'valueAddedTaxIncluded': True }) 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) auctionPeriod = ModelType(LotAuctionPeriod, default={}) auctionUrl = URLType() guarantee = ModelType(Guarantee) 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) @serializable def numberOfBids(self): """A property that is serialized by schematics exports.""" bids = [ bid for bid in self.__parent__.bids if self.id in [ i.relatedLot for i in bid.lotValues if i.status in ["active", "pending"] ] and bid.status in ["active", "pending"] ] return len(bids) @serializable(serialized_name="guarantee", serialize_when_none=False, type=ModelType(Guarantee)) def lot_guarantee(self): if self.guarantee: currency = self.__parent__.guarantee.currency if self.__parent__.guarantee else self.guarantee.currency return Guarantee( dict(amount=self.guarantee.amount, currency=currency)) @serializable(serialized_name="fundingKind") def lot_fundingKind(self): return self.__parent__.fundingKind # if self.__parent__.fundingKind else self.fundingKind @serializable(serialized_name="minimalStep", type=ModelType(Value), serialize_when_none=False) def lot_minimalStep(self): pass # return Value(dict(amount=self.minimalStep.amount, # currency=self.__parent__.minimalStep.currency, # valueAddedTaxIncluded=self.__parent__.minimalStep.valueAddedTaxIncluded)) @serializable(serialized_name="minValue", type=ModelType(Value)) def lot_minValue(self): return Value( dict(amount=self.minValue.amount, currency=self.__parent__.minValue.currency, valueAddedTaxIncluded=self.__parent__.minValue. valueAddedTaxIncluded)) def validate_yearlyPaymentsPercentageRange(self, data, value): if data['__parent__']['fundingKind'] == 'other' and value != Decimal( '0.8'): raise ValidationError( 'when tender fundingKind is other, yearlyPaymentsPercentageRange should be equal 0.8' ) if data['__parent__']['fundingKind'] == 'budget' and ( value > Decimal('0.8') or value < Decimal('0')): raise ValidationError( 'when tender fundingKind is budget, yearlyPaymentsPercentageRange should be less or equal 0.8, and more or equal 0' )
class Guarantee(BaseGuarantee): amount = DecimalType(required=True, precision=-2, min_value=Decimal("0.0")) # Amount as a number.
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 + 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, "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", "status"] 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 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"), (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 @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.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 @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 period: if is_new_created(data): _validate_tender_period_start_date(data, period) _validate_tender_period_duration(data, period, TENDERING_DURATION) 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 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" # Not required milestones def validate_milestones(self, data, value): pass
class Value(BaseValue): amount = DecimalType(required=True, precision=-2, min_value=Decimal('0')) class Options: roles = {'edit': whitelist('amount')}