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)
Пример #2
0
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)
Пример #4
0
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"],
    )
Пример #6
0
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])
Пример #7
0
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,
    )
Пример #8
0
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