How to introduce a model class method in Django admin - django

I have a model that has class methods. In testing the class methods work and alter the model instances according to my needs. The issue is using this class method in the admin. When an application cannot pay a late payment fee is applied creating another transaction altering the balance. The method in models is decorated with a #classmethod decorator:
class Transactions(models.Model):
application = models.ForeignKey(Application,
related_name='application_id', blank=True, null=True)
transaction_type = models.CharField(max_length=56,
choices=TRANSACTION_TYPE, null=True)
transaction_source = models.CharField(max_length=56,
choices=TRANSACTION_SOURCE, null=True)
transaction_method = models.CharField(max_length=56,
choices=TRANSACTION_METHOD, null=True)
amount = models.DecimalField(max_digits=10, decimal_places=2)
created_date = models.DateField()
posted_date = models.DateField()
external_reference = models.CharField(max_length=100, null=True,
verbose_name='External Reference')
def __unicode__(self):
return self.application.forename + " " +
self.application.surname + "(" + str(self.application.id) + ")"
#classmethod
def late_payment_fee(cls, app, desired_action):
"""
This function enacts a late payment fee to an application as a
transaction
:param app: application the fee is going to be applied to
:param desired_action: either True or False, when reversing the
fee the transaction shouldn't be deleted,
just another transaction of the opposite effect in order to
help loans collection with tracking, True will
enact a feee, False will reverse the fee
:return: a transaction which is stored in the database
"""
today = str(date.today())
if desired_action:
trans_type = MISSEDREPAYMENTFEE
amount = float(12)
else:
trans_type = MISSEDREPAYMENTFEEREVERSAL
amount = float(-12)
cls.create_trasaction(app, trans_type, INTERNALBOOKING,
INTERNALBOOKING, amount, today, today, None)
I need to get it so when status is altered, or when a tickbox is checked in the admin for an application it fires the class method. I have Googled overriding models in admin but cannot find anything. Here's the admin:
class ApplicationAdmin(ImportExportModelAdmin):
resource_class = ApplicationResource
search_fields = ['forename', 'surname']
list_filter = ('status', 'underwritingresult', 'test', 'signed',
'mandateapproved', 'dealership', 'brand')
list_select_related = ('instalment',)
list_display = ('id', 'SPV_ID', 'forename', 'surname'......
inlines = [
InstalmentInline,
AddressInline
]
exclude = ['customer', 'accountnumber', 'sortcode']
readonly_fields = ('Account_Number', 'Sort_Code', 'SPV_ID')
def get_readonly_fields(self, request, obj=None):
readonly_fields = []
if obj.locked :
for field in self.model._meta.fields:
readonly_fields.append(field.name)
else:
readonly_fields = ('Account_Number', 'Sort_Code', 'SPV_ID')
return readonly_fields
def Account_Number(self, obj):
return Decrypt(obj.accountnumber)
def Sort_Code(self, obj):
return Decrypt(obj.sortcode)
def SPV_ID(self, obj):
return obj.spv.id
def has_delete_permission(self, request, obj=None):
return False
Many thanks for reading this.

I've now found the solution (sorry if my initial question was not very clear). It turned out that I needed to override the save function in the Application model. However, another issue was raised when doing this. If a user changed something benign like the name of the customer then the class method would fire and create new transactions even though there was no real change. Because of this, we have to override the init and the save function in the model but still actually save. For this model, we are interested to see if the status has changed or a late fee has been applied. In order to do this, we have to store the initial status values by overriding the init method in the model:
__initial_status = None
__initial_fee_status = None
def __init__(self, *args, **kwargs):
"""
This function overrides the __init__ function in order to
save initial_status facilitating comparisons in the save function
:param args: allows the function to accept an arbitrary number of
arguments and/or keyword arguments
:param kwargs: same as above
"""
super(Application, self).__init__(*args, **kwargs)
self.__initial_status = self.status
self.__initial_fee_status = self.feeadded
Now we have them stored we can pass them through our save function. If there is a change we can utilize the class method:
def save(self, *args, **kwargs):
"""
This function overrides the standard save function. The application
is still saved, however, other functions are fired when it is
saved.
:return: creates reversal commission and dealership transactions
if the status is CANCELLED
"""
if self.signed and self.status != self.__initial_status:
if self.status == Application.CANCELLED:
Transactions.create_commision_trasaction(app=self,
reverse=True)
Transactions.create_dealership_trasaction(app=self,
reverse=True)
elif self.status == Application.OK:
Transactions.create_commision_trasaction(app=self,
reverse=False)
Transactions.create_dealership_trasaction(app=self,
reverse=False)
if self.signed and self.feeadded != self.__initial_fee_status:
if self.feeadded:
Transactions.late_payment_fee(app=self, desired_action=True)
elif self.feeadded is False:
Transactions.late_payment_fee(app=self, desired_action=False)
super(Application, self).save(*args, **kwargs)

Related

django create custom id def save()

I have this postgres function, What it does is when I save an item, it creates an cl_itemid test2021-20221-1. then in table clearing_office the column office_serial increments +1 so next item saved will be test2021-20221-2
SELECT CONCAT(f_office_id,f_sy,f_sem,'-',office_serial) INTO office_lastnumber from curriculum.clearing_office
where office_id=f_office_id;
UPDATE curriculum.clearing_office SET office_serial =office_serial+1 where office_id=f_office_id;
{
"studid": "4321-4321",
"office": "test",
"sem": "1",
"sy": "2021-2022",
}
Is it possible to create this through Django models or perhaps maybe an alternative solution for this problem?
This is my model class
class Item(models.Model):
cl_itemid = models.CharField(primary_key=True, max_length=20)
studid = models.CharField(max_length=9, blank=True, null=True)
office = models.ForeignKey('ClearingOffice', models.DO_NOTHING, blank=True, null=True)
sem = models.CharField(max_length=1, blank=True, null=True)
sy = models.CharField(max_length=9, blank=True, null=True)
class Meta:
managed = False
db_table = 'item'
One simple way might be to use a default function in python.
def get_default_id():
last_id = Item.objects.last().cl_itemid
split_id = last_id.split('-')
split_id[-1] = str(int(split_id[-1])+1)
new_id = '-'.join(split_id)
return new_id
class Item(models.Model):
cl_itemid = models.CharField(..., default=get_default_id)
Similar approach described in this answer.
One thing you will need to anticipate with this approach is that there might be a race condition when two or more new objects are created simultaneously (i.e., the default function runs at the same time for two new objects, potentially resulting in the same ID being returned for the two new objects). You can work around that potential issue with transactions or retries if you think it will be a problem in your application.
A robust approach would be to create your own model field and handle the implementation internally in the field. That would allow you to implement the solution in different ways, depending on the DB dialect and have it work across DB implementations without the race condition issue.
I do this in alot of my Models, tad different. I just use Forms and tac on a custom save method
I haven't worked with unmanaged models, but from what I can see from google it's not different
Form (forms.py)
class ItemForm(forms.ModelForm):
class Meta:
model = Item
fields = (
'cl_itemid',
'studid',
'office',
'sem',
'sy',
)
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
obj = super(ItemForm, self).save(commit=False)
if not self.instance:
# you only want it hitting this if it's new!
from datetime import datetime
now = datetime.now()
count = Item.objects.all().count()
# you could also just grab the last
# + chop the id to get the last number
obj.cl_itemid = 'test{0}-{1}'.format(now.strftime('%Y-%m%d%y'), count)
if commit:
obj.save()
You might even be able to tac it BEFORE the actual save method super(ItemForm, self).save(commit=False) or even do a cl_itemid ..i'll do some testing on my end to see if that could be an option, might be much cleaner
Usage
View (POST)
def ItemFormView(request):
from app.forms import ItemForm
if request.method == "POST":
# this is just for edits
itemObj = Item.objects.get(pk=request.POST.get('itemid')) if request.POST.get('itemid') else None
form = ItemForm(request.POST or None, instance=itemObj)
if form.is_valid():
obj = form.save()
return HttpResponseRedirect(reverse('sucesspage')
print(form.errors)
print(form.errors.as_data())
# this will render the page with the errors
data = {'form': form}
else:
data = {'form': ItemForm()}
return render(request, 'itemform.html', data)
Shell
f = ItemForm(data={'office':'test', 'sem':'1', 'etc':'etc'})
if f.is_valid():
o = f.save()

Unable to get repr for queryset when call from Viewset

I am trying to call a queryset for a model to add to my serializer using objects.all() but the debug said Unable to set repr for <class 'django.db.models.query.Queryset'>
Here is my viewset
class TransactionReceiptViewSet(viewsets.GenericViewSet,
viewsets.mixins.RetrieveModelMixin,
viewsets.mixins.ListModelMixin):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
serializer_class = base_serializers.TransactionReceiptSerializer
queryset = models.TransactionReceipt.objects.all()
def get_queryset(self):
user = self.request.user
return models.TransactionReceipt.objects.filter(user_profile=user)
def retrieve(self, request, *args, **kwargs):
response = super(TransactionReceiptViewSet, self).retrieve(request, *args, **kwargs)
receipt = self.get_object()
serializer = self.get_serializer(receipt)
product_qs = models.ProductReceipt.objects.all()
products_data = base_serializers.ProductReceiptSerializer(
product_qs, many=True)
serializer.data['products'] = products_data
return Response(serializer.data)
and here is the model I tried to call for
class ProductReceipt(models.Model):
id = models.AutoField(primary_key=True)
amount = models.IntegerField(default=1)
product = models.ForeignKey(Product, on_delete=models.DO_NOTHING, default=None)
created_date = models.DateTimeField('Date of purchase', auto_now=True)
transaction_receipt = models.ForeignKey(TransactionReceipt, on_delete=models.CASCADE)
price = models.IntegerField(default=0)
def __str__(self):
return "object created"
def __init__(self):
super().__init__()
self.product = Product()
self.transaction_receipt = TransactionReceipt()
def save(self, **kwargs):
self.amount = 1
self.created_date = datetime.now()
self.price = self.product.price_tag.price
When I debug the API, it said that Unable to set repr for <class 'django.db.models.query.Queryset'> in product_qs and nothing is returned
Edit:
I think that the Model have to do something with this. I have tried to create a ModelViewSet for ProductReceipt and it worked fine. But when i try to make the query manually. it somehow broke the mapping to the foreign key??? and return nothing?
Okey lets check a couple of things. First of all in your ProductReceipt class the def save(self, **kwargs) method is not calling super and that's a huge problem because the objects are not gonna be saved ever. Secondly, in the ProductReceipt class the def __init__(self) method you are assigning a new Product and a new TransactionReceipt to your ProductReceipt instance, but you are not setting the data of this two objects neither saving them in any place (maybe you should assign them inside save method and save them before calling super?).
Try this corrections and if it keeps not working we will another possible errors.
Finally, def __str__(self) is a string representation of your object, it will be a good implementation for example:
def __str__(self):
return self.product.name + ' x' + str(amount)
Turn out that the product field is not set to null=True. And i have old data with that field point to nothing. There for it break when trying to query from the database. In short, i didn't migrate properly.

Related model not updating in save override of a model being saved with ModelFormset

So I have Transaction model that is FK-d to a Share. In an 'Account' view, I have a ModelFormset of these Transactions and I can save multiple transactions by looping through the forms and saving them.
On my Transaction's save() method I try and update the balance on the linked Share. this works if I save one transaction, but when I POST my ModelFormset with multiple transactions, everytime I hit the self.share.balance = self.share.balance + amt line in the Transaction save() override (that is for every new Transaction), the share.balance is what it was before any of the previous transactions in the formset were saved.
Does anyone know why the added amount to share balance from a previous saved transaction is not carried on the subsequent saves (why only the last Transaction's amount will be added to share balance)?
Transaction model which should update balance on parent-model Share
class Transaction(models.Model):
share = models.ForeignKey(Share, on_delete=models.CASCADE, null=True, blank=True)
account = models.ForeignKey(Account, on_delete=models.CASCADE, null=True, blank=True)
db_cr = models.CharField(choices=DBCR, max_length=2)
amt = models.DecimalField('Amount', max_digits=11, decimal_places=2)
post_dt = models.DateTimeField('Post Time', null=True, blank=True)
def save(self, *args, **kwargs):
if not self.pk:
...
if self.share:
if self in self.share.transaction_set.all():
logging.error('Transaction %s already posted' % self.id)
return False
amt = self.amt if self.db_cr == 'cr' else -self.amt
self.share.balance = self.share.balance + amt
self.share.save()
Share Model
class Share(models.Model):
name = models.CharField(max_length=80)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
definition = models.ForeignKey(ShareDef, on_delete=models.PROTECT)
balance = models.DecimalField('Balance', max_digits=11, decimal_places=2, default=0)
def __str__(self):
return '%s %s %s %s'%(self.account,
self.name,
self.definition.sym_code,
self.balance )
def save(self, *args, **kwargs):
if not self.pk:
if not self.name:
self.name = self.definition.name
super(Share, self).save(*args, **kwargs)
In view, I have a Transaction formset
#...in view
TranFormSet = modelformset_factory(Transaction, exclude=('origin','ach_entry'), extra=1)
if request.method=='POST':
...
tran_formset = TranFormSet(request.POST)
...
if tran_formset.is_valid():
for form in tran_formset:
tran = form.save(commit=False)
tran.account = account
tran.origin = 'tt'
tran.save()
else:
#...following kind of weird because of how I'm setting querysets of ModelChoiceFields
kwargs = {'account_instance': account}
tran_formset = TranFormSet(queryset=Transaction.objects.none())
tran_formset.form = (curry(TranForm, **kwargs))
Form
class TranForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
account_instance = kwargs.pop('account_instance', None)
super(TranForm, self).__init__(*args, **kwargs)
if account_instance:
self.fields['share'].queryset = account_instance.share_set.all()
if self.instance.pk:
del self.fields['share']
class Meta:
model=Transaction
exclude=['origin', 'ach_entry', 'account']
post_dt = forms.DateTimeField(initial=datetime.date.today(), widget=forms.TextInput(attrs=
{
'class': 'datepicker'
}))
share = forms.ModelChoiceField(empty_label='---------', required=False, queryset=Share.objects.all())
It's unclear what may be causing the issue, but it may be helpful to perform the update of the self.share.balance in a single update() query. This can be done using F expressions:
from django.db.models import F
class Transaction(models.Model):
# ...
def update_share_balance(self):
if self.db_cr == "cr":
amount = self.amt
else:
amount = -self.amt
# By using the queryset update() method, we can perform the
# change in a single query, without using a potentially old
# value from `self.share.balance`
return Share.objects.filter(id=self.share_id).update(
balance=F("balance") + amount
)
def save(self, *args, **kwargs):
if not self.pk:
# ...
if self.share:
# ...
self.update_share_balance()
# Also, be sure to call the super().save() method at the end!
super().save(*args, **kwargs)

RelatedObjectDoesNotExist - in clean function of the model

I have model
#with_author
class Lease(CommonInfo):
version = IntegerVersionField( )
is_renewed = models.BooleanField(default=False)
unit = models.ForeignKey(Unit)
is_terminated = models.BooleanField(default=False)
def __unicode__(self):
return u'%s %i %s ' % ("lease#", self.id, self.unit)
def clean(self):
model = self.__class__
if self.unit and (self.is_active == True) and model.objects.filter(unit=self.unit, is_terminated = False , is_active = True).count() == 1:
raise ValidationError('Unit has active lease already, Terminate existing one prior to creation of new one or create a not active lease '.format(self.unit))
and I have a form
class LeaseForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LeaseForm, self).__init__(*args, **kwargs)
self.fields['unit'].required = True
class Meta:
model = Lease
fields = [ 'unit',
'is_active','is_renewed', 'description']
and each time a save this form without selecting value for unit I am getting
error RelatedObjectDoesNotExist
from my clean function in model since there is no self.unit
but I am explicitly validating the unit field.(at least i believe so)
what am I doing wrong?
Note that full_clean() will not be called automatically when you call
your model’s save() method. You’ll need to call it manually when you
want to run one-step model validation for your own manually created
models. [docs]
This is apparently done for backwards compatibility reasons, check this ticket out.
Model's full_clean() method is responsible for calling Model.clean(), but because it's never been called, your clean method inside model is basically omitted.
You can do couple of things for this. You can call model's clean manually. Or, you can move your validation logic into ModelForm using its clean methods. If you are mainly creating instance via form, I think it's the best place to perform validation (and more common practice).
Try this:
class LeaseForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LeaseForm, self).__init__(*args, **kwargs)
self.fields['unit'].required = True
# IF your validation logic includes multiple fields override this
def clean(self):
cleaned_data = super(LeaseForm, self).clean()
# .. your logic
return cleaned_data
# IF just your field's value is enough for validation override this
def clean__unit(self):
data = self.cleaned_data.get('unit', None)
# .. your logic
return data
class Meta:
model = Lease
fields = [ 'unit',
'is_active','is_renewed', 'description']

ModelForm save fails

I am trying to save a modelform that represents a bank account but I keep getting a ValueError even though the form appears to validate. The models I have to use are:
class Person(models.Model):
name = models.CharField()
class Bank(models.Model):
bsb = models.CharField()
bank_name = models.CharField()
def __unicode__(self):
return '%s - %s', (self.bank_name, self.bsb)
def _get_list_item(self):
return self.id, self
list_item = property(-get_list_item)
class BankAccount(models.Model):
bank = models.ForignKey(Bank)
account_name = models.CharField()
account_number = models.CharField()
class PersonBankAcc(models.Model):
person = models.ForeignKey(Person)
The ModelForm for the personBankAcc;
def PersonBankAccForm(forms.ModelForm):
bank = forms.ChoiceField(widget=SelectWithPop)
class Meta:
model = PersonBankAcct
exclude = ['person']
def __init__(self, *args, **kwargs):
super(PersonBankAccForm, self).__init__(*args, **kwargs)
bank_choices = [bank.list_item for banks in Bank.objects.all()]
bank_choices.isert(0,('','------'))
self.fields['bank'].choices = bank_choices
The view is:
def editPersonBankAcc(request, personBankAcc_id=0):
personBankAcc = get_object_or_404(PersonBankAcc, pk=personBankAcc_id)
if request.method == 'POST':
form = PersonBankAccForm(request.POST, instance=personBankAcc )
if form.is_valid():
print 'form is valid'
form.save()
return HttpResponseRedirect('editPerson/' + personBankAcc.person.id +'/')
else:
form = PersonBankAccForm(instance=personBankAcc )
return render_to_response('editPersonBankAcc', {'form': form})
When I try to save the form I get the a VlaueError exception even though it gets passed the form.is_valid() check, the error I get is:
Cannot assign "u'26'": PersonBankAcc.bank must be a "bank" instance
I know the issue is arising because of the widget I am using in the PersonBankAccForm:
bank = forms.ChoiceField(widget=SelectWithPop)
because if I remove it it works. But all that does is gives me the ability to add a new bank to the database via a popup and then inserts it into the select list, similar to the admin popup forms. I have checked the database and the new bank is added. But it fails even if I don't change anything, if I call the form and submit it, I get the same error.
I don't understand why it does not fail at the is_valid check.
Any help would be greatly appreciated.
Thanks
Andrew
better yet, i don't think it really needs to be in the init function...
def PersonBankAccForm(forms.ModelForm):
bank = forms.ModelChoiceField(queryset=Bank.objects.all(),widget=SelectWithPop(),empty_label='-----')
class Meta:
model = EmpBankAcct
exclude = ['person']
def __init__(self, *args, **kwargs):
super(PersonBankAccForm, self).__init__(*args, **kwargs)