Django query include join - django

I tried to calculate the count of the increased and decreased competitor product price which are related to products. I couldn't handle it. Can I do this by overriding get_context_data method? I couldn't write a query to get the data from comp_product model. How can I handle this?
Models:
class Product(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE,related_name='products')
category = models.CharField(max_length=120)
brand = models.CharField(max_length=120)
product = models.CharField(max_length=120)
price = models.DecimalField(decimal_places=2,max_digits=100)
class Comp_Product(models.Model):
product = models.ForeignKey(Product,on_delete=models.CASCADE, related_name="comp_products")
competitor = models.URLField()
price = models.DecimalField(decimal_places=2,max_digits=100)
change = models.FloatField()
stock = models.BooleanField()
last_update = models.DateField(auto_now_add=True)
View:
class DashboardList(ListView):
template_name='dashboard_by_user.html'
def get_queryset(self):
p = Product.objects.filter(user=self.request.user)
return p
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
Products = context['object_list']
context['distinct_category_count'] = Products.values('category').distinct().count()
context['distinct_brand_count'] = Products.values('brand').distinct().count()
context['product_count'] = Products.values('product').count()
context['num_comp_products_with_lower_price'] = p.comp_products.filter(price__lt=p.price).count()
context['num_comp_products_with_higher_price'] = p.comp_products.filter(price__gt=p.price).count()
return context

If I assume you want to retrieve for a Product p the number of competitor products Comp_Product which have a lower or higher price then this will get you started:
# This is the product you're looking at, it'll be retrieved
# via the queryset and it's just randomly the first.
# If you want to do this for all products in the queryset
# you'll need to work with annotate.
p = self.queryset.first()
num_comp_products_with_lower_price = p.comp_products.filter(price__lt=p.price).count()
num_comp_products_with_higher_price = p.comp_products.filter(price__gt=p.price).count()
If you have p in your view it shouldn't be a problem to do this in get_context_data.

Related

Querying other Model in class-based view produces error

I have two tables, in one of which the possible items with their properties are recorded, in the other the stock levels of these respective items are recorded.
class itemtype(models.Model):
item_id = models.IntegerField(primary_key=True)
item_name = models.CharField(max_length=100)
group_name = models.CharField(max_length=100)
category_name = models.CharField(max_length=100)
mass = models.FloatField()
volume = models.FloatField()
packaged_volume = models.FloatField(null=True)
used_in_storage = models.BooleanField(default=False, null=True)
class Meta:
indexes = [
models.Index(fields=['item_id'])
]
def __str__(self):
return '{}, {}'.format(self.item_id, self.item_name)
class material_storage(models.Model):
storage_id = models.AutoField(primary_key=True)
material = models.ForeignKey(itemtype, on_delete=models.PROTECT)
amount_total = models.IntegerField(null=True)
price_avg = models.FloatField(null=True)
amount = models.IntegerField(null=True)
price = models.FloatField(null=True)
timestamp = models.DateTimeField(default=timezone.now)
def __str__(self):
return '{}, {} avg.: {} ISK'.format(self.material, self.amount, self.price)
I have a ModelForm based on the table material_storage, in which a checkbox indicates whether transport costs should be included or not.
In the form_valid() method of this ModelForm class the calculations are performed. To do so, I have to retrieve the volume per unit of the given item to use it for my transport cost calculations. Trying to geht that value the way shown below leads to an error I don't really understand.
class MaterialChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.item_name
class NewAssetForm(forms.ModelForm):
material = MaterialChoiceField(models.itemtype.objects.filter(used_in_storage= True))
needs_transport = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super(NewAssetForm, self).__init__(*args, **kwargs)
self.fields['amount'].widget.attrs['min'] = 1
self.fields['price'].widget.attrs['min'] = 0.00
class Meta:
model = models.material_storage
fields = (
'material',
'amount',
'price',
)
widgets = {
'material': forms.Select(),
}
class NewItemView(FormView):
template_name = 'assetmanager/newasset.html'
form_class = forms.NewAssetForm
success_url = '/storage/current'
def form_valid(self, form):
unit_volume = itemtype.objects.values('packaged_volume').filter(item_id=form.cleaned_data['material'])[0]['packaged_volume']
I believe that this has something to do with querying a different model than specified in the form, but I don't understand what exactly is the problem. Especially the fact, that running the exact same query in the django shell returns the correct value does not really help to understand what is going wrong here. Could somebody please tell me how to get the desired value the correct way?
Change last line from:
unit_volume = itemtype.objects.values('packaged_volume').filter(item_id=form.cleaned_data['material'])[0]['packaged_volume']
to:
unit_volume = itemtype.objects.values('packaged_volume').filter(item_id=form.cleaned_data['material'].item_id)[0]['packaged_volume']
The error says, you are giving Item instance to the query, where is item_id asked.

Django: Build up portfolio valuation table with two queryset

I want build a portfolio position table based on those models (models.py):
class Portfolio(models.Model):
"""
Portfolio model, portfolio description
"""
id = models.UUIDField(primary_key=True,default=uuid.uuid4,editable=False)
code = models.CharField(max_length=100, blank=True)
class PortfolioPosition(models.Model):
"""
PortfolioPosition Model, instruments hold in portfolio
"""
portfolio = models.ForeignKey(Portfolio, on_delete=models.CASCADE, related_name='portfolio',null=True)
private_debt_issue = models.OneToOneField(PrivateDebtIssue, on_delete=models.CASCADE)
start_date = models.DateField(null=False, default=date.today)
end_date = models.DateField(null=True, blank=True)
quantity = models.DecimalField(max_digits=12, decimal_places=4, default=Decimal('1.0000'))
class PrivateDebtIssue(models.Model):
"""
Model representing a Private Debt instrument.
"""
id = models.UUIDField(primary_key=True,default=uuid.uuid4,editable=False)
name = models.CharField(max_length=250, blank=True)
class PrivateDebtIssueValuation(models.Model):
"""
DataPoint value
"""
private_debt_issue = models.ForeignKey(PrivateDebtIssue,on_delete=models.CASCADE, related_name='private_debt_issue')
date = models.DateField()
fund_value = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
Maybe is not the best way to handle it, but I want build a portfolio position table with all open private debt positions and the last value, based on a input date.
I wrote this code:
class PortfolioDetailView(DetailView):
model = Portfolio
context_object_name = 'portfolio'
template_name = 'private_debts/portfolio_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# INPUT DATE BASED ON DATEPICKER
# set default date value
input_date = date.today()
if self.request.method == 'GET':
valuation_date = self.request.GET.get('valuation_date')
if valuation_date is not None:
input_date = datetime.strptime(valuation_date,'%m/%d/%Y')
# based on Portfolio object get portfolio positions and related PrivateDebtIssue objects
context['portfolio_position'] = PortfolioPosition.objects.all().select_related('portfolio','private_debt_issue').filter(
Q(portfolio=self.object)
& Q(start_date__lte=input_date)
& (Q(end_date__isnull=True) | Q(end_date__gte=input_date))
)
context['position_value'] = queryset
return context
This code works fine, but how do I get the value of the PrivateDebtIssue for a specific date and put it in a table?
I want to build a table with portfolio PortfolioPosition .position, PrivateDebtIssue.Name and PrivateDebtIssueValuation.fund_value, based on a valuation date.
At the end, I did something not recommended in Django: direct query with execute function. Like this
class PortfolioDetailView(DetailView):
model = Portfolio
context_object_name = 'portfolio'
template_name = 'private_debts/portfolio_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# set default date value
input_date = date.today()
input_date = date(2019,12,31)
if self.request.method == 'GET':
valuation_date = self.request.GET.get('valuation_date')
if valuation_date is not None:
input_date = datetime.strptime(valuation_date,'%m/%d/%Y')
sql = """
SELECT * \
FROM private_debts_portfolioposition \
INNER JOIN private_debts_privatedebtissue \
ON (private_debts_portfolioposition.private_debt_issue_id = private_debts_privatedebtissue.id) \
INNER JOIN private_debts_privatedebtissuevaluation \
ON (private_debts_privatedebtissuevaluation.private_debt_issue_id = private_debts_privatedebtissue.id) \
WHERE private_debts_portfolioposition.portfolio_id = '{}'::uuid \
AND private_debts_privatedebtissuevaluation.date = '{}'
""".format(self.object.id, input_date)
portfolio_position = my_custom_sql(sql)
context['portfolio_position_table'] = portfolio_position
context['valuation_date'] = valuation_date
return context
And I used the example 'suggested' by the docs
These are the two functions:
def dictfetchall(cursor):
"Return all rows from a cursor as a dict"
columns = [col[0] for col in cursor.description]
return [
dict(zip(columns, row))
for row in cursor.fetchall()
]
def my_custom_sql(sql):
with connection.cursor() as cursor:
cursor.execute(sql)
row = dictfetchall(cursor)
return row
I hope someone could suggest a better way to handle it.
Thanks.

Django creates new cart-product instead of updating already existing object

In my django shop I have a adding to cart function. But if I add the same product 2 times to the cart with a different quantity, 2 different objects are created. What's wrong with my code?
here is my view
def add_to_cart_view(request):
cart = getting_or_creating_cart(request)
product_slug = request.POST.get('product_slug')
product = Product.objects.get(slug=product_slug)
if request.method == "POST":
form = CartAddProductForm(request.POST or None)
if form.is_valid():
quantity = form.cleaned_data['quantity']
new_item, created = CartItem.objects.get_or_create(
product=product,
item_cost=product.price,
quantity=quantity,
all_items_cost=product.price*quantity,
)
if new_item.product.title == product.title:
cart.items.add(new_item)
cart.save()
if not created:
new_item.quantity += quantity
new_item.save(force_update=True)
cart.save()
new_cart_total = 0.00
for item in cart.items.all():
new_cart_total += float(item.all_items_cost)
cart.cart_total_cost = new_cart_total
cart.save()
return JsonResponse({
'cart_total': cart.items.count()
})
And here is my models
class CartItem(models.Model):
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
quantity = models.PositiveIntegerField(null=True, default=1)
item_cost = models.DecimalField(max_digits=9, decimal_places=2, default=0.00)
all_items_cost = models.DecimalField(max_digits=9, decimal_places=2, default=0.00)
def __str__(self):
return str(self.product.title)
class Cart(models.Model):
items = models.ManyToManyField(CartItem, blank=True)
cart_total_cost = models.DecimalField(max_digits=9, decimal_places=2, default=0.00)
def __str__(self):
return str(self.id)
Thanks for any help!
#dirkgroten provided a very good answer, you can also use unique_together option to prevent creating a duplicate entry
suppose, you've three fields name, size, brand in the Product model
and you don't want to create any new entry with the same name, size and brand
You can set it like
class Product:
name = CharField(....)
size = CharField(....)
brand = CharField(....)
field4 = CharField(....)
class Meta:
unique_together = ("name","size","brand")
I personally do not entertain the use of unique_together, but it'll surely prevent from creating multiple entries in these kinds of situations from DB definition side, but you've to handle the same in the code too
The get_or_create function will try to fetch a CartItem with all the exact properties you pass it. In your case you're trying to match against product, item_cost, quantity and all_items_cost. If you pass it the same product with a different quantity, it won't match, it'll create a new CartItem.
Look at the documentation of get_or_create. Use only product for the query and defaults for setting the value when creating a new CartItem:
new_item, created = CartItem.objects.get_or_create(
product=product,
defaults = dict(
item_cost=product.price,
quantity=quantity,
all_items_cost=product.price*quantity),
)

Custom validation of ModelForm in Django

I am working on a Django app to register sales. I have created three models: Project, Employee, and Sale.
The project and employee models are as follows:
class Project(models.Model):
project_id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 100, unique = True)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Employee(models.Model):
employee_id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 50)
email = models.CharField(max_length = 40)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
Then the sale model:
class Sale(models.Model):
sale_name = models.CharField(max_length = 30)
project = models.ForeignKey('Project', on_delete = models.CASCADE)
proactive_seller = models.ManyToManyField(Employee, related_name = 'proactive')
participants = models.ManyToManyField(Employee, related_name = 'participant')
doers = models.ManyToManyField(Employee, related_name = 'doer')
start_date = models.DateField()
end_date = models.DateField()
def __str__(self):
return self.sale_name
So the each sale object contains information on what project the sale is related to, which employee was the proactive/lead seller, which employees were participating in the sale, and also which employees will be doing the actual project.
In my forms.py I want to make sure that the sales are unique in the sense that I want to raise an error if the user is trying to enter a sale which already has the same project, same date and the same doers, i.e. the doers can't be allocated to the project more than once at a time.
My forms.py is currently looking like this:
class SaleForm(ModelForm):
class Meta:
model = Sale
widgets = {
'start_date': DatePickerInput(),
'end_date': DatePickerInput(),
}
I tried the following:
def clean(self):
cleaned_data = super.clean()
start = cleaned_data.get('start_date')
end = cleaned_data.get('end_date')
doers = cleaned_data.get('doers')
project = cleaned_data.get('project')
if start and end and doers and project:
queryset = Sale.objects.all()
# Filter based on project
q = queryset.filter(project__name=project, start_date = start, end_date = end)
for employee in doers:
q = q.filter(doers__name=employee)
if q.count() > 1:
raise forms.ValidationError('Sale has already been registered.')
However, the validation does not work as expected: I am still allowed to allocate “employees” to the same “project” at the same time (ie start date and end date).
Help is much appreciated.
What you're attempting to do is validate every instance of the M2M relationship for a given instance. This can be fairly difficult to do. What should suffice is to filter down on sales with the same number of doers and filter out sales that contain a different doer on them.
from django.db.models import F, OuterRef, Exists, Q
q = queryset.filter(project__name=project, start_date=start, end_date = end)
other_doers = Employee.objects.filter(
# Exclude any employee with the name of the doers on this project.
# We only want other doers.
~Q(name__in=[e.name for e in doers]),
# This links the subquery to the main query (Sale)
doer=OuterRef('id'),
)
q = q.annotate(
# Get the count of doers per sale
doer_count=Count('doers__id', distinct=True),
# Check if other doers are in the project
has_other_doer=Exists(other_doers)
).filter(
# Only look for sales with the same number of doers
doer_count=len(doers),
# Filter out sales that contain other doers
has_other_doer=False,
)

updating account balance with django

absolute n00b here, just fiddling around. Trying to make a very simple app to track my personal expenses. I have a class for entering the expenses, a class for the categories and a class for my account balance. Plan is to create en entry in the account balance everytime I create an expense.
How do I update my account balance? I'll have to get fields from the latest entry in expenses to do the math with in my balance class, right?
This is what I have. Any help appreciated.
from django.db import models
from django.utils import timezone
class Category(models.Model):
category = models.CharField(max_length=200,blank=False)
def __str__(self):
return self.category
class Balance(models.Model):
date = models.DateTimeField(default=timezone.now)
previous_balance = ????
transaction = ????
current_balance = previous_balance - transaction
class Expense(models.Model):
date = models.DateTimeField(default=timezone.now)
spender = models.ForeignKey('auth.User')
description = models.CharField(max_length=200)
category = models.ForeignKey(Category,default=1)
ABN = 'ABN'
ING = 'ING'
ACCOUNT_CHOICES = (
(ABN, 'ABN'),
(ING, 'ING'),
)
account = models.CharField(
max_length=30,
choices=ACCOUNT_CHOICES,
default=ABN,
)
amount = models.DecimalField(max_digits=10, decimal_places=2)
def commit(self):
self.commit_date = timezone.now()
self.save()
def __str__(self):
return u"%s. Kosten: %s" % (self.description, self.amount)
If I'm understanding your question correctly, you want to be able to get your current balance after creating Expenses. If so, you can use Django's aggregation:
from django.db.models import Sum
class Balance(models.Model):
date = models.DateTimeField(default=timezone.now)
# Keep the amount you start with
starting_balance = models.IntegerField()
# Get the Sum of all expenses and do some simple subtraction
def get_current_balance(self):
total_expenses = Expense.objects.all().aggregate(Sum('amount'))
return self.starting_balance - total_expenses['amount__sum']
Then in your views, you can do something like:
current_balance = some_balance_instance.get_current_balance()
considered balance change will be trigger by expense record change, you can overwrite save on Expense model. then balance table can be maintain in auto.
import datetime
class Expense(models.Model):
date = models.DateTimeField(default=timezone.now)
spender = models.ForeignKey('auth.User')
description = models.CharField(max_length=200)
category = models.ForeignKey(Category,default=1)
ABN = 'ABN'
ING = 'ING'
ACCOUNT_CHOICES = (
(ABN, 'ABN'),
(ING, 'ING'),
)
account = models.CharField(
max_length=30,
choices=ACCOUNT_CHOICES,
default=ABN,
)
amount = models.DecimalField(max_digits=10, decimal_places=2)
def save(self, *args, **kwargs):
super(Expense, self).save(*args, **kwargs)
last_bal = Balance.objects.order_by('id').last()
Balance.objects.create(date=datetime.datetime.now(), previouse_balance=last_bal.current_balance,
transaction=self, current_balance=last_bal.current_balance + self.amount)