def save(self): failed = False for form in self.forms: # Every form represents a CartPosition or OrderPosition with questions attached if not form.is_valid(): failed = True else: # This form was correctly filled, so we store the data as # answers to the questions / in the CartPosition object for k, v in form.cleaned_data.items(): if k == 'attendee_name': form.pos.attendee_name = v if v != '' else None form.pos.save() elif k.startswith('question_') and v is not None: field = form.fields[k] if hasattr(field, 'answer'): # We already have a cached answer object, so we don't # have to create a new one if v == '': field.answer.delete() else: self._save_to_answer(field, field.answer, v) field.answer.save() elif v != '': answer = QuestionAnswer( cartposition=(form.pos if isinstance(form.pos, CartPosition) else None), orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None), question=field.question, ) self._save_to_answer(field, answer, v) answer.save() return not failed
def save(self): failed = False for form in self.forms: meta_info = form.pos.meta_info_data # Every form represents a CartPosition or OrderPosition with questions attached if not form.is_valid(): failed = True else: # This form was correctly filled, so we store the data as # answers to the questions / in the CartPosition object for k, v in form.cleaned_data.items(): if k == 'attendee_name_parts': form.pos.attendee_name_parts = v if v else None elif k == 'attendee_email': form.pos.attendee_email = v if v != '' else None elif k == 'company': form.pos.company = v if v != '' else None elif k == 'street': form.pos.street = v if v != '' else None elif k == 'zipcode': form.pos.zipcode = v if v != '' else None elif k == 'city': form.pos.city = v if v != '' else None elif k == 'country': form.pos.country = v if v != '' else None elif k == 'state': form.pos.state = v if v != '' else None elif k.startswith('question_'): field = form.fields[k] if hasattr(field, 'answer'): # We already have a cached answer object, so we don't # have to create a new one if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \ or (isinstance(v, QuerySet) and not v.exists()): if field.answer.file: field.answer.file.delete() field.answer.delete() else: self._save_to_answer(field, field.answer, v) field.answer.save() elif v != '' and v is not None: answer = QuestionAnswer( cartposition=(form.pos if isinstance(form.pos, CartPosition) else None), orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None), question=field.question, ) self._save_to_answer(field, answer, v) answer.save() else: meta_info.setdefault('question_form_data', {}) if v is None: if k in meta_info['question_form_data']: del meta_info['question_form_data'][k] else: meta_info['question_form_data'][k] = v form.pos.meta_info = json.dumps(meta_info) form.pos.save() return not failed
def save(self): failed = False for form in self.forms: meta_info = form.pos.meta_info_data # Every form represents a CartPosition or OrderPosition with questions attached if not form.is_valid(): failed = True else: # This form was correctly filled, so we store the data as # answers to the questions / in the CartPosition object for k, v in form.cleaned_data.items(): if k == 'attendee_name_parts': form.pos.attendee_name_parts = v if v else None form.pos.save() elif k == 'attendee_email': form.pos.attendee_email = v if v != '' else None form.pos.save() elif k.startswith('question_'): field = form.fields[k] if hasattr(field, 'answer'): # We already have a cached answer object, so we don't # have to create a new one if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \ or (isinstance(v, QuerySet) and not v.exists()): if field.answer.file: field.answer.file.delete() field.answer.delete() else: self._save_to_answer(field, field.answer, v) field.answer.save() elif v != '' and v is not None: answer = QuestionAnswer( cartposition=(form.pos if isinstance(form.pos, CartPosition) else None), orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None), question=field.question, ) self._save_to_answer(field, answer, v) answer.save() else: meta_info.setdefault('question_form_data', {}) if v is None: if k in meta_info['question_form_data']: del meta_info['question_form_data'][k] else: meta_info['question_form_data'][k] = v form.pos.meta_info = json.dumps(meta_info) form.pos.save(update_fields=['meta_info']) return not failed
def assign(self, value, order, position, invoice_address, **kwargs): if value: if not hasattr(order, '_answers'): order._answers = [] if isinstance(value, QuestionOption): a = QuestionAnswer(orderposition=position, question=self.q, answer=str(value)) a._options = [value] order._answers.append(a) elif isinstance(value, list): a = QuestionAnswer(orderposition=position, question=self.q, answer=', '.join(str(v) for v in value)) a._options = value order._answers.append(a) else: order._answers.append(QuestionAnswer(question=self.q, answer=str(value), orderposition=position))
def create(self, validated_data): fees_data = validated_data.pop('fees') if 'fees' in validated_data else [] positions_data = validated_data.pop('positions') if 'positions' in validated_data else [] payment_provider = validated_data.pop('payment_provider', None) payment_info = validated_data.pop('payment_info', '{}') payment_date = validated_data.pop('payment_date', now()) force = validated_data.pop('force', False) simulate = validated_data.pop('simulate', False) self._send_mail = validated_data.pop('send_mail', False) if 'invoice_address' in validated_data: iadata = validated_data.pop('invoice_address') name = iadata.pop('name', '') if name and not iadata.get('name_parts'): iadata['name_parts'] = { '_legacy': name } ia = InvoiceAddress(**iadata) else: ia = None lockfn = self.context['event'].lock if simulate: lockfn = NoLockManager with lockfn() as now_dt: free_seats = set() seats_seen = set() consume_carts = validated_data.pop('consume_carts', []) delete_cps = [] quota_avail_cache = {} v_budget = {} voucher_usage = Counter() if consume_carts: for cp in CartPosition.objects.filter( event=self.context['event'], cart_id__in=consume_carts, expires__gt=now() ): quotas = (cp.variation.quotas.filter(subevent=cp.subevent) if cp.variation else cp.item.quotas.filter(subevent=cp.subevent)) for quota in quotas: if quota not in quota_avail_cache: quota_avail_cache[quota] = list(quota.availability()) if quota_avail_cache[quota][1] is not None: quota_avail_cache[quota][1] += 1 if cp.voucher: voucher_usage[cp.voucher] -= 1 if cp.expires > now_dt: if cp.seat: free_seats.add(cp.seat) delete_cps.append(cp) errs = [{} for p in positions_data] for i, pos_data in enumerate(positions_data): if pos_data.get('voucher'): v = pos_data['voucher'] if pos_data.get('addon_to'): errs[i]['voucher'] = ['Vouchers are currently not supported for add-on products.'] continue if not v.applies_to(pos_data['item'], pos_data.get('variation')): errs[i]['voucher'] = [error_messages['voucher_invalid_item']] continue if v.subevent_id and pos_data.get('subevent').pk != v.subevent_id: errs[i]['voucher'] = [error_messages['voucher_invalid_subevent']] continue if v.valid_until is not None and v.valid_until < now_dt: errs[i]['voucher'] = [error_messages['voucher_expired']] continue voucher_usage[v] += 1 if voucher_usage[v] > 0: redeemed_in_carts = CartPosition.objects.filter( Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt) ).exclude(pk__in=[cp.pk for cp in delete_cps]) v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count() if v_avail < voucher_usage[v]: errs[i]['voucher'] = [ 'The voucher has already been used the maximum number of times.' ] if v.budget is not None: price = pos_data.get('price') if price is None: price = get_price( item=pos_data.get('item'), variation=pos_data.get('variation'), voucher=v, custom_price=None, subevent=pos_data.get('subevent'), addon_to=pos_data.get('addon_to'), invoice_address=ia, ).gross pbv = get_price( item=pos_data['item'], variation=pos_data.get('variation'), voucher=None, custom_price=None, subevent=pos_data.get('subevent'), addon_to=pos_data.get('addon_to'), invoice_address=ia, ) if v not in v_budget: v_budget[v] = v.budget - v.budget_used() disc = pbv.gross - price if disc > v_budget[v]: new_disc = v_budget[v] v_budget[v] -= new_disc if new_disc == Decimal('0.00') or pos_data.get('price') is not None: errs[i]['voucher'] = [ 'The voucher has a remaining budget of {}, therefore a discount of {} can not be ' 'given.'.format(v_budget[v] + new_disc, disc) ] continue pos_data['price'] = price + (disc - new_disc) else: v_budget[v] -= disc seated = pos_data.get('item').seat_category_mappings.filter(subevent=pos_data.get('subevent')).exists() if pos_data.get('seat'): if not seated: errs[i]['seat'] = ['The specified product does not allow to choose a seat.'] try: seat = self.context['event'].seats.get(seat_guid=pos_data['seat'], subevent=pos_data.get('subevent')) except Seat.DoesNotExist: errs[i]['seat'] = ['The specified seat does not exist.'] else: pos_data['seat'] = seat if (seat not in free_seats and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat in seats_seen: errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)] seats_seen.add(seat) elif seated: errs[i]['seat'] = ['The specified product requires to choose a seat.'] if not force: for i, pos_data in enumerate(positions_data): if pos_data.get('voucher'): if pos_data['voucher'].allow_ignore_quota or pos_data['voucher'].block_quota: continue new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent')) if pos_data.get('variation') else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent'))) if len(new_quotas) == 0: errs[i]['item'] = [gettext_lazy('The product "{}" is not assigned to a quota.').format( str(pos_data.get('item')) )] else: for quota in new_quotas: if quota not in quota_avail_cache: quota_avail_cache[quota] = list(quota.availability()) if quota_avail_cache[quota][1] is not None: quota_avail_cache[quota][1] -= 1 if quota_avail_cache[quota][1] < 0: errs[i]['item'] = [ gettext_lazy('There is not enough quota available on quota "{}" to perform the operation.').format( quota.name ) ] if any(errs): raise ValidationError({'positions': errs}) if validated_data.get('locale', None) is None: validated_data['locale'] = self.context['event'].settings.locale order = Order(event=self.context['event'], **validated_data) order.set_expires(subevents=[p.get('subevent') for p in positions_data]) order.meta_info = "{}" order.total = Decimal('0.00') if simulate: order = WrappedModel(order) order.last_modified = now() order.code = 'PREVIEW' else: order.save() if ia: if not simulate: ia.order = order ia.save() else: order.invoice_address = ia ia.last_modified = now() pos_map = {} for pos_data in positions_data: answers_data = pos_data.pop('answers', []) addon_to = pos_data.pop('addon_to', None) attendee_name = pos_data.pop('attendee_name', '') if attendee_name and not pos_data.get('attendee_name_parts'): pos_data['attendee_name_parts'] = { '_legacy': attendee_name } pos = OrderPosition(**pos_data) if simulate: pos.order = order._wrapped else: pos.order = order if addon_to: pos.addon_to = pos_map[addon_to] if pos.price is None: price = get_price( item=pos.item, variation=pos.variation, voucher=pos.voucher, custom_price=None, subevent=pos.subevent, addon_to=pos.addon_to, invoice_address=ia, ) pos.price = price.gross pos.tax_rate = price.rate pos.tax_value = price.tax pos.tax_rule = pos.item.tax_rule else: pos._calculate_tax() pos.price_before_voucher = get_price( item=pos.item, variation=pos.variation, voucher=None, custom_price=None, subevent=pos.subevent, addon_to=pos.addon_to, invoice_address=ia, ).gross if simulate: pos = WrappedModel(pos) pos.id = 0 answers = [] for answ_data in answers_data: options = answ_data.pop('options', []) answ = WrappedModel(QuestionAnswer(**answ_data)) answ.options = WrappedList(options) answers.append(answ) pos.answers = answers pos.pseudonymization_id = "PREVIEW" else: if pos.voucher: Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1) pos.save() for answ_data in answers_data: options = answ_data.pop('options', []) answ = pos.answers.create(**answ_data) answ.options.add(*options) pos_map[pos.positionid] = pos if not simulate: for cp in delete_cps: cp.delete() order.total = sum([p.price for p in pos_map.values()]) fees = [] for fee_data in fees_data: is_percentage = fee_data.pop('_treat_value_as_percentage', False) if is_percentage: fee_data['value'] = round_decimal(order.total * (fee_data['value'] / Decimal('100.00')), self.context['event'].currency) is_split_taxes = fee_data.pop('_split_taxes_like_products', False) if is_split_taxes: d = defaultdict(lambda: Decimal('0.00')) trz = TaxRule.zero() for p in pos_map.values(): tr = p.tax_rule d[tr] += p.price - p.tax_value base_values = sorted([tuple(t) for t in d.items()], key=lambda t: (t[0] or trz).rate) sum_base = sum(t[1] for t in base_values) fee_values = [(t[0], round_decimal(fee_data['value'] * t[1] / sum_base, self.context['event'].currency)) for t in base_values] sum_fee = sum(t[1] for t in fee_values) # If there are rounding differences, we fix them up, but always leaning to the benefit of the tax # authorities if sum_fee > fee_data['value']: fee_values[0] = (fee_values[0][0], fee_values[0][1] + (fee_data['value'] - sum_fee)) elif sum_fee < fee_data['value']: fee_values[-1] = (fee_values[-1][0], fee_values[-1][1] + (fee_data['value'] - sum_fee)) for tr, val in fee_values: fee_data['tax_rule'] = tr fee_data['value'] = val f = OrderFee(**fee_data) f.order = order._wrapped if simulate else order f._calculate_tax() fees.append(f) if not simulate: f.save() else: f = OrderFee(**fee_data) f.order = order._wrapped if simulate else order f._calculate_tax() fees.append(f) if not simulate: f.save() order.total += sum([f.value for f in fees]) if simulate: order.fees = fees order.positions = pos_map.values() return order # ignore payments else: order.save(update_fields=['total']) if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider: payment_provider = 'free' if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID: order.status = Order.STATUS_PAID order.save() order.payments.create( amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED, payment_date=now() ) elif payment_provider == "free" and order.total != Decimal('0.00'): raise ValidationError('You cannot use the "free" payment provider for non-free orders.') elif validated_data.get('status') == Order.STATUS_PAID: if not payment_provider: raise ValidationError('You cannot create a paid order without a payment provider.') order.payments.create( amount=order.total, provider=payment_provider, info=payment_info, payment_date=payment_date, state=OrderPayment.PAYMENT_STATE_CONFIRMED ) elif payment_provider: order.payments.create( amount=order.total, provider=payment_provider, info=payment_info, state=OrderPayment.PAYMENT_STATE_CREATED ) return order
def save(self): failed = False for form in self.forms: meta_info = form.pos.meta_info_data # Every form represents a CartPosition or OrderPosition with questions attached if not form.is_valid(): failed = True else: # This form was correctly filled, so we store the data as # answers to the questions / in the CartPosition object for k, v in form.cleaned_data.items(): if k == 'attendee_name_parts': form.pos.attendee_name_parts = v if v else None elif k == 'attendee_email': form.pos.attendee_email = v if v != '' else None elif k == 'company': form.pos.company = v if v != '' else None elif k == 'street': form.pos.street = v if v != '' else None elif k == 'zipcode': form.pos.zipcode = v if v != '' else None elif k == 'city': form.pos.city = v if v != '' else None elif k == 'country': form.pos.country = v if v != '' else None elif k == 'state': form.pos.state = v if v != '' else None elif k.startswith('question_'): field = form.fields[k] if hasattr(field, 'answer'): # We already have a cached answer object, so we don't # have to create a new one if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \ or (isinstance(v, QuerySet) and not v.exists()): if field.answer.file: field.answer.file.delete() field.answer.delete() else: self._save_to_answer(field, field.answer, v) field.answer.save() elif v != '' and v is not None: answer = QuestionAnswer( cartposition=(form.pos if isinstance( form.pos, CartPosition) else None), orderposition=(form.pos if isinstance( form.pos, OrderPosition) else None), question=field.question, ) try: self._save_to_answer(field, answer, v) answer.save() except IntegrityError: # Since we prefill ``field.answer`` at form creation time, there's a possible race condition # here if the users submits their save request a second time while the first one is still running, # thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper" # fix would be a transaction with select_for_update(), or at least fetching using get_or_create here # again. However, both of these approaches have a significant performance overhead for *all* requests, # while the issue happens very very rarely. So we opt for just catching the error and retrying properly. answer = QuestionAnswer.objects.get( cartposition=(form.pos if isinstance( form.pos, CartPosition) else None), orderposition=(form.pos if isinstance( form.pos, OrderPosition) else None), question=field.question, ) self._save_to_answer(field, answer, v) answer.save() else: meta_info.setdefault('question_form_data', {}) if v is None: if k in meta_info['question_form_data']: del meta_info['question_form_data'][k] else: meta_info['question_form_data'][k] = v form.pos.meta_info = json.dumps(meta_info) form.pos.save() return not failed
def save(self): failed = False for form in self.forms: meta_info = form.pos.meta_info_data # Every form represents a CartPosition or OrderPosition with questions attached if not form.is_valid(): failed = True else: if form.cleaned_data.get('saved_id'): prof = AttendeeProfile.objects.filter( customer=self.cart_customer, pk=form.cleaned_data.get( 'saved_id')).first() or AttendeeProfile( customer=getattr(self, 'cart_customer', None)) answers_key_to_index = { a.get('field_name'): i for i, a in enumerate(prof.answers) } else: prof = AttendeeProfile( customer=getattr(self, 'cart_customer', None)) answers_key_to_index = {} # This form was correctly filled, so we store the data as # answers to the questions / in the CartPosition object for k, v in form.cleaned_data.items(): if k in ('save', 'saved_id'): continue elif k == 'attendee_name_parts': form.pos.attendee_name_parts = v if v else None prof.attendee_name_parts = form.pos.attendee_name_parts prof.attendee_name_cached = form.pos.attendee_name elif k in ('attendee_email', 'company', 'street', 'zipcode', 'city', 'country', 'state'): v = v if v != '' else None setattr(form.pos, k, v) setattr(prof, k, v) elif k.startswith('question_'): field = form.fields[k] if hasattr(field, 'answer'): # We already have a cached answer object, so we don't # have to create a new one if v == '' or v is None or (isinstance(field, forms.FileField) and v is False) \ or (isinstance(v, QuerySet) and not v.exists()): if field.answer.file: field.answer.file.delete() field.answer.delete() else: self._save_to_answer(field, field.answer, v) field.answer.save() if isinstance( field, forms.ModelMultipleChoiceField ) or isinstance(field, forms.ModelChoiceField): answer_value = { o.identifier: str(o) for o in field.answer.options.all() } elif isinstance(field, forms.BooleanField): answer_value = bool(field.answer.answer) else: answer_value = str(field.answer.answer) answer_dict = { 'field_name': k, 'field_label': str(field.label), 'value': answer_value, 'question_type': field.question.type, 'question_identifier': field.question.identifier, } if k in answers_key_to_index: prof.answers[ answers_key_to_index[k]] = answer_dict else: prof.answers.append(answer_dict) elif v != '' and v is not None: answer = QuestionAnswer( cartposition=(form.pos if isinstance( form.pos, CartPosition) else None), orderposition=(form.pos if isinstance( form.pos, OrderPosition) else None), question=field.question, ) try: self._save_to_answer(field, answer, v) answer.save() except IntegrityError: # Since we prefill ``field.answer`` at form creation time, there's a possible race condition # here if the users submits their save request a second time while the first one is still running, # thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper" # fix would be a transaction with select_for_update(), or at least fetching using get_or_create here # again. However, both of these approaches have a significant performance overhead for *all* requests, # while the issue happens very very rarely. So we opt for just catching the error and retrying properly. answer = QuestionAnswer.objects.get( cartposition=(form.pos if isinstance( form.pos, CartPosition) else None), orderposition=(form.pos if isinstance( form.pos, OrderPosition) else None), question=field.question, ) self._save_to_answer(field, answer, v) answer.save() if isinstance(field, forms.ModelMultipleChoiceField ) or isinstance( field, forms.ModelChoiceField): answer_value = { o.identifier: str(o) for o in answer.options.all() } elif isinstance(field, forms.BooleanField): answer_value = bool(answer.answer) else: answer_value = str(answer.answer) answer_dict = { 'field_name': k, 'field_label': str(field.label), 'value': answer_value, 'question_type': field.question.type, 'question_identifier': field.question.identifier, } if k in answers_key_to_index: prof.answers[ answers_key_to_index[k]] = answer_dict else: prof.answers.append(answer_dict) else: field = form.fields[k] meta_info.setdefault('question_form_data', {}) if v is None: if k in meta_info['question_form_data']: del meta_info['question_form_data'][k] else: meta_info['question_form_data'][k] = v answer_dict = { 'field_name': k, 'field_label': str(field.label), 'value': str(v), 'question_type': None, 'question_identifier': None, } if k in answers_key_to_index: prof.answers[answers_key_to_index[k]] = answer_dict else: prof.answers.append(answer_dict) form.pos.meta_info = json.dumps(meta_info) form.pos.save() if form.cleaned_data.get('save') and not failed: prof.save() self.cart_session[ f'saved_attendee_profile_{form.pos.pk}'] = prof.pk return not failed