Trying to create a one-time calculated field in Django model

75 Views Asked by At

Building my first Django app, and I'm running into a snag. I have a Django model that creates Job objects, and I want each job code to be unique and auto-generated, with a particular format. The format is: aaaMMnnYYYY, where aaa is a 3-letter client identifier that we set, nn is a counter that represents the nth job from that client in that month., and MM and YYYY are month and year respectively. e.g., for the 3rd job from client "AIE" in feb 2023, the ID would be AIE02032023.

Using a calculated property with the @property decorator causes the field to be updated with every save, so I'm trying to do this by modifying the save() method. There's also a related Cost object that has a Job attribute as a Foreign Key. The way I have it now, the job code gets assigned as expected, but when I add a Cost to the Job, the 'iterating' part of the job code iterates, changing the job code, which causes uniqueness errors as well as URL errors (I'm using the job code in the URLConf. Is there any way to have this field get calculated once and then never change?

As a side note, I'd also like to be able to override the job code. Is there a way to set flags within a model, such as job_code_overridden = False, etc.?

Here's the relevant code, let me know what else you need to see.

models.py:

class Job(models.Model):
    
    job_name = models.CharField(max_length=50, default='New Job')
    client = models.ForeignKey(Client, on_delete=models.CASCADE)
    job_code = models.CharField(max_length=15, unique=True,)

    def get_job_code(self):
        '''
        I only want this to run once
        Format abcMMnnYYYY

        '''
        jc = ''
        prefix = self.client.job_code_prefix
        month = str(str(self.job_date).split('-')[1])
        identifier = len(Job.objects.filter(job_date__contains = f'-{month}-',
                                    client__job_code_prefix = prefix)) + 2
        year = str(str(self.job_date).split('-')[0])
        jc = f'{prefix}{month}{identifier:02d}{year}'

        return jc


    @property
    def total_cost(self):
        all_costs = Cost.objects.filter(job__job_code = self.job_code)
        total = 0
        if all_costs:
            for cost in all_costs:
                total += cost.amount
        return total

        # Is there a way to add something like the flags in the commented-out code here?
    def save(self, *args, **kwargs):
        # if not self.job_code_fixed:
        if self.job_code != self.get_job_code():
             self.job_code = self.get_job_code()
             # self.job_code_fixed = True
        super().save(*args, **kwargs)

costsheet.py:

class costsheetView(ListView):
    template_name = "main_app/costsheet.html"
    form_class = CostForm
    model = Cost
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        current_job_code = self.kwargs['job_code']
        currentJob = Job.objects.get(job_code=current_job_code)
        return context

    def get(self, request, *args, **kwargs):
        cost_form = self.form_class()
        current_job_code = self.kwargs['job_code']
        currentJob = Job.objects.get(job_code=current_job_code)
        all_costs = Cost.objects.filter(job__job_code = current_job_code)
        return render(request, self.template_name, {'cost_form':cost_form, 'currentJob':currentJob,'all_costs':all_costs})

    def post(self, request, *args, **kwargs):
        cost_form = self.form_class(request.POST)
        current_job_code = self.kwargs['job_code']
        currentJob = Job.objects.get(job_code=current_job_code)
        messages = []
        errors = ''
        if cost_form.is_valid():
            instance = cost_form.save()
            instance.job = currentJob
            instance.save()
            currentJob.vendors.add(instance.vendor)
            currentJob.save()
            messages.append(f'cost added, job date: {currentJob.job_date}')
        else: 
            print('oops')
            print(cost_form.errors)
            errors = cost_form.errors

        all_costs = Cost.objects.filter(job__job_code = current_job_code)
        return render(request, self.template_name, {'cost_form':cost_form, 
                                                             'currentJob':currentJob, 
                                                    'errors':errors, 
                                                    'messages':messages,
                                                    'all_costs':all_costs,
                                                    })

Lastly, in the save() method I know I could do something like

if job_code != get_job_code():
   job_code = get_job_code()

..but the job 'month' often changes throughout the life of the job, and if I run get_job_code() after the month changes then the job code will change again, which is undesirable.

1

There are 1 best solutions below

0
On BEST ANSWER

In the end I simply overrode the save() method, using a job_code_is_fixed boolean flag to keep the job_code from updating.

class Job(models.Model):
    #...
    job_code = models.CharField(
        max_length=15, unique=True, blank=True, null=True
        )
    job_code_is_fixed = models.BooleanField(default=False)
    #...

    def set_job_code(self, prefix=None, year=None, month=None):
        '''
        Create a job code in the following format:
        {prefix}{month:02d}{i:02d}{year}

        e.g. APL06012023:
        prefix: APL
        month: 06
        i: 01 
        year: 2023

        params:
        prefix: job code prefix
        year: the year to be used in the job code
        month: the month to be used in the job code
        i: iterator for generating unique job codes
        passable args are auto generated, so they default to None unless passed in.

        This function will run in the save() method as long as job_code_is_fixed is False
        '''

        jc = ''
        prefix = self.client.job_code_prefix if prefix is None else prefix
        month = int(timezone.now().month) if month is None else month
        year = int(timezone.now().year) if year is None else year

        i = 1
        while jc == '' and i <= 99:

            temp_jc = f'{prefix}{month:02d}{i:02d}{year}'
            if not Job.objects.filter(job_code=temp_jc).exists():
                jc = temp_jc
                return jc
            else:
                i += 1

        if jc == '':
            # Handle error

    def save(self, *args, **kwargs):
    # Auto-generate the job code
        if not self.job_code_is_fixed:
            self.job_code = self.set_job_code()
            self.job_code_is_fixed = True
        # ...
        super().save(*args, **kwargs)