class Goal(models.Model): name = models.CharField(max_length=500) balance = MoneyField(max_digits=14, decimal_places=2, default=Money("0", "PHP"), validators=[MinMoneyValidator(0)]) target_amount = MoneyField(max_digits=14, decimal_places=2, default_currency='PHP', validators=[MinMoneyValidator(1)]) owner = models.ForeignKey(User, on_delete=models.CASCADE) target_date = models.DateField() created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) opened = models.DateTimeField(null=True) @property def saving_progress(self): progress = Decimal(self.balance / self.target_amount) progress = round(progress * 100, 2) return progress if progress < 100 else 100 @property def date_progress(self): date_interval = self.target_date - self.created.date() days_passed = datetime.now().date() - self.created.date() progress = Decimal(days_passed / date_interval) progress = round(progress * 100, 2) return progress if progress < 100 else 100
class Transaction(models.Model): balance = models.ForeignKey(Balance, on_delete=models.CASCADE) amount = MoneyField(max_digits=14, decimal_places=2, default_currency='USD', validators=[ MinMoneyValidator(0), MaxMoneyValidator(100000), MinMoneyValidator({'USD': 0}), MaxMoneyValidator({'USD': 100000}), ]) details = models.CharField(max_length=50) date_time = models.DateTimeField(auto_now=True)
class ValidatedMoneyModel(models.Model): money = MoneyField( max_digits=10, decimal_places=2, validators=[ MinMoneyValidator({"EUR": 100, "USD": 50}), MaxMoneyValidator({"EUR": 1000, "USD": 500}), MinMoneyValidator(Money(500, "NOK")), MaxMoneyValidator(Money(900, "NOK")), MinMoneyValidator(10), MaxMoneyValidator(1500), ], )
class Wallet(models.Model): TYPE_CHOICES = [ ('CH', 'Cash'), ('EM', 'E-Money'), ('CC', 'Credit Card'), ('DC', 'Debit Card'), ] name = models.CharField(max_length=500, null=False, blank=False) type = models.CharField(max_length=500, null=False, blank=False, choices=TYPE_CHOICES) owner = models.ForeignKey(User, on_delete=models.CASCADE) balance = MoneyField(max_digits=14, decimal_places=2, default=Money("0", "PHP"), validators=[MinMoneyValidator(0)]) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) opened = models.DateTimeField(null=True) class Meta: unique_together = ('name', 'type', 'owner') def __str__(self): return '{}({})'.format(self.name, self.pk)
def __init__(self, **kwargs): """Overwrite default values and validators.""" # detect if creating migration if 'migrate' in sys.argv or 'makemigrations' in sys.argv: # remove currency information for a clean migration kwargs['default_currency'] = '' kwargs['currency_choices'] = [] else: # set defaults kwargs.update(money_kwargs()) # Set a minimum value validator validators = kwargs.get('validators', []) allow_negative = kwargs.pop('allow_negative', False) # If no validators are provided, add some "standard" ones if len(validators) == 0: if not allow_negative: validators.append(MinMoneyValidator(0), ) kwargs['validators'] = validators super().__init__(**kwargs)
class Service(models.Model): Title = models.CharField(max_length=30) type = models.CharField(max_length=60, choices=Type_Choices, default='None', blank=True) description = models.TextField() rate_per_hour = MoneyField(max_digits=19, decimal_places=4, default_currency='KES', validators=[MinMoneyValidator(1)]) user = models.ForeignKey(User)
class Loan(models.Model): amount = MoneyField(max_digits=19, decimal_places=4, default_currency='CNY', validators=[ MinMoneyValidator(0), ]) branch = models.ForeignKey(Branch, related_name='loans', on_delete=models.CASCADE) customers = models.ManyToManyField(Customer, related_name='loans') @property def status(self): released = self.released if released == 0: return 'empty' elif released >= self.amount: return 'full' else: return 'half' @property def released(self): return sum(payment.amount for payment in self.payments.all()) @property def remained(self): released = self.released if released == 0: return self.amount else: return self.amount - released
class PositiveValidatedMoneyModel(models.Model): """Validated model with a field requiring a non-negative value.""" money = MoneyField(max_digits=10, decimal_places=2, validators=[ MinMoneyValidator(0), ])
class Price(CachedModel, CommonInfo, TimeStampedModel): """ Price class """ objects = PriceManager() price = MoneyField(max_digits=20, decimal_places=2, verbose_name=_('price'), validators=[MinMoneyValidator(0)], db_index=True) country = models.ForeignKey(Country, on_delete=models.PROTECT, verbose_name=_('country'), null=True, blank=True, db_index=True) is_enabled = models.BooleanField(default=True, db_index=True, verbose_name=_('is enabled')) service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name=_('service'), db_index=True, related_name='prices') period_from = models.PositiveIntegerField( db_index=True, null=True, blank=True, verbose_name=_('from'), ) period_to = models.PositiveIntegerField( db_index=True, null=True, blank=True, verbose_name=_('to'), ) for_unit = models.BooleanField(default=True, db_index=True, verbose_name=_('for unit')) def __str__(self): return '{}: {} {}-{} {}'.format( self.country if self.country else 'Base', self.price, self.period_from or '∞', self.period_to or '∞', 'per unit' if self.for_unit else 'per period', ) def clean(self): """ Price validation """ validate_price_periods(self) class Meta: ordering = ['country', 'period_from', 'period_to', 'price'] unique_together = ('service', 'country', 'period_from', 'period_to')
class ValidatedMoneyModel(models.Model): money = MoneyField(max_digits=10, decimal_places=2, validators=[ MinMoneyValidator({ 'EUR': 100, 'USD': 50 }), MaxMoneyValidator({ 'EUR': 1000, 'USD': 500 }), MinMoneyValidator(Money(500, 'NOK')), MaxMoneyValidator(Money(900, 'NOK')), MinMoneyValidator(10), MaxMoneyValidator(1500), ])
def __init__(self, *args, **kwargs): self.default_currency = kwargs.pop("default_currency", None) super(MoneyField, self).__init__(*args, **kwargs) # Rest Framework converts `min_value` / `max_value` to validators, that are not aware about `Money` class # We need to adjust them for idx, validator in enumerate(self.validators): if isinstance(validator, MinValueValidator): self.validators[idx] = MinMoneyValidator(self.min_value) elif isinstance(validator, MaxValueValidator): self.validators[idx] = MaxMoneyValidator(self.max_value)
class LoanPayment(models.Model): amount = MoneyField(max_digits=19, decimal_places=4, default_currency='CNY', validators=[ MinMoneyValidator(0), ]) date = models.DateField(default=datetime.date.today, blank=True) loan = models.ForeignKey(Loan, related_name='payments', on_delete=models.CASCADE)
class Advertisement(models.Model): """Custom model for advertisements Attributes: user (AppUser): a user to which an ad belongs title (str): a title of an ad description (str): description for an ad photo (image): 1 photo for an ad address (str): address where to meet created_date (datetime): when an ad was added modified_date (datetime): the last time an ad was modified price (money): how much an ad's product costs tags(TaggableManager): M2M with Tag model from taggit lib Methods: get_absolute_url: The method that returns a unique link to an every advert instance """ user = models.ForeignKey(AppUser, on_delete=models.CASCADE, null=True, verbose_name=_('User')) title = models.CharField(max_length=100, verbose_name=_('Title')) description = models.TextField(max_length=3000, verbose_name=_('Description')) address = models.CharField(max_length=100, null=True, verbose_name=_('Address')) created_date = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('Date of creation')) modified_date = models.DateTimeField( auto_now=True, verbose_name=_('Date of modification')) price = MoneyField(max_digits=14, decimal_places=2, default_currency='RUB', verbose_name=_('Price'), validators=[MinMoneyValidator(0)]) favourite = models.ManyToManyField(AppUser, related_name='user_favourites') published = models.BooleanField(default=True, verbose_name=_('Published')) tags = TaggableManager(blank=True) def __str__(self): return f'{self.title} ({self.user})' def get_absolute_url(self): return reverse('info', args=[self.id]) class Meta: verbose_name = _('Ad') verbose_name_plural = _('Ads')
class UserProfile(models.Model): user = models.OneToOneField(User, related_name='user', on_delete=models.PROTECT) photo = models.ImageField(upload_to='profile_image', blank=True) website = models.URLField(default='', blank=True) bio = models.TextField(default='', blank=True) phone = models.CharField(max_length=20, blank=True, default='') city = models.CharField(max_length=100, default='', blank=True) country = models.CharField(max_length=100, default='', blank=True) organization = models.CharField(max_length=100, default='', blank=True) Credit = MoneyField( max_digits=10, decimal_places=2, validators=[ MinMoneyValidator(0), MaxMoneyValidator(1500), MinMoneyValidator(Money(0, 'NOK')), MaxMoneyValidator(Money(900, 'NOK')), MinMoneyValidator({'EUR': 100, 'USD': 0}), MaxMoneyValidator({'EUR': 1000, 'USD': 500}), ]) def __str__(self): return str(self.user)
class PettycashReimbursementRequest(models.Model): dst = models.ForeignKey( User, help_text="Person to reemburse (usually you, yourself)", on_delete=models.CASCADE, related_name="isReimbursedTo", ) date = models.DateField(help_text="Date of expense", default=timezone.now) submitted = models.DateTimeField( help_text="Date the request was submitted", default=timezone.now, ) amount = MoneyField( max_digits=8, decimal_places=2, default_currency="EUR", validators=[ MinMoneyValidator(0), MaxMoneyValidator(settings.MAX_PAY_REIMBURSE.amount), ], help_text= "This system will only accept reimbursement up to %s. Above that; contact the trustees directly (%s)" % (settings.MAX_PAY_REIMBURSE.amount, settings.TRUSTEES), ) viaTheBank = models.BooleanField( default=False, help_text= "Check this box if you want to be paid via a IBAN/SEPA transfer; otherwise the amount will be credited to your Makerspace petty cash acount", ) description = models.CharField( max_length=300, help_text="Description / omschrijving van waarvoor deze betaling is", ) scan = StdImageField( upload_to=upload_to_pattern, delete_orphans=True, variations=settings.IMG_VARIATIONS, validators=settings.IMG_VALIDATORS, blank=True, null=True, help_text="Scan, photo or similar of the receipt", ) history = HistoricalRecords()
class Expense(models.Model): """Expense to be reimbursed.""" # TODO add attachments document_number = models.CharField('Document number', max_length=50, blank=True) requisition_number = models.IntegerField('Requisition number', blank=True, null=True) description = models.TextField("Description") value = MoneyField( "Amount", max_digits=11, decimal_places=2, default_currency="EUR", validators=[MinMoneyValidator(Money(0.01))], ) eur_value = models.DecimalField('Amount in EUR', max_digits=11, decimal_places=2) is_social = models.BooleanField('It is a social expense', default=False) receipt = models.FileField('Receipt', upload_to=user_directory_path) receipt_date = models.DateField('Receipt date', null=True, default=None) reimbursement = models.ForeignKey(to="Reimbursement", on_delete=models.CASCADE, related_name="expenses") expensecode = models.ForeignKey(to='finance.ExpenseCode', on_delete=models.CASCADE, related_name="reimbursement_expenses", verbose_name='Expense code') class Meta: ordering = ('document_number', ) verbose_name = 'Reimbursement expense' verbose_name_plural = 'Reimbursements expenses' def clean(self): if self.description is not None: # restrict number of lines nlines = self.description.count("\n") if nlines > 8 or len(self.description) > 600: raise ValidationError({"description": "Too much text."}) def short_description(self): return textwrap.shorten(self.description, width=100, placeholder="...")
class AbstractPrice(SafeDeleteModel): """ An abstract class that represents a price that was created at a given datetime. """ class Meta: abstract = True price = MoneyField( decimal_places=2, max_digits=10, validators=[MinMoneyValidator(0)] ) created_at = models.DateTimeField(auto_now_add=True)
class Dish(Model): name = CharField(max_length=64) restaurant = ForeignKey(Restaurant, on_delete=CASCADE) cost = MoneyField( max_digits=6, decimal_places=3, validators=[MinMoneyValidator(0)] ) calories = IntegerField( validators=[MinValueValidator(0)] ) grams = IntegerField( validators=[MinValueValidator(0)] ) def __str__(self): return self.name
def __init__(self, **kwargs): # detect if creating migration if 'migrate' in sys.argv or 'makemigrations' in sys.argv: # remove currency information for a clean migration kwargs['default_currency'] = '' kwargs['currency_choices'] = [] else: # set defaults kwargs.update(money_kwargs()) # Set a minimum value validator validators = kwargs.get('validators', []) if len(validators) == 0: validators.append(MinMoneyValidator(0), ) kwargs['validators'] = validators super().__init__(**kwargs)
class Transaction(models.Model): STATUS = (('SUCCESS', 'Success'), ('FAILED', 'Failed'), ('PENDING', 'Pending')) SERVICES = ( ('P2P', 'Pochi to Pochi'), ('DEPOSIT', 'Deposit'), ('WITHDRAW', 'Withdraw'), ('BONUS', 'Bonus'), ('FEES', 'Fees'), ) full_timestamp = models.DateTimeField(auto_now_add=True) profile_id = models.CharField(max_length=10, db_index=True) account = models.CharField(max_length=13, db_index=True) msisdn = models.CharField(max_length=10, db_index=True, default='NA') trans_id = models.CharField(max_length=25, default='NA') service = models.CharField(max_length=8, db_index=True, choices=SERVICES) channel = models.CharField(max_length=25, db_index=True, default='NA') mode = models.CharField(max_length=6, db_index=True, choices=MODES, default='POCHI') dst_account = models.CharField(max_length=25, db_index=True, default='NA') amount = MoneyField(max_digits=10, decimal_places=2, default_currency='TZS', validators=[ MinMoneyValidator({ 'TZS': 1000, 'USD': 50 }), MaxMoneyValidator({ 'TZS': 1000000000, 'USD': 1000000 }), ]) charge = MoneyField(max_digits=10, decimal_places=2, default_currency='TZS') reference = models.CharField(max_length=15, db_index=True, default='NA') status = models.CharField(max_length=7, choices=STATUS, default='PENDING') result_code = models.CharField(max_length=3, default='111', db_index=True) message = models.TextField(max_length=1024, default='NA') processed_timestamp = models.DateTimeField(null=True)
class RateClass(models.Model): name = models.CharField(verbose_name=_("name"), max_length=150, unique=True) description = models.CharField(verbose_name=_("description"), max_length=250, blank=True) rate = MoneyField( verbose_name=_("rate"), decimal_places=2, max_digits=6, default_currency="EUR", validators=[MinMoneyValidator(0)], ) class Meta: verbose_name = _("rate class") verbose_name_plural = _("rate classes") def __str__(self): return self.name
class Product(models.Model): """ Container model for a product which stores information common to all of its variations. """ name = models.CharField(max_length=255) slug = models.SlugField(blank=True, unique=True) description = models.TextField(blank=True, null=True) product_template = models.ForeignKey(ProductTemplate, related_name="products", on_delete=models.CASCADE) min_price = MoneyField( max_digits=19, decimal_places=4, default=0, default_currency=settings.DEFAULT_CURRENCY, currency_choices=settings.CURRENCY_CHOICES, currency_max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, validators=[ MinMoneyValidator(0), ]) active = models.BooleanField(default=False) created = models.DateTimeField(editable=False, default=timezone.now) modified = models.DateTimeField(default=timezone.now) objects = ProductManager() def save(self, *args, **kwargs): """ Update timestamps. """ if not self.id: self.created = timezone.now() self.modified = timezone.now() return super().save(*args, **kwargs) def __str__(self): return self.name
class CashOut(models.Model): STATUS = (('SUCCESS', 'Success'), ('FAILED', 'Failed'), ('PENDING', 'Pending')) ext_entity = models.CharField(max_length=100) ext_acc_no = models.CharField(max_length=30, default='NA') amount = MoneyField(max_digits=10, decimal_places=2, default_currency='TZS', validators=[ MinMoneyValidator({ 'TZS': 1000, 'USD': 50 }), MaxMoneyValidator({ 'TZS': 1000000000, 'USD': 1000000 }), ]) status = models.CharField(max_length=8, choices=STATUS, default='PENDING') result = models.CharField(max_length=3, default='111', db_index=True) message = models.TextField(max_length=1024, default='NA') reference = models.CharField(max_length=15, db_index=True, default='NA') ext_trans_id = models.CharField(max_length=25, default='NA')
class Ledger(models.Model): TYPES = (('DEBIT', 'Debit'), ('CREDIT', 'Credit')) full_timestamp = models.DateTimeField(auto_now_add=True) profile_id = models.CharField(max_length=10, db_index=True) account = models.CharField(max_length=13, db_index=True) trans_type = models.CharField(max_length=10, choices=TYPES) mode = models.CharField(max_length=6, db_index=True, choices=MODES, default='POCHI') amount = MoneyField(max_digits=10, decimal_places=2, default_currency='TZS', validators=[ MinMoneyValidator({ 'TZS': 1000, 'USD': 50 }), MaxMoneyValidator({ 'TZS': 1000000000, 'USD': 1000000 }), ]) trans_id = models.CharField(max_length=25, default='NA') reference = models.CharField(max_length=15, db_index=True, default='NA') available_o_bal = MoneyField(max_digits=10, decimal_places=2, default_currency='TZS') available_c_bal = MoneyField(max_digits=10, decimal_places=2, default_currency='TZS') current_o_bal = MoneyField(max_digits=10, decimal_places=2, default_currency='TZS') current_c_bal = MoneyField(max_digits=10, decimal_places=2, default_currency='TZS')
class Subscription(CommonInfo, TimeStampedModel): """ The class represents client subscriptions """ objects = SubscriptionManager() STATUSES = ( ('enabled', _('enabled')), ('canceled', _('canceled')), ) client = models.ForeignKey(Client, on_delete=models.PROTECT, db_index=True, verbose_name=_('client'), related_name='subscriptions') order = models.ForeignKey(Order, on_delete=models.SET_NULL, verbose_name=_('order'), null=True, blank=True, db_index=True, related_name='subscriptions') country = models.ForeignKey(Country, on_delete=models.PROTECT, verbose_name=_('country'), db_index=True, related_name='subscriptions') status = models.CharField(max_length=20, default='enabled', choices=STATUSES, verbose_name=_('status'), db_index=True) price = MoneyField(max_digits=20, decimal_places=2, blank=True, verbose_name=_('price'), validators=[MinMoneyValidator(0)], db_index=True) period = models.PositiveIntegerField( verbose_name=_('period'), db_index=True, help_text=_('the billing period in months')) merchant = models.CharField( max_length=255, null=True, blank=True, db_index=True, verbose_name=_('merchant'), ) customer = models.CharField( max_length=255, db_index=True, verbose_name=_('customer'), ) subscription = models.CharField( max_length=255, db_index=True, verbose_name=_('subscription'), ) def cancel(self): """ Cancel the subscription """ braintree = BraintreeGateway(self.country, 'sandbox') result = braintree.cancel_subscription(self.subscription) if (result): self.status = 'canceled' self.save() def save(self, *args, **kwargs): if self.status == 'enabled': for subscription in Subscription.objects.get_active( self.client, self.pk): subscription.cancel() super().save(*args, **kwargs) class Meta: ordering = ['-created']
class Good(TimeStampedModel): class Meta: verbose_name = pgettext_lazy('product', 'Good') verbose_name_plural = _('Goods') unique_together = ('name', 'category') AVAILABLE = 'available' NOT_AVAILABLE = 'not_available' ON_REQUEST = 'on_request' AVAILABILITY_CHOICES = ( (AVAILABLE, _('Available')), (NOT_AVAILABLE, _('Not available')), (ON_REQUEST, _('On request')), ) name = models.CharField(verbose_name=pgettext_lazy('not person', 'Name'), max_length=30, null=False, blank=False) description = models.TextField(verbose_name=_('Description'), blank=True, default='') category = models.ForeignKey(GoodsCategory, related_name='goods', on_delete=models.PROTECT, verbose_name=_('Category')) seller = models.ForeignKey(Store, related_name='goods', on_delete=models.CASCADE, verbose_name=_('Seller')) price = MoneyField(verbose_name=_('Price'), max_digits=14, decimal_places=2, default_currency='UAH', validators=[MinMoneyValidator(0)]) discount = models.PositiveIntegerField(default=0, validators=[MaxValueValidator(99)], verbose_name=_('Discount')) availability = models.CharField(max_length=20, choices=AVAILABILITY_CHOICES, default=AVAILABLE, verbose_name=_('Availability')) def __str__(self): return f'{self.name} ({self.category})' def categories(self): return self.category.categories_chain() def categories_names(self): return [category.name for category in self.categories()] categories_names.short_description = _('Categories names') def categories_names_chain(self): return ' > '.join([str(category) for category in self.categories()]) categories_names_chain.short_description = _('Categories names chain') @property def categories_ids(self): return [category.id for category in self.categories()] @property def specifications(self): try: return self._specifications except GoodSpecifications.DoesNotExist: return None @property def final_price(self): return round(self.price * (1 - self.discount / 100)) @property def main_image_url(self): image = self.images.filter( is_main=True).first() or self.images.order_by('id').first() return image.image_url if image else ''
class Product(models.Model): class Meta: verbose_name = "producto" verbose_name_plural = "productos" name = models.CharField(max_length=100, verbose_name="nombre") short_description = models.CharField(max_length=255, verbose_name="descripcion corta") description = tinymce_models.HTMLField(verbose_name="descripcion larga") price = MoneyField(max_digits=12, decimal_places=2, validators=[MinMoneyValidator(Decimal("0.01"))], verbose_name="precio") discount = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], verbose_name="descuento") weight = models.IntegerField(validators=[MinValueValidator(0)], verbose_name="peso") weight_unit = models.CharField(max_length=100, choices=WEIGHT_UNITS, verbose_name="unidad del peso") width = models.IntegerField(validators=[MinValueValidator(0)], verbose_name="ancho") width_unit = models.CharField(max_length=100, choices=LENGTH_UNITS, verbose_name="unidad del ancho") height = models.IntegerField(validators=[MinValueValidator(0)], verbose_name="alto") height_unit = models.CharField(max_length=100, choices=LENGTH_UNITS, verbose_name="unidad del alto") depth = models.IntegerField(validators=[MinValueValidator(0)], verbose_name="profundidad") depth_unit = models.CharField(max_length=100, choices=LENGTH_UNITS, verbose_name="unidad de la profundidad") objects = ProductQuerySet.as_manager() def __str__(self): return self.name def id_as_string(self): return str(self.id) def rating_mean(self): if not self.ratings.values(): return 0 else: values = [] for it in self.ratings.values(): values.append(it['value']) resultado = statistics.mean(values) return round(resultado, 2) def cover_image(self): image = self.images.first() return image.url() if image else '/static/no_image.jpg' def price_with_discount(self): # da el precio con descuento en la moneda del producto return self.price * (100 - self.discount) / 100 def sale_price(self): # da el precio con descuento en pesos argentinos if self.price.currency.code == 'ARS': return self.price_with_discount() # convertir dolares a pesos return Money(self.price_with_discount().amount * 150, "ARS") def shipping_price(self): ancho = convertir_a_metros(self.width, self.width_unit) alto = convertir_a_metros(self.height, self.height_unit) profundidad = convertir_a_metros(self.depth, self.depth_unit) peso = convertir_a_kilogramos(self.weight, self.weight_unit) multiplicador = max([1, ancho * alto * profundidad * peso]) valor = 50 return Money(multiplicador * valor, "ARS") def variants_with_values(self): from Products.models import ProductVariantValue return self.variants.filter(Exists(ProductVariantValue.objects.filter(variant__id=OuterRef('pk'))))
class PettycashTransaction(models.Model): dst = models.ForeignKey( User, help_text="Whom to pay the money to", on_delete=models.CASCADE, related_name="isReceivedBy", blank=True, null=True, ) src = models.ForeignKey( User, help_text="Whom paid you", on_delete=models.CASCADE, related_name="isSentBy", blank=True, null=True, ) date = models.DateTimeField(blank=True, null=True, help_text="Date of transaction") amount = MoneyField( max_digits=8, decimal_places=2, null=True, default_currency="EUR", validators=[MinMoneyValidator(0)], ) description = models.CharField( max_length=300, blank=True, null=True, help_text="Description / omschrijving van waarvoor deze betaling is", ) history = HistoricalRecords() def url(self): return settings.BASE + self.path() def path(self): return reverse("transactions", kwargs={"pk": self.id}) def __str__(self): if self.dst == self.src: return "@%s BALANCE %s" % (self.date, self.amount) return "@%s %s->%s '%s' %s" % ( self.date, self.src, self.dst, self.description, self.amount, ) def delete(self, *args, **kwargs): rc = super(PettycashTransaction, self).delete(*args, **kwargs) try: adjust_balance_cache(self, self.src, self.amount) adjust_balance_cache(self, self.dst, -self.amount) except Exception as e: logger.error("Transaction cache failure on delete: %s" % (e)) return rc def refund_booking(self): """ Refund a booking by doing a new 'reverse' booking, this way all amounts stay positive """ new_transaction = PettycashTransaction() new_transaction.src = self.dst new_transaction.dst = self.src new_transaction.amount = self.amount new_transaction.description = "refund %s (%d)" % (self.description, self.pk) new_transaction.save() def save(self, *args, **kwargs): bypass = False if kwargs is not None and "bypass" in kwargs: bypass = kwargs["bypass"] del kwargs["bypass"] if self.pk: if not bypass: raise ValidationError( "you may not edit an existing Transaction - instead create a new one" ) logger.info("Bypass used on save of %s" % self) if not self.date: self.date = timezone.now() if self.amount < Money(0, EUR): if not bypass: raise ValidationError("Blocked negative transaction.") logger.info("Bypass for negative transaction used on save of %s" % self) rc = super(PettycashTransaction, self).save(*args, **kwargs) try: adjust_balance_cache(self, self.src, -self.amount) adjust_balance_cache(self, self.dst, self.amount) except Exception as e: logger.error("Transaction cache failure: %s" % (e)) return rc
def clean(self): balance_validator = MinMoneyValidator(0 - self.credit_limit) try: balance_validator(self.balance) except ValidationError as e: raise ValidationError({'balance': e})
class Order(CommonInfo, TimeStampedModel): """ Order class """ STATUSES = ( ('new', _('new')), ('processing', _('processing')), ('paid', _('paid')), ('canceled', _('canceled')), ('corrupted', _('corrupted')), ) objects = OrderManager() tracker = FieldTracker() status = models.CharField(max_length=20, default='new', choices=STATUSES, verbose_name=_('status'), db_index=True) note = models.TextField(null=True, blank=True, db_index=True, verbose_name=_('note'), help_text=_('Clear to regenerate note')) price = MoneyField(max_digits=20, decimal_places=2, default=0, verbose_name=_('price'), validators=[MinMoneyValidator(0)], db_index=True, help_text=_('Set zero to recalculate price')) client = models.ForeignKey(Client, on_delete=models.CASCADE, db_index=True, verbose_name=_('client'), related_name='orders') expired_date = models.DateTimeField(db_index=True, blank=True, verbose_name=_('expired date')) paid_date = models.DateTimeField(db_index=True, null=True, blank=True, verbose_name=_('paid date')) payment_system = models.CharField(max_length=30, null=True, blank=True, choices=[ (s, _(s)) for s in settings.PAYMENT_SYSTEMS ], verbose_name=_('payment system'), db_index=True) client_services = models.ManyToManyField( 'clients.ClientService', blank=True, verbose_name=_('client services'), through=ClientService.orders.through) discount = models.ForeignKey(ClientDiscount, on_delete=models.SET_NULL, null=True, blank=True, db_index=True, related_name='orders') @property def get_room_service(self): client_service = self.client_services.filter( service__type='rooms', service__period_units__in=('month', 'year')).first() return getattr(client_service, 'service', None) @property def client_services_by_category(self): """ Grouped services """ return self.client_services.get_order_services_by_category(self) def get_payer(self, client_filter=None, local=True): """ Return payer """ company = self.client.get_bill_company() client = self.client if client_filter: if not all([getattr(client, f, None) for f in client_filter]): client = None lang = get_lang(self.client.country.tld) local_company = getattr(company, lang, None) local_client = getattr(client, lang, None) if local_company: return company if local_client: return client if not local: return client def calc_price(self): """ Calculate && return price """ if self.status == 'corrupted': return Money(0, EUR) price = self.client_services.total(self.client_services) return self.apply_discount(price) def apply_discount(self, price): """ Apply the client discount to the price """ discount = getattr(self.client, 'discount', None) now = arrow.utcnow().datetime skip = False if not discount or not price: return price if not discount.remaining_uses: skip = True if discount.start_date and discount.start_date > now: skip = True if discount.end_date and discount.end_date < now: skip = True if discount == self.discount: skip = False if skip: return price price = price * (100 - discount.percentage_discount) / 100 if discount != self.discount: discount.usage_count += 1 self.discount = discount discount.save() return price def set_corrupted(self): """ Set corrupted orders """ if len(set([s.price.currency for s in self.client_services.all()])) > 1: self.status = 'corrupted' self.price = self.calc_price() self.save() logger = logging.getLogger('billing') logger.error('Order corrupted #{}.'.format(self.pk)) def set_paid(self, payment_system): """ Set paid orders """ self.status = 'paid' self.payment_system = payment_system self.paid_date = arrow.utcnow().datetime self.full_clean() self.save() @property def price_str(self): return '{} {}'.format(self.price.amount, self.price.currency) def generate_note(self): """ Generate and return default order note """ if self.client_services.count(): return render_to_string('finances/order_note.md', {'order': self}) return None def clean(self, *args, **kwargs): if self.status == 'paid' and \ (not self.payment_system or not self.paid_date): raise ValidationError({ 'status': _('Can`t set "paid" status with empty payment system \ and paid date') }) super(Order, self).clean(*args, **kwargs) def __str__(self): return '#{} - {} - {} - {} - {}'.format( self.id, self.status, self.client, self.price, self.expired_date.strftime('%c')) class Meta: ordering = ( '-modified', '-created', ) permissions = (('list_manager', _('Can see assigned entries')), )