Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FR] Calculating Subtotal of Sale Order Items for use in Sale Order Form #7608

Closed
1 of 2 tasks
bradenfallon opened this issue Jul 10, 2024 · 3 comments
Closed
1 of 2 tasks
Labels
enhancement This is an suggested enhancement or new feature inactive Indicates lack of activity triage:not-checked Item was not checked by the core team

Comments

@bradenfallon
Copy link

Please verify that this feature request has NOT been suggested before.

  • I checked and didn't find a similar feature request

Problem statement

Below is a code snippet from a sale order template. This snippet generates a table that has the sale order items and a separate area for the line order items. In my case, I am modeling this sale order template after a receipt. I would like to include the total of only the sale order items as a "subtotal" at the base of the sale order items portion of the table. The "total" will appear at the bottom off the extra line portion of the table. Currently, a variable to reference that "subtotal" does not exist. Only the order.total_price variable to get the overall total including extra line items.

<table class='table table-striped table-condensed'>

    <thead>
        <tr>
            <th>{% trans "Item" %}</th>
            <th>{% trans "Reference" %}</th>
            <th>{% trans "Quantity" %}</th>
            <th>{% trans "Unit Price" %}</th>
            <th>{% trans "Total Price" %}</th>
        </tr>
    </thead>

    <body>

        {% for line in lines.all %}
        <tr>
            <td>
                <div class='thumb-container'>
                    <img src='{% part_image line.part height=240 %}' alt='' class='part-thumb'>
                    <span class='part-text'>
                        {{ line.part.name }}
                    </span>
                </div>
            </td>
            <td>{{ line.part.IPN }}</td>
            <td>{% decimal line.quantity %}</td>
            <td>{% render_currency line.price %}</td>
            <td>{% render_currency line.total_line_price %}</td>
        </tr>
        {% endfor %}

        <tr>
            <td colspan="3"></td>
            <th>{% trans "Subtotal" %}</th>
            <td>{% render_currency subtotal_variable currency=order.currency %}</td>
        </tr>

Suggested solution

After looking through the code on Github I think I have narrowed down the two files that would need to be altered to include this "subtotal" variable. The admin.py and models.py in InvenTree/src/backend/InvenTree/order/. I am trying to build a potential solution to this, but my unfamiliarity with the code and all references is making it challenging.

Here are some of the relevant places I think the code would need altering.

class TotalPriceResourceMixin:
    """Mixin for exporting total price data."""

    total_price = Field(attribute='total_price', column_name=_('Total Price'))

    def dehydrate_total_price(self, order):
        """Return the total price amount, not the object itself."""
        if order.total_price:
            return order.total_price.amount
        return ''
    def save(self, *args, **kwargs):
        """Update the total_price field when saved."""
        # Recalculate total_price for this order
        self.update_total_price(commit=False)

        if hasattr(self, '_SAVING_TOTAL_PRICE') and self._SAVING_TOTAL_PRICE:
            # Avoid recursion on save
            return super().save(*args, **kwargs)
        self._SAVING_TOTAL_PRICE = True

        # Save the object as we can not access foreign/m2m fields before saving
        self.update_total_price(commit=True)

    total_price = InvenTreeModelMoneyField(
        null=True,
        blank=True,
        allow_negative=False,
        verbose_name=_('Total Price'),
        help_text=_('Total price for this order'),
    )
    def update_total_price(self, commit=True):
        """Recalculate and save the total_price for this order."""
        self.total_price = self.calculate_total_price(target_currency=self.currency)

        if commit:
            self.save()

    def calculate_total_price(self, target_currency=None):
        """Calculates the total price of all order lines, and converts to the specified target currency.

        If not specified, the default system currency is used.

        If currency conversion fails (e.g. there are no valid conversion rates),
        then we simply return zero, rather than attempting some other calculation.
        """
        # Set default - see B008
        if target_currency is None:
            target_currency = currency_code_default()

        total = Money(0, target_currency)

        # Check if the order has been saved (otherwise we can't calculate the total price)
        if self.pk is None:
            return total

        # order items
        for line in self.lines.all():
            if not line.price:
                continue

            try:
                total += line.quantity * convert_money(line.price, target_currency)
            except MissingRate:
                # Record the error, try to press on
                _1, _2, _3 = sys.exc_info()

                log_error('order.calculate_total_price')
                logger.exception("Missing exchange rate for '%s'", target_currency)

                # Return None to indicate the calculated price is invalid
                return None

        # extra items
        for line in self.extra_lines.all():
            if not line.price:
                continue

            try:
                total += line.quantity * convert_money(line.price, target_currency)
            except MissingRate:
                # Record the error, try to press on

                log_error('order.calculate_total_price')
                logger.exception("Missing exchange rate for '%s'", target_currency)

                # Return None to indicate the calculated price is invalid
                return None

        # set decimal-places
        total.decimal_places = 4

        return total

Describe alternatives you've considered

The other way to achieve this is to open the template editor and open the respective sales order. Then create a <p> or <span> and manually calculate and insert the value. I was trying to automate this, but this is an option.

Examples of other systems

No response

Do you want to develop this?

  • I want to develop this.
@bradenfallon bradenfallon added enhancement This is an suggested enhancement or new feature triage:not-checked Item was not checked by the core team labels Jul 10, 2024
@bradenfallon
Copy link
Author

bradenfallon commented Aug 4, 2024

@SchrodingersGat

I ended up adding a custom filters file to the template tags folder.
I couldn't figure out how to get it to work in the main InvenTree files, so I made a filter instead.
Inside that custom filters file, I added the following code:

import requests
import json
import qrcode as python_qrcode
import report.helpers
from PIL import Image
from django import template
from django.utils.html import format_html

register = template.Library()

@register.simple_tag()
def calculate_subtotal(lines):
    subtotal_sum = 0.0
    for line in lines:
        # Remove any $ or commas from the total_line_price and convert to float
        total_line_price_str = str(line.total_line_price).replace('$', '').replace(',', '')
        try:
            total_line_price_float = float(total_line_price_str)
        except ValueError:
            total_line_price_float = 0.0

        # Add the current line price to the total sum
        subtotal_sum += total_line_price_float
    
    # Format the total sum as currency
    formatted_subtotal_sum = "${:,.2f}".format(subtotal_sum)
    return formatted_subtotal_sum

I then call the filter in the Sale Order template like so:
{% calculate_subtotal lines.all as subtotal_amount %}

@Alexbits
Copy link

This can also be implemented with a custom ReportMixin plugin by adding a context variable to the report.

from plugin import InvenTreePlugin
from plugin.mixins import ReportMixin
from decimal import *
from djmoney.money import Money
from djmoney.contrib.exchange.models import convert_money
from common.currency import currency_code_default

class InvoiceReportPlugin(ReportMixin, InvenTreePlugin):
    NAME = 'Invoice Report Plugin'
    SLUG = 'invoicereportplugin'
    TITLE = 'Invoice Report Plugin'
    DESCRIPTION = 'A plugin which adds total_no_extra context variable to the report context.'
    VERSION = '0.0.1'

    def calculate_total_no_extra_lines(self, order, target_currency):        
        total = Money(0, target_currency)
        for line in order.lines.all():
            if not line.price:
                continue
            total += line.quantity * convert_money(line.price, target_currency)
        return total

    def add_report_context(self, report_instance, model_instance, request, context):
        """Add example content to the report instance."""
        # We can add any extra context data we want to the report
        # Generate a random string of data
        if report_instance.model_type == 'salesorder':             
            target_currency = currency_code_default()            
            context['total_no_extra'] = self.calculate_total_no_extra_lines(model_instance, target_currency)

Copy link
Contributor

This issue seems stale. Please react to show this is still important.

@github-actions github-actions bot added the inactive Indicates lack of activity label Nov 21, 2024
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Nov 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement This is an suggested enhancement or new feature inactive Indicates lack of activity triage:not-checked Item was not checked by the core team
Projects
None yet
Development

No branches or pull requests

2 participants