class CreditApplicationApplicant(models.Model): first_name = models.CharField(_("First Name"), max_length=15) last_name = models.CharField(_("Last Name"), max_length=20) middle_initial = NullCharField(_("Middle Initial"), max_length=1) date_of_birth = DateOfBirthField(_("Date of Birth")) ssn = USSocialSecurityNumberField(_("Social Security Number")) annual_income = models.IntegerField( _("Annual Income"), validators=[MinValueValidator(0), MaxValueValidator(999999)] ) email_address = models.EmailField( _("Email"), max_length=50, null=True, blank=True, validators=[MinLengthValidator(7)], ) home_phone = PhoneNumberField(_("Home Phone")) mobile_phone = PhoneNumberField(_("Mobile Phone"), null=True, blank=True) work_phone = PhoneNumberField(_("Work Phone"), null=True, blank=True) employer_name = NullCharField(_("Employer Name"), max_length=30) housing_status = NullCharField( _("Housing Status"), max_length=_max_len(HOUSING_STATUSES), choices=HOUSING_STATUSES, ) address = models.ForeignKey( "wellsfargo.CreditApplicationAddress", verbose_name=_("Address"), related_name="+", on_delete=models.CASCADE, )
def test_raises_exception_for_invalid_null_blank_combo(self): with self.assertRaises(ImproperlyConfigured): NullCharField(null=True, blank=False) with self.assertRaises(ImproperlyConfigured): NullCharField(null=False, blank=True) with self.assertRaises(ImproperlyConfigured): NullCharField(null=False, blank=False)
class AccountInquiryResult(AccountNumberMixin, models.Model): credit_app_source = models.ForeignKey( "wellsfargo.CreditApplication", verbose_name=_("Credit Application Source"), related_name="inquiries", null=True, blank=True, on_delete=models.CASCADE, ) prequal_response_source = models.ForeignKey( "wellsfargo.PreQualificationResponse", verbose_name=_("Pre-Qualification Source"), related_name="inquiries", null=True, blank=True, on_delete=models.SET_NULL, ) main_applicant_full_name = NullCharField(_("Main Applicant Name"), max_length=50) joint_applicant_full_name = NullCharField(_("Joint Applicant Name"), max_length=50) main_applicant_address = models.ForeignKey( "wellsfargo.CreditApplicationAddress", verbose_name=_("Main Applicant Address"), related_name="+", null=True, blank=True, on_delete=models.SET_NULL, ) joint_applicant_address = models.ForeignKey( "wellsfargo.CreditApplicationAddress", verbose_name=_("Joint Applicant Address"), related_name="+", null=True, blank=True, on_delete=models.SET_NULL, ) credit_limit = models.DecimalField(_("Account Credit Limit"), decimal_places=2, max_digits=12) available_credit = models.DecimalField(_("Current Available Credit"), decimal_places=2, max_digits=12) created_datetime = models.DateTimeField(auto_now_add=True) modified_datetime = models.DateTimeField(auto_now=True) class Meta: ordering = ("-created_datetime", "-id") verbose_name = _("Account Inquiry Result") verbose_name_plural = _("Account Inquiry Results")
class CreditApplicationAddress(models.Model): address_line_1 = models.CharField(_("Address Line 1"), max_length=26) address_line_2 = NullCharField(_("Address Line 2"), max_length=26) city = models.CharField(_("City"), max_length=18) state_code = USStateField(_("State")) postal_code = USZipCodeField( _("Postcode"), validators=[MinLengthValidator(5), MaxLengthValidator(5)] )
class AbstractProduct(models.Model): """ The base product object There's three kinds of products; they're distinguished by the structure field. - A stand alone product. Regular product that lives by itself. - A child product. All child products have a parent product. They're a specific version of the parent. - A parent product. It essentially represents a set of products. An example could be a yoga course, which is a parent product. The different times/locations of the courses would be associated with the child products. """ STANDALONE, PARENT, CHILD = 'standalone', 'parent', 'child' STRUCTURE_CHOICES = ((STANDALONE, _('Stand-alone product')), (PARENT, _('Parent product')), (CHILD, _('Child product'))) structure = models.CharField(_("Product structure"), max_length=10, choices=STRUCTURE_CHOICES, default=STANDALONE) is_public = models.BooleanField( _('Is public'), default=True, db_index=True, help_text=_( "Show this product in search results and catalogue listings.")) upc = NullCharField( _("UPC"), max_length=64, blank=True, null=True, unique=True, help_text=_("Universal Product Code (UPC) is an identifier for " "a product which is not specific to a particular " " supplier. Eg an ISBN for a book.")) parent = models.ForeignKey( 'self', blank=True, null=True, on_delete=models.CASCADE, related_name='children', verbose_name=_("Parent product"), help_text=_("Only choose a parent product if you're creating a child " "product. For example if this is a size " "4 of a particular t-shirt. Leave blank if this is a " "stand-alone product (i.e. there is only one version of" " this product).")) # Title is mandatory for canonical products but optional for child products title = models.CharField(pgettext_lazy('Product title', 'Title'), max_length=255, blank=True) slug = models.SlugField(_('Slug'), max_length=255, unique=False) description = models.TextField(_('Description'), blank=True) #: "Kind" of product, e.g. T-Shirt, Book, etc. #: None for child products, they inherit their parent's product class product_class = models.ForeignKey( 'catalogue.ProductClass', null=True, blank=True, on_delete=models.PROTECT, verbose_name=_('Product type'), related_name="products", help_text=_("Choose what type of product this is")) attributes = models.ManyToManyField( 'catalogue.ProductAttribute', through='ProductAttributeValue', verbose_name=_("Attributes"), help_text=_("A product attribute is something that this product may " "have, such as a size, as specified by its class")) #: It's possible to have options product class-wide, and per product. product_options = models.ManyToManyField( 'catalogue.Option', blank=True, verbose_name=_("Product options"), help_text=_("Options are values that can be associated with a item " "when it is added to a customer's basket. This could be " "something like a personalised message to be printed on " "a T-shirt.")) recommended_products = models.ManyToManyField( 'catalogue.Product', through='ProductRecommendation', blank=True, verbose_name=_("Recommended products"), help_text=_("These are products that are recommended to accompany the " "main product.")) # Denormalised product rating - used by reviews app. # Product has no ratings if rating is None rating = models.FloatField(_('Rating'), null=True, editable=False) date_created = models.DateTimeField(_("Date created"), auto_now_add=True, db_index=True) # This field is used by Haystack to reindex search date_updated = models.DateTimeField(_("Date updated"), auto_now=True, db_index=True) categories = models.ManyToManyField('catalogue.Category', through='ProductCategory', verbose_name=_("Categories")) #: Determines if a product may be used in an offer. It is illegal to #: discount some types of product (e.g. ebooks) and this field helps #: merchants from avoiding discounting such products #: Note that this flag is ignored for child products; they inherit from #: the parent product. is_discountable = models.BooleanField( _("Is discountable?"), default=True, help_text=_( "This flag indicates if this product can be used in an offer " "or not")) objects = ProductQuerySet.as_manager() # browsable property is deprecated and will be removed in Oscar 2.1 # Use Product.objects.browsable() instead. browsable = BrowsableProductManager() class Meta: abstract = True app_label = 'catalogue' ordering = ['-date_created'] verbose_name = _('Product') verbose_name_plural = _('Products') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.attr = ProductAttributesContainer(product=self) def __str__(self): if self.title: return self.title if self.attribute_summary: return "%s (%s)" % (self.get_title(), self.attribute_summary) else: return self.get_title() def get_absolute_url(self): """ Return a product's absolute URL """ return reverse('catalogue:detail', kwargs={ 'product_slug': self.slug, 'pk': self.id }) def clean(self): """ Validate a product. Those are the rules: +---------------+-------------+--------------+--------------+ | | stand alone | parent | child | +---------------+-------------+--------------+--------------+ | title | required | required | optional | +---------------+-------------+--------------+--------------+ | product class | required | required | must be None | +---------------+-------------+--------------+--------------+ | parent | forbidden | forbidden | required | +---------------+-------------+--------------+--------------+ | stockrecords | 0 or more | forbidden | 0 or more | +---------------+-------------+--------------+--------------+ | categories | 1 or more | 1 or more | forbidden | +---------------+-------------+--------------+--------------+ | attributes | optional | optional | optional | +---------------+-------------+--------------+--------------+ | rec. products | optional | optional | unsupported | +---------------+-------------+--------------+--------------+ | options | optional | optional | forbidden | +---------------+-------------+--------------+--------------+ Because the validation logic is quite complex, validation is delegated to the sub method appropriate for the product's structure. """ getattr(self, '_clean_%s' % self.structure)() if not self.is_parent: self.attr.validate_attributes() def _clean_standalone(self): """ Validates a stand-alone product """ if not self.title: raise ValidationError(_("Your product must have a title.")) if not self.product_class: raise ValidationError(_("Your product must have a product class.")) if self.parent_id: raise ValidationError(_("Only child products can have a parent.")) def _clean_child(self): """ Validates a child product """ if not self.parent_id: raise ValidationError(_("A child product needs a parent.")) if self.parent_id and not self.parent.is_parent: raise ValidationError( _("You can only assign child products to parent products.")) if self.product_class: raise ValidationError( _("A child product can't have a product class.")) if self.pk and self.categories.exists(): raise ValidationError( _("A child product can't have a category assigned.")) # Note that we only forbid options on product level if self.pk and self.product_options.exists(): raise ValidationError(_("A child product can't have options.")) def _clean_parent(self): """ Validates a parent product. """ self._clean_standalone() if self.has_stockrecords: raise ValidationError( _("A parent product can't have stockrecords.")) def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.get_title()) super().save(*args, **kwargs) self.attr.save() # Properties @property def is_standalone(self): return self.structure == self.STANDALONE @property def is_parent(self): return self.structure == self.PARENT @property def is_child(self): return self.structure == self.CHILD def can_be_parent(self, give_reason=False): """ Helps decide if a the product can be turned into a parent product. """ reason = None if self.is_child: reason = _('The specified parent product is a child product.') if self.has_stockrecords: reason = _("One can't add a child product to a product with stock" " records.") is_valid = reason is None if give_reason: return is_valid, reason else: return is_valid @property def options(self): """ Returns a set of all valid options for this product. It's possible to have options product class-wide, and per product. """ pclass_options = self.get_product_class().options.all() return pclass_options | self.product_options.all() @cached_property def has_options(self): # Extracting annotated value with number of product class options # from product list queryset. has_product_class_options = getattr(self, 'has_product_class_options', None) has_product_options = getattr(self, 'has_product_options', None) if has_product_class_options is not None and has_product_options is not None: return has_product_class_options or has_product_options return self.options.exists() @property def is_shipping_required(self): return self.get_product_class().requires_shipping @property def has_stockrecords(self): """ Test if this product has any stockrecords """ return self.stockrecords.exists() @property def num_stockrecords(self): return self.stockrecords.count() @property def attribute_summary(self): """ Return a string of all of a product's attributes """ attributes = self.attribute_values.all() pairs = [attribute.summary() for attribute in attributes] return ", ".join(pairs) def get_title(self): """ Return a product's title or it's parent's title if it has no title """ title = self.title if not title and self.parent_id: title = self.parent.title return title get_title.short_description = pgettext_lazy("Product title", "Title") def get_product_class(self): """ Return a product's item class. Child products inherit their parent's. """ if self.is_child: return self.parent.product_class else: return self.product_class get_product_class.short_description = _("Product class") def get_is_discountable(self): """ At the moment, :py:attr:`.is_discountable` can't be set individually for child products; they inherit it from their parent. """ if self.is_child: return self.parent.is_discountable else: return self.is_discountable def get_categories(self): """ Return a product's categories or parent's if there is a parent product. """ if self.is_child: return self.parent.categories else: return self.categories get_categories.short_description = _("Categories") # Images def get_missing_image(self): """ Returns a missing image object. """ # This class should have a 'name' property so it mimics the Django file # field. return MissingProductImage() def get_all_images(self): if self.is_child and not self.images.exists( ) and self.parent_id is not None: return self.parent.images.all() return self.images.all() def primary_image(self): """ Returns the primary image for a product. Usually used when one can only display one product image, e.g. in a list of products. """ images = self.get_all_images() ordering = self.images.model.Meta.ordering if not ordering or ordering[0] != 'display_order': # Only apply order_by() if a custom model doesn't use default # ordering. Applying order_by() busts the prefetch cache of # the ProductManager images = images.order_by('display_order') try: return images[0] except IndexError: # We return a dict with fields that mirror the key properties of # the ProductImage class so this missing image can be used # interchangeably in templates. Strategy pattern ftw! missing_image = self.get_missing_image() return { 'original': missing_image.name, 'caption': '', 'is_missing': True } # Updating methods def update_rating(self): """ Recalculate rating field """ self.rating = self.calculate_rating() self.save() update_rating.alters_data = True def calculate_rating(self): """ Calculate rating value """ result = self.reviews.filter( status=self.reviews.model.APPROVED).aggregate(sum=Sum('score'), count=Count('id')) reviews_sum = result['sum'] or 0 reviews_count = result['count'] or 0 rating = None if reviews_count > 0: rating = float(reviews_sum) / reviews_count return rating def has_review_by(self, user): if user.is_anonymous: return False return self.reviews.filter(user=user).exists() def is_review_permitted(self, user): """ Determines whether a user may add a review on this product. Default implementation respects OSCAR_ALLOW_ANON_REVIEWS and only allows leaving one review per user and product. Override this if you want to alter the default behaviour; e.g. enforce that a user purchased the product to be allowed to leave a review. """ if user.is_authenticated or settings.OSCAR_ALLOW_ANON_REVIEWS: return not self.has_review_by(user) else: return False @cached_property def num_approved_reviews(self): return self.reviews.approved().count() @property def sorted_recommended_products(self): """Keeping order by recommendation ranking.""" return [ r.recommendation for r in self.primary_recommendations.select_related( 'recommendation').all() ]
class AbstractProduct(models.Model): """ ベースとなる製品モデルオブジェクト。 製品には3種類ある。これらは構造フィールドによって区別される。 - stand alone 製品 : 単独で存在するレギュラー製品。 - child 製品(子製品) :すべての子製品には必ず親製品が存在する。 それらは親の特定のバージョンになる。 - parent 製品(親製品) :基本的に、製品のセットを表す。 【例】ヨガコース ・コースそのもの:親製品 ・コースの時間/場所:子製品 """ STANDALONE, PARENT, CHILD = 'standalone', 'parent', 'child' STRUCTURE_CHOICES = ((STANDALONE, _('Stand-alone product')), (PARENT, _('Parent product')), (CHILD, _('Child product'))) # カラム定義 structure = models.CharField(_("製品構成"), max_length=10, choices=STRUCTURE_CHOICES, default=STANDALONE) is_public = models.BooleanField( _('Is public'), default=True, help_text=_( "Show this product in search results and catalogue listings.")) upc = NullCharField( _("UPC"), max_length=64, blank=True, null=True, unique=True, help_text=_("Universal Product Code (UPC) is an identifier for " "a product which is not specific to a particular " " supplier. Eg an ISBN for a book.")) parent = models.ForeignKey( 'self', blank=True, null=True, on_delete=models.CASCADE, related_name='children', verbose_name=_("親製品"), help_text=_("Only choose a parent product if you're creating a child " "product. For example if this is a size " "4 of a particular t-shirt. Leave blank if this is a " "stand-alone product (i.e. there is only one version of" " this product).")) # # タイトルは標準製品には必須になるが、子製品に対してはオプションとなる title = models.CharField(pgettext_lazy(u'製品タイトル', u'タイトル'), max_length=255, blank=True) slug = models.SlugField(_('スラッグ'), max_length=255, unique=False) description = models.TextField(_('詳細説明'), blank=True) #: "Kind" of product, e.g. T-Shirt, Book, etc. #: None for child products, they inherit their parent's product class product_class = models.ForeignKey( 'catalogue.ProductClass', null=True, blank=True, on_delete=models.PROTECT, verbose_name=_('製品タイプ'), related_name="products", help_text=_("Choose what type of product this is")) attributes = models.ManyToManyField( 'catalogue.ProductAttribute', through='ProductAttributeValue', verbose_name=_("Attributes"), help_text=_("A product attribute is something that this product may " "have, such as a size, as specified by its class")) #: It's possible to have options product class-wide, and per product. product_options = models.ManyToManyField( 'catalogue.Option', blank=True, verbose_name=_("製品オプション"), help_text=_("Options are values that can be associated with a item " "when it is added to a customer's basket. This could be " "something like a personalised message to be printed on " "a T-shirt.")) recommended_products = models.ManyToManyField( 'catalogue.Product', through='ProductRecommendation', blank=True, verbose_name=_("オススメ商品"), help_text=_("These are products that are recommended to accompany the " "main product.")) # 非正規化された製品評価-レビューアプリで使用される # 評価「なし」の場合、製品には評価項目がなくなる rating = models.FloatField( _('Rating'), null=True, editable=False) # editable=False:adminページに表示させない date_created = models.DateTimeField(_("Date created"), auto_now_add=True, db_index=True) # This field is used by Haystack to reindex search date_updated = models.DateTimeField(_("Date updated"), auto_now=True, db_index=True) categories = models.ManyToManyField('catalogue.Category', through='ProductCategory', verbose_name=_("Categories")) #: Determines if a product may be used in an offer. It is illegal to #: discount some types of product (e.g. ebooks) and this field helps #: merchants from avoiding discounting such products #: Note that this flag is ignored for child products; they inherit from #: the parent product. is_discountable = models.BooleanField( _("割引可能か?"), default=True, help_text=_("このフラグは、この製品をオファーで使用できるかどうかを示す。")) objects = ProductQuerySet.as_manager() # browsable property is deprecated and will be removed in Oscar 2.1 # Use Product.objects.browsable() instead. browsable = BrowsableProductManager() class Meta: abstract = True app_label = 'catalogue' ordering = ['-date_created'] verbose_name = _('Product') verbose_name_plural = _('Products') def __init__(self, *args, **kwargs): """初期化メソッド""" super().__init__(*args, **kwargs) self.attr = ProductAttributesContainer(product=self) def __str__(self): """adminページへの表示仕様を決定するメソッド""" if self.title: return self.title if self.attribute_summary: return "%s (%s)" % (self.get_title(), self.attribute_summary) else: return self.get_title() def get_absolute_url(self): """製品の絶対URLを返却するメソッド""" return reverse('catalogue:detail', kwargs={ 'product_slug': self.slug, 'pk': self.id }) def clean(self): """ 以下ルールにしたがって製品を判定する +---------------+-------------+--------------+--------------+ | | stand alone | parent | child | +---------------+-------------+--------------+--------------+ | title | required | required | optional | +---------------+-------------+--------------+--------------+ | product class | required | required | must be None | +---------------+-------------+--------------+--------------+ | parent | forbidden | forbidden | required | +---------------+-------------+--------------+--------------+ | stockrecords | 0 or more | forbidden | 0 or more | +---------------+-------------+--------------+--------------+ | categories | 1 or more | 1 or more | forbidden | +---------------+-------------+--------------+--------------+ | attributes | optional | optional | optional | +---------------+-------------+--------------+--------------+ | rec. products | optional | optional | unsupported | +---------------+-------------+--------------+--------------+ | options | optional | optional | forbidden | +---------------+-------------+--------------+--------------+ 検証ロジックは非常に複雑なため、検証は製品の構造に適したサブメソッドに委任される。 """ getattr(self, '_clean_%s' % self.structure)() if not self.is_parent: self.attr.validate_attributes() def _clean_standalone(self): """スタンドアロン商品をバリデート(判定)する""" if not self.title: raise ValidationError(_("Your product must have a title.")) if not self.product_class: raise ValidationError(_("Your product must have a product class.")) if self.parent_id: raise ValidationError(_("Only child products can have a parent.")) def _clean_child(self): """子製品をバリデート(判定)する""" if not self.parent_id: raise ValidationError(_("A child product needs a parent.")) if self.parent_id and not self.parent.is_parent: raise ValidationError( _("You can only assign child products to parent products.")) if self.product_class: raise ValidationError( _("A child product can't have a product class.")) if self.pk and self.categories.exists(): raise ValidationError( _("A child product can't have a category assigned.")) # Note that we only forbid options on product level if self.pk and self.product_options.exists(): raise ValidationError(_("A child product can't have options.")) def _clean_parent(self): """親製品をバリデート(判定)する""" self._clean_standalone() if self.has_stockrecords: raise ValidationError( _("A parent product can't have stockrecords.")) def save(self, *args, **kwargs): """編集を保存するメソッド。adminページにて「保存する」ボタンを押下したときに呼び出される""" if not self.slug: self.slug = slugify(self.get_title()) super().save(*args, **kwargs) self.attr.save() # Properties @property def is_standalone(self): """「structure」カラムに'standalone'を設定するメソッド""" return self.structure == self.STANDALONE @property def is_parent(self): """「structure」カラムに'parent'を設定するメソッド""" return self.structure == self.PARENT @property def is_child(self): """「structure」カラムに'child'を設定するメソッド""" return self.structure == self.CHILD def can_be_parent(self, give_reason=False): """製品を親製品に変えることができるかどうかを判断するのに役立つメソッド""" reason = None if self.is_child: reason = _('The specified parent product is a child product.') if self.has_stockrecords: reason = _("One can't add a child product to a product with stock" " records.") is_valid = reason is None if give_reason: return is_valid, reason else: return is_valid @property def options(self): """ 該当製品に対して全ての有効なオプションのセットを返却するメソッド オプション製品はクラス全体で、製品ごとに持つことができる。 """ pclass_options = self.get_product_class().options.all() return pclass_options | self.product_options.all() @cached_property def has_options(self): # 製品リストクエリセットから多数の製品クラスオプションを使用して注釈付きの値を抽出する。 num_product_class_options = getattr(self, 'num_product_class_options', None) num_product_options = getattr(self, 'num_product_options', None) if num_product_class_options is not None and num_product_options is not None: return num_product_class_options > 0 or num_product_options > 0 return self.get_product_class().options.exists( ) or self.product_options.exists() @property def is_shipping_required(self): return self.get_product_class().requires_shipping @property def has_stockrecords(self): """この製品に在庫記録があるかどうかテストするメソッド""" return self.stockrecords.exists() @property def num_stockrecords(self): """該当製品の在庫数を返却するメソッド""" return self.stockrecords.count() @property def attribute_summary(self): """ 製品のすべての属性の文字列を返却するメソッド """ attributes = self.attribute_values.all() pairs = [attribute.summary() for attribute in attributes] return ", ".join(pairs) def get_title(self): """ 製品のタイトルを取得し返却する。タイトルがない場合は親のタイトルを返す """ title = self.title if not title and self.parent_id: title = self.parent.title return title get_title.short_description = pgettext_lazy("Product title", "Title") def get_product_class(self): """ 該当製品の「製品タイプ」カラム部分の値を返却するメソッド ※子製品は親製品を継承する """ if self.is_child: return self.parent.product_class else: return self.product_class get_product_class.short_description = _("Product class") def get_is_discountable(self): """ 現時点では、子製品に対してis_discountableを個別に設定することはできず、親から継承する。 """ if self.is_child: return self.parent.is_discountable else: return self.is_discountable def get_categories(self): """ 親製品がある場合、製品のカテゴリまたは親のカテゴリを返すメソッド """ if self.is_child: return self.parent.categories else: return self.categories get_categories.short_description = _("Categories") # Images def get_missing_image(self): """ 不足している画像オブジェクトを返すメソッド。 """ # This class should have a 'name' property so it mimics the Django file # field. return MissingProductImage() def get_all_images(self): if self.is_child and not self.images.exists(): return self.parent.images.all() return self.images.all() def primary_image(self): """ Returns the primary image for a product. Usually used when one can only display one product image, e.g. in a list of products. """ images = self.get_all_images() ordering = self.images.model.Meta.ordering if not ordering or ordering[0] != 'display_order': # Only apply order_by() if a custom model doesn't use default # ordering. Applying order_by() busts the prefetch cache of # the ProductManager images = images.order_by('display_order') try: return images[0] except IndexError: # We return a dict with fields that mirror the key properties of # the ProductImage class so this missing image can be used # interchangeably in templates. Strategy pattern ftw! missing_image = self.get_missing_image() return { 'original': missing_image.name, 'caption': '', 'is_missing': True } # Updating methods def update_rating(self): """ ratingフィールドを再計算し、更新するメソッド """ self.rating = self.calculate_rating() self.save() update_rating.alters_data = True def calculate_rating(self): """割引額を計算するメソッド""" result = self.reviews.filter( status=self.reviews.model.APPROVED).aggregate(sum=Sum('score'), count=Count('id')) reviews_sum = result['sum'] or 0 reviews_count = result['count'] or 0 rating = None if reviews_count > 0: rating = float(reviews_sum) / reviews_count return rating def has_review_by(self, user): """ ・レビュー内容を返却する? ・ゲストユーザの場合、Falseを返す """ if user.is_anonymous: return False return self.reviews.filter(user=user).exists() def is_review_permitted(self, user): """ ユーザーがこの製品にレビューを追加できるかどうかを決定するメソッド。 ■デフォルトでの実装 「OSCAR_ALLOW_ANON_REVIEWS」を尊重 →ユーザーと製品ごとに1つのレビューのみ投稿可能 ※デフォルトの動作を変更する場合は、これをオーバーライドする必要がある 例えば レビューを残すことを許可するために、ユーザーが製品を購入したことを強制する """ if user.is_authenticated or settings.OSCAR_ALLOW_ANON_REVIEWS: return not self.has_review_by(user) else: return False @cached_property def num_approved_reviews(self): """承認されたレビューの数を返却するメソッド?""" return self.reviews.approved().count() @property def sorted_recommended_products(self): """推奨ランキングによる順序を維持して返却するメソッド""" return [ r.recommendation for r in self.primary_recommendations.select_related( 'recommendation').all() ]
class AbstractProduct(models.Model): """ The base product object If an item has no parent, then it is the "canonical" or abstract version of a product which essentially represents a set of products. If a product has a parent then it is a specific version of a catalogue. For example, a canonical product would have a title like "Green fleece" while its children would be "Green fleece - size L". """ upc = NullCharField( _("UPC"), max_length=64, blank=True, null=True, unique=True, help_text=_("Universal Product Code (UPC) is an identifier for " "a product which is not specific to a particular " " supplier. Eg an ISBN for a book.")) # No canonical product should have a stock record as they cannot be bought. parent = models.ForeignKey( 'self', null=True, blank=True, related_name='variants', verbose_name=_("Parent"), help_text=_("Only choose a parent product if this is a 'variant' of " "a canonical catalogue. For example if this is a size " "4 of a particular t-shirt. Leave blank if this is a " "CANONICAL PRODUCT (ie there is only one version of this " "product).")) # Title is mandatory for canonical products but optional for child products title = models.CharField(_('Product title'), max_length=255, blank=True) slug = models.SlugField(_('Slug'), max_length=255, unique=False) description = models.TextField(_('Description'), blank=True) #: "Type" of product. #: None for Product variants, they inherit their parent's product class product_class = models.ForeignKey( 'catalogue.ProductClass', null=True, on_delete=models.PROTECT, verbose_name=_('Product Type'), related_name="products", help_text=_("Choose what type of product this is")) attributes = models.ManyToManyField( 'catalogue.ProductAttribute', through='ProductAttributeValue', verbose_name=_("Attributes"), help_text=_("A product attribute is something that this product MUST " "have, such as a size, as specified by its class")) product_options = models.ManyToManyField( 'catalogue.Option', blank=True, verbose_name=_("Product Options"), help_text=_("Options are values that can be associated with a item " "when it is added to a customer's basket. This could be " "something like a personalised message to be printed on " "a T-shirt.")) recommended_products = models.ManyToManyField( 'catalogue.Product', through='ProductRecommendation', blank=True, verbose_name=_("Recommended Products"), help_text=_("These are products that are recommended to accompany the " "main product.")) # Denormalised product rating - used by reviews app. # Product has no ratings if rating is None rating = models.FloatField(_('Rating'), null=True, editable=False) date_created = models.DateTimeField(_("Date Created"), auto_now_add=True) # This field is used by Haystack to reindex search date_updated = models.DateTimeField(_("Date Updated"), auto_now=True, db_index=True) categories = models.ManyToManyField('catalogue.Category', through='ProductCategory', verbose_name=_("Categories")) #: Determines if a product may be used in an offer. It is illegal to #: discount some types of product (e.g. ebooks) and this field helps #: merchants from avoiding discounting such products is_discountable = models.BooleanField( _("Is discountable?"), default=True, help_text=_( "This flag indicates if this product can be used in an offer " "or not")) objects = ProductManager() browsable = BrowsableProductManager() class Meta: abstract = True ordering = ['-date_created'] verbose_name = _('Product') verbose_name_plural = _('Products') def __init__(self, *args, **kwargs): super(AbstractProduct, self).__init__(*args, **kwargs) self.attr = ProductAttributesContainer(product=self) def __unicode__(self): if self.is_variant: return u"%s (%s)" % (self.get_title(), self.attribute_summary) return self.get_title() def get_absolute_url(self): """ Return a product's absolute url """ return reverse('catalogue:detail', kwargs={ 'product_slug': self.slug, 'pk': self.id }) def clean(self): if self.is_top_level and not self.title: raise ValidationError(_("Canonical products must have a title")) if self.is_top_level and not self.product_class: raise ValidationError( _("Canonical products must have a product class")) if not self.is_group: self.attr.validate_attributes() def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.get_title()) super(AbstractProduct, self).save(*args, **kwargs) self.attr.save() # Properties @property def options(self): pclass = self.get_product_class() if pclass: return list( chain(self.product_options.all(), self.get_product_class().options.all())) return self.product_options.all() @property def is_top_level(self): """ Test if this product is a parent (who may or may not have children) """ return self.parent_id is None @cached_property def is_group(self): """ Test if this is a top level product and has more than 0 variants """ return self.is_top_level and self.variants.exists() @property def is_variant(self): """Return True if a product is not a top level product""" return not self.is_top_level @property def is_shipping_required(self): return self.get_product_class().requires_shipping @property def has_stockrecords(self): """ Test if this product has any stockrecords """ return self.num_stockrecords > 0 @property def num_stockrecords(self): return self.stockrecords.all().count() @property def attribute_summary(self): """ Return a string of all of a product's attributes """ pairs = [] for value in self.attribute_values.select_related().all(): pairs.append(value.summary()) return ", ".join(pairs) @property def min_variant_price_incl_tax(self): """ Return minimum variant price including tax """ return self._min_variant_price('price_incl_tax') @property def min_variant_price_excl_tax(self): """ Return minimum variant price excluding tax """ return self._min_variant_price('price_excl_tax') def _min_variant_price(self, property): """ Return minimum variant price """ prices = [] for variant in self.variants.all(): if variant.has_stockrecords: prices.append(getattr(variant.stockrecord, property)) if not prices: return None prices.sort() return prices[0] # Wrappers def get_title(self): """ Return a product's title or it's parent's title if it has no title """ title = self.title if not title and self.parent_id: title = self.parent.title return title get_title.short_description = pgettext_lazy(u"Product title", u"Title") def get_product_class(self): """ Return a product's item class """ if self.product_class_id or self.product_class: return self.product_class if self.parent and self.parent.product_class: return self.parent.product_class return None get_product_class.short_description = _("Product class") # Images def get_missing_image(self): """ Returns a missing image object. """ # This class should have a 'name' property so it mimics the Django file # field. return MissingProductImage() def primary_image(self): """ Returns the primary image for a product. Usually used when one can only display one product image, e.g. in a list of products. """ images = self.images.all() ordering = self.images.model.Meta.ordering if not ordering or ordering[0] != 'display_order': # Only apply order_by() if a custom model doesn't use default # ordering. Applying order_by() busts the prefetch cache of # the ProductManager images = images.order_by('display_order') try: return images[0] except IndexError: # We return a dict with fields that mirror the key properties of # the ProductImage class so this missing image can be used # interchangeably in templates. Strategy pattern ftw! return { 'original': self.get_missing_image(), 'caption': '', 'is_missing': True } # Updating methods def update_rating(self): """ Recalculate rating field """ self.rating = self.calculate_rating() self.save() update_rating.alters_data = True def calculate_rating(self): """ Calculate rating value """ result = self.reviews.filter( status=self.reviews.model.APPROVED).aggregate(sum=Sum('score'), count=Count('id')) reviews_sum = result['sum'] or 0 reviews_count = result['count'] or 0 rating = None if reviews_count > 0: rating = float(reviews_sum) / reviews_count return rating def has_review_by(self, user): if user.is_anonymous(): return False return self.reviews.filter(user=user).exists() def is_review_permitted(self, user): """ Determines whether a user may add a review on this product. Default implementation respects OSCAR_ALLOW_ANON_REVIEWS and only allows leaving one review per user and product. Override this if you want to alter the default behaviour; e.g. enforce that a user purchased the product to be allowed to leave a review. """ if user.is_authenticated() or settings.OSCAR_ALLOW_ANON_REVIEWS: return not self.has_review_by(user) else: return False @cached_property def num_approved_reviews(self): return self.reviews.filter(status=self.reviews.model.APPROVED).count()
class TransferMetadata(AccountNumberMixin, models.Model): """ Store WFRS specific metadata about a transfer """ user = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_("Requesting User"), related_name="wfrs_transfers", null=True, blank=True, on_delete=models.CASCADE, ) merchant_name = NullCharField(_("Merchant Name"), max_length=200) merchant_num = NullCharField(_("Merchant Number"), max_length=200) merchant_reference = models.CharField(max_length=128, null=True) amount = models.DecimalField(decimal_places=2, max_digits=12) type_code = models.CharField(_("Transaction Type"), choices=TRANS_TYPES, max_length=2) ticket_number = models.CharField(_("Ticket Number"), null=True, blank=True, max_length=12) financing_plan = models.ForeignKey( "wellsfargo.FinancingPlan", verbose_name=_("Plan Number"), null=True, blank=False, on_delete=models.SET_NULL, ) auth_number = models.CharField(_("Authorization Number"), null=True, blank=True, max_length=6, default="000000") status = models.CharField(_("Status"), choices=TRANS_STATUSES, max_length=2) message = models.TextField(_("Message")) disclosure = models.TextField(_("Disclosure")) created_datetime = models.DateTimeField(_("Created"), auto_now_add=True) modified_datetime = models.DateTimeField(_("Modified"), auto_now=True) @classmethod def get_by_oscar_transaction(cls, transaction, type_code=TRANS_TYPE_AUTH): return (cls.objects.filter( merchant_reference=transaction.reference).filter( type_code=type_code).order_by("-created_datetime").first()) @property def type_name(self): return dict(TRANS_TYPES).get(self.type_code) @property def status_name(self): return dict(TRANS_STATUSES).get(self.status) @property def financing_plan_number(self): return self.financing_plan.plan_number if self.financing_plan else None @cached_property def order(self): return self.get_order() def get_oscar_transaction(self): Transaction = get_model("payment", "Transaction") try: return Transaction.objects.get(reference=self.merchant_reference) except Transaction.DoesNotExist: return None def get_order(self): transaction = self.get_oscar_transaction() if not transaction: return None return transaction.source.order
def test_from_db_value_converts_null_to_string(self): field = NullCharField() self.assertEqual('', field.from_db_value(None, expression=None, connection=None, context=None))
def test_get_prep_value_converts_empty_string_to_null(self): field = NullCharField() self.assertEqual(None, field.get_prep_value(''))
class CreditApplication(MaybeAccountNumberMixin, models.Model): transaction_code = models.CharField( _("Transaction Code"), max_length=_max_len(CREDIT_APP_TRANS_CODES), choices=CREDIT_APP_TRANS_CODES, default=CREDIT_APP_TRANS_CODE_CREDIT_APPLICATION, help_text=_("Indicates where the transaction takes place."), ) reservation_number = NullCharField( _("Reservation Number"), max_length=20, help_text=_("The unique code that correlates with the user’s reservation."), ) application_id = NullCharField( _("Prequalified Application ID"), max_length=8, help_text=_("An 8-character alphanumeric ID identifying the application."), ) requested_credit_limit = models.IntegerField( _("Requested Credit Limit"), null=True, blank=True, validators=[MinValueValidator(0), MaxValueValidator(99999)], help_text=_( "This denotes the total price value of the items that the applicant’s shopping cart." ), ) language_preference = models.CharField( _("Language Preference"), max_length=_max_len(LANGUAGES), choices=LANGUAGES, default=ENGLISH, help_text=_("The main applicant’s language preference values"), ) salesperson = NullCharField( _("Sales Person ID"), max_length=10, help_text=_("Alphanumeric value associated with the salesperson."), ) # Main applicant data main_applicant = models.ForeignKey( "wellsfargo.CreditApplicationApplicant", verbose_name=_("Main Applicant"), related_name="+", on_delete=models.CASCADE, help_text=_("The main applicant’s personal details."), ) joint_applicant = models.ForeignKey( "wellsfargo.CreditApplicationApplicant", verbose_name=_("Joint Applicant"), null=True, blank=True, related_name="+", on_delete=models.CASCADE, help_text=_("The joint applicant’s details."), ) # Submit Status status = models.CharField( _("Application Status"), max_length=_max_len(CREDIT_APP_STATUSES), choices=CREDIT_APP_STATUSES, default="", help_text=_("Application Status"), ) # Internal Metadata merchant_name = NullCharField(_("Merchant Name"), max_length=200) merchant_num = NullCharField(_("Merchant Number"), max_length=200) application_source = models.CharField( _("Application Source"), default=_("Website"), max_length=25, help_text=_( "Where/how is user applying? E.g. Website, Call Center, In-Store, etc." ), ) user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, verbose_name=_("Owner"), help_text=_( "Select the user user who is applying and who will own (be the primary user of) this account." ), related_name="+", on_delete=models.SET_NULL, ) ip_address = models.GenericIPAddressField( _("Submitting User's IP Address"), null=True, blank=True, help_text=_("Submitting User's IP Address"), ) submitting_user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, verbose_name=_("Submitting User"), help_text=_( "Select the user who filled out and submitted the credit application (not always the same as the user who is applying for credit)." ), related_name="+", on_delete=models.SET_NULL, ) created_datetime = models.DateTimeField(_("Created Date/Time"), auto_now_add=True) modified_datetime = models.DateTimeField(_("Modified Date/Time"), auto_now=True) class Meta: verbose_name = _("Wells Fargo Credit Application") verbose_name_plural = _("Wells Fargo Credit Applications") @property def is_joint(self): return False @property def full_name(self): return "%s %s" % (self.main_applicant.first_name, self.main_applicant.last_name) def get_inquiries(self): return self.inquiries.order_by("-created_datetime").all() def get_credit_limit(self): inquiry = self.get_inquiries().first() if not inquiry: return None return inquiry.credit_limit def get_orders(self): """ Find orders that were probably placed using the account that resulted from this application. It's not foolproof since we don't store the full account number. """ if not hasattr(self, "_orders_cache"): Order = get_model("order", "Order") # all transfers made with last 4 digits reference_uuids = set( TransferMetadata.objects.filter( last4_account_number=self.last4_account_number ) .values_list("merchant_reference", flat=True) .distinct() .all() ) # all orders made by app.email that contain ref above UUIDs emails = [self.main_applicant.email_address] if self.joint_applicant: emails.append(self.joint_applicant.email_address) orders = ( Order.objects.filter( Q(guest_email__in=emails) | Q(user__email__in=emails) ) .filter(sources__transactions__reference__in=reference_uuids) .filter(date_placed__gte=self.created_datetime) .order_by("date_placed") .all() ) self._orders_cache = orders return self._orders_cache def get_first_order(self): if not hasattr(self, "_first_order_cache"): self._first_order_cache = self.get_orders().first() return self._first_order_cache def get_first_order_merchant_name(self): Transaction = get_model("payment", "Transaction") order = self.get_first_order() if not order: return None transfers = [] for source in order.sources.filter(source_type__name="Wells Fargo").all(): for transaction in source.transactions.filter( txn_type=Transaction.AUTHORISE ).all(): transfer = TransferMetadata.get_by_oscar_transaction(transaction) if transfer: transfers.append(transfer) if len(transfers) <= 0: return None return transfers[0].merchant_name
class PreQualificationRequest(models.Model): uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) entry_point = models.CharField( _("Entry Point"), max_length=_max_len(PREQUAL_ENTRY_POINT_CHOICES), choices=PREQUAL_ENTRY_POINT_CHOICES, default=PREQUAL_ENTRY_POINT_WEB, ) customer_initiated = models.BooleanField( _("Check was deliberately initiated by customer action"), default=False) email = models.EmailField(_("Email"), null=True, blank=True) first_name = models.CharField(_("First Name"), max_length=15) middle_initial = models.CharField(_("Middle Initial"), null=True, blank=True, max_length=1) last_name = models.CharField(_("Last Name"), max_length=20) line1 = models.CharField(_("Address Line 1"), max_length=26) line2 = models.CharField(_("Address Line 2"), max_length=26, null=True, blank=True) city = models.CharField(_("City"), max_length=15) state = USStateField(_("State")) postcode = USZipCodeField(_("Postcode")) phone = PhoneNumberField(_("Phone")) ip_address = models.GenericIPAddressField(null=True, blank=True) merchant_name = NullCharField(_("Merchant Name"), max_length=200) merchant_num = NullCharField(_("Merchant Number"), max_length=200) created_datetime = models.DateTimeField(auto_now_add=True) modified_datetime = models.DateTimeField(auto_now=True) class Meta: verbose_name = _("Pre-Qualification Request") verbose_name_plural = _("Pre-Qualification Requests") ordering = ("-created_datetime", "-id") indexes = [ models.Index(fields=["-created_datetime", "-id"]), ] @property def entry_point_name(self): return dict(PREQUAL_ENTRY_POINT_CHOICES).get(self.entry_point, self.entry_point) @property def status(self): response = getattr(self, "response", None) if response: return response.status return PREQUAL_TRANS_STATUS_REJECTED @property def status_name(self): response = getattr(self, "response", None) if response: return response.status_name return get_prequal_trans_status_name(PREQUAL_TRANS_STATUS_REJECTED, self.customer_initiated) @property def credit_limit(self): resp = getattr(self, "response", None) if resp is None: return None return resp.credit_limit @property def customer_response(self): resp = getattr(self, "response", None) if resp is None: return None return resp.customer_response @property def sdk_application_result(self): resp = getattr(self, "response", None) if resp is None: return None app_result = getattr(resp, "sdk_application_result", None) if app_result is None: return None return app_result.application_status @cached_property def resulting_order(self): resp = getattr(self, "response", None) if resp and resp.customer_order: return resp.customer_order # Look for other orders which might have been placed by this customer Order = get_model("order", "Order") email_matches = Q(guest_email=self.email) | Q(user__email=self.email) date_matches = Q(date_placed__gt=self.created_datetime) order = (Order.objects.filter(email_matches & date_matches).order_by( "date_placed").first()) return order @property def order_total(self): return self.resulting_order.total_incl_tax if self.resulting_order else None @property def order_date_placed(self): return self.resulting_order.date_placed if self.resulting_order else None @cached_property def order_merchant_name(self): Transaction = get_model("payment", "Transaction") order = self.resulting_order if not order: return None transfers = [] for source in order.sources.filter( source_type__name="Wells Fargo").all(): for transaction in source.transactions.filter( txn_type=Transaction.AUTHORISE).all(): transfer = TransferMetadata.get_by_oscar_transaction( transaction) if transfer: transfers.append(transfer) if len(transfers) <= 0: return None return transfers[0].merchant_name @property def response_reported_datetime(self): resp = getattr(self, "response", None) if resp is None: return None return resp.reported_datetime def get_signed_id(self): return signing.Signer().sign(self.pk) def get_resume_offer_url(self, next_url="/"): url = reverse("wfrs-api-prequal-resume", args=[self.get_signed_id()]) qs = urllib.parse.urlencode({ "next": next_url, }) return "{}?{}".format(url, qs)