def from_orm(cls, stock: Stock): # type: ignore activation_code = (ActivationCode.query.filter( ActivationCode.stockId == stock.id).first() if stock.canHaveActivationCodes else None) stock.hasActivationCodes = bool(activation_code) stock.activationCodesExpirationDatetime = activation_code.expirationDate if activation_code else None return super().from_orm(stock)
def test_queryNotSoftDeleted(): alive = factories.StockFactory() deleted = factories.StockFactory(isSoftDeleted=True) stocks = Stock.queryNotSoftDeleted().all() assert len(stocks) == 1 assert alive in stocks assert deleted not in stocks
def from_orm(cls, stock: Stock): # type: ignore # here we have N+1 requests (for each stock we query an activation code) # but it should be more efficient than loading all activationCodes of all stocks stock.hasActivationCode = ( stock.canHaveActivationCodes and offers_repository.get_available_activation_code(stock) is not None ) return super().from_orm(stock)
def _edit_stock( stock: Stock, price: float, quantity: int, beginning: datetime.datetime, booking_limit_datetime: datetime.datetime, ) -> Stock: # FIXME (dbaty, 2020-11-25): We need this ugly workaround because # the frontend sends us datetimes like "2020-12-03T14:00:00Z" # (note the "Z" suffix). Pydantic deserializes it as a datetime # *with* a timezone. However, datetimes are stored in the database # as UTC datetimes *without* any timezone. Thus, we wrongly detect # a change for the "beginningDatetime" field for Allocine stocks: # because we do not allow it to be changed, we raise an error when # we should not. def as_utc_without_timezone(d: datetime.datetime) -> datetime.datetime: return d.astimezone(pytz.utc).replace(tzinfo=None) if beginning: beginning = as_utc_without_timezone(beginning) if booking_limit_datetime: booking_limit_datetime = as_utc_without_timezone( booking_limit_datetime) validation.check_stock_is_updatable(stock) validation.check_required_dates_for_stock(stock.offer, beginning, booking_limit_datetime) validation.check_stock_price(price, stock.offer) validation.check_stock_quantity(quantity, stock.dnBookedQuantity) validation.check_activation_codes_expiration_datetime_on_stock_edition( stock.activationCodes, booking_limit_datetime, ) updates = { "price": price, "quantity": quantity, "beginningDatetime": beginning, "bookingLimitDatetime": booking_limit_datetime, } # fmt: off updated_fields = { attr for attr, new_value in updates.items() if new_value != getattr(stock, attr) } # fmt: on if "price" in updated_fields: validation.check_stock_has_no_custom_reimbursement_rule(stock) if stock.offer.isFromAllocine: validation.check_update_only_allowed_stock_fields_for_allocine_offer( updated_fields) stock.fieldsUpdated = list(updated_fields) for model_attr, value in updates.items(): setattr(stock, model_attr, value) return stock
def _build_stock_from_stock_detail(stock_detail: Dict, offers_id: int) -> Stock: return Stock( quantity=stock_detail["available_quantity"], bookingLimitDatetime=None, offerId=offers_id, price=stock_detail["price"], dateModified=datetime.now(), idAtProviders=stock_detail["stocks_fnac_reference"], )
def delete_stock(stock: Stock) -> None: validation.check_stock_is_deletable(stock) stock.isSoftDeleted = True repository.save(stock) # the algolia sync for the stock will happen within this function cancelled_bookings = cancel_bookings_when_offerer_deletes_stock(stock) logger.info( "Deleted stock and cancelled its bookings", extra={ "stock": stock.id, "bookings": [b.id for b in cancelled_bookings] }, ) if cancelled_bookings: for booking in cancelled_bookings: try: user_emails.send_warning_to_user_after_pro_booking_cancellation( booking) except mailing.MailServiceException as exc: logger.exception( "Could not notify beneficiary about deletion of stock", extra={ "exc": str(exc), "stock": stock.id, "booking": booking.id, }, ) try: user_emails.send_offerer_bookings_recap_email_after_offerer_cancellation( cancelled_bookings) except mailing.MailServiceException as exc: logger.exception( "Could not notify offerer about deletion of stock", extra={ "exc": str(exc), "stock": stock.id, }, ) send_cancel_booking_notification.delay( [booking.id for booking in cancelled_bookings])
def _create_stock( offer: Offer, price: float, quantity: int = None, beginning: datetime.datetime = None, booking_limit_datetime: datetime.datetime = None, ) -> Stock: validation.check_required_dates_for_stock(offer, beginning, booking_limit_datetime) validation.check_stock_can_be_created_for_offer(offer) validation.check_stock_price(price, offer) validation.check_stock_quantity(quantity) return Stock( offer=offer, price=price, quantity=quantity, beginningDatetime=beginning, bookingLimitDatetime=booking_limit_datetime, )
def upsert_stocks(offer_id: int, stock_data_list: list[Union[StockCreationBodyModel, StockEditionBodyModel]], user: User) -> list[Stock]: activation_codes = [] stocks = [] edited_stocks = [] edited_stocks_previous_beginnings = {} offer = offer_queries.get_offer_by_id(offer_id) for stock_data in stock_data_list: if isinstance(stock_data, StockEditionBodyModel): stock = (Stock.queryNotSoftDeleted().filter_by( id=stock_data.id).options(joinedload( Stock.activationCodes)).first_or_404()) if stock.offerId != offer_id: errors = ApiErrors() errors.add_error( "global", "Vous n'avez pas les droits d'accès suffisant pour accéder à cette information." ) errors.status_code = 403 raise errors edited_stocks_previous_beginnings[ stock.id] = stock.beginningDatetime edited_stock = _edit_stock( stock, price=stock_data.price, quantity=stock_data.quantity, beginning=stock_data.beginning_datetime, booking_limit_datetime=stock_data.booking_limit_datetime, ) edited_stocks.append(edited_stock) stocks.append(edited_stock) else: activation_codes_exist = stock_data.activation_codes is not None and len( stock_data.activation_codes) > 0 # type: ignore[arg-type] if activation_codes_exist: validation.check_offer_is_digital(offer) validation.check_activation_codes_expiration_datetime( stock_data.activation_codes_expiration_datetime, stock_data.booking_limit_datetime, ) quantity = len( stock_data.activation_codes ) if activation_codes_exist else stock_data.quantity # type: ignore[arg-type] created_stock = _create_stock( offer=offer, price=stock_data.price, quantity=quantity, beginning=stock_data.beginning_datetime, booking_limit_datetime=stock_data.booking_limit_datetime, ) if activation_codes_exist: for activation_code in stock_data.activation_codes: # type: ignore[union-attr] activation_codes.append( ActivationCode( code=activation_code, expirationDate=stock_data. activation_codes_expiration_datetime, stock=created_stock, )) stocks.append(created_stock) repository.save(*stocks, *activation_codes) logger.info("Stock has been created or updated", extra={"offer": offer_id}) if offer.validation == OfferValidationStatus.DRAFT: offer.validation = set_offer_status_based_on_fraud_criteria(offer) offer.author = user offer.lastValidationDate = datetime.datetime.utcnow() if offer.validation == OfferValidationStatus.PENDING or offer.validation == OfferValidationStatus.REJECTED: offer.isActive = False repository.save(offer) if offer.validation == OfferValidationStatus.APPROVED: admin_emails.send_offer_creation_notification_to_administration( offer) for stock in edited_stocks: previous_beginning = edited_stocks_previous_beginnings[stock.id] if stock.beginningDatetime != previous_beginning and not stock.offer.isEducational: _notify_beneficiaries_upon_stock_edit(stock) search.async_index_offer_ids([offer.id]) return stocks