Beispiel #1
0
def money_kwargs():
    """ returns the database settings for MoneyFields """
    from common.settings import currency_code_mappings, currency_code_default

    kwargs = {}
    kwargs['currency_choices'] = currency_code_mappings()
    kwargs['default_currency'] = currency_code_default()
    return kwargs
Beispiel #2
0
    def update_exchange_rates(self):
        """
        Update exchange rates each time the server is started, *if*:

        a) Have not been updated recently (one day or less)
        b) The base exchange rate has been altered
        """

        try:
            from djmoney.contrib.exchange.models import ExchangeBackend
            from datetime import datetime, timedelta
            from InvenTree.tasks import update_exchange_rates
            from common.settings import currency_code_default
        except AppRegistryNotReady:
            pass

        base_currency = currency_code_default()

        update = False

        try:
            backend = ExchangeBackend.objects.get(name='InvenTreeExchange')

            last_update = backend.last_update

            if last_update is not None:
                delta = datetime.now().date() - last_update.date()
                if delta > timedelta(days=1):
                    print(f"Last update was {last_update}")
                    update = True
            else:
                # Never been updated
                print("Exchange backend has never been updated")
                update = True

            # Backend currency has changed?
            if not base_currency == backend.base_currency:
                print(
                    f"Base currency changed from {backend.base_currency} to {base_currency}"
                )
                update = True

        except (ExchangeBackend.DoesNotExist):
            print("Exchange backend not found - updating")
            update = True

        except:
            # Some other error - potentially the tables are not ready yet
            return

        if update:
            update_exchange_rates()
Beispiel #3
0
def decimal2money(d, currency=None):
    """Format a Decimal number as Money.

    Args:
        d: A python Decimal object
        currency: Currency of the input amount, defaults to default currency in settings

    Returns:
        A Money object from the input(s)
    """
    if not currency:
        currency = currency_code_default()
    return Money(d, currency)
Beispiel #4
0
    def update_exchange_rates(self):  # pragma: no cover
        """Update exchange rates each time the server is started.

        Only runs *if*:
        a) Have not been updated recently (one day or less)
        b) The base exchange rate has been altered
        """
        try:
            from djmoney.contrib.exchange.models import ExchangeBackend

            from common.settings import currency_code_default
            from InvenTree.tasks import update_exchange_rates
        except AppRegistryNotReady:  # pragma: no cover
            pass

        base_currency = currency_code_default()

        update = False

        try:
            backend = ExchangeBackend.objects.get(name='InvenTreeExchange')

            last_update = backend.last_update

            if last_update is None:
                # Never been updated
                logger.info("Exchange backend has never been updated")
                update = True

            # Backend currency has changed?
            if base_currency != backend.base_currency:
                logger.info(
                    f"Base currency changed from {backend.base_currency} to {base_currency}"
                )
                update = True

        except (ExchangeBackend.DoesNotExist):
            logger.info("Exchange backend not found - updating")
            update = True

        except Exception:
            # Some other error - potentially the tables are not ready yet
            return

        if update:
            try:
                update_exchange_rates()
            except Exception as e:
                logger.error(f"Error updating exchange rates: {e}")
Beispiel #5
0
def update_exchange_rates():
    """
    Update currency exchange rates
    """

    try:
        from djmoney.contrib.exchange.models import ExchangeBackend, Rate

        from common.settings import currency_code_default, currency_codes
        from InvenTree.exchange import InvenTreeExchange
    except AppRegistryNotReady:  # pragma: no cover
        # Apps not yet loaded!
        logger.info(
            "Could not perform 'update_exchange_rates' - App registry not ready"
        )
        return
    except:  # pragma: no cover
        # Other error?
        return

    # Test to see if the database is ready yet
    try:
        backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
    except ExchangeBackend.DoesNotExist:
        pass
    except:  # pragma: no cover
        # Some other error
        logger.warning("update_exchange_rates: Database not ready")
        return

    backend = InvenTreeExchange()
    logger.info(f"Updating exchange rates from {backend.url}")

    base = currency_code_default()

    logger.info(f"Using base currency '{base}'")

    try:
        backend.update_rates(base_currency=base)

        # Remove any exchange rates which are not in the provided currencies
        Rate.objects.filter(backend="InvenTreeExchange").exclude(
            currency__in=currency_codes()).delete()
    except Exception as e:  # pragma: no cover
        logger.error(f"Error updating exchange rates: {e}")
Beispiel #6
0
    def update_rates(self, base_currency=currency_code_default()):

        symbols = ','.join(currency_codes())

        try:
            super().update_rates(base=base_currency, symbols=symbols)
        # catch connection errors
        except URLError:
            print('Encountered connection error while updating')
        except OperationalError as e:
            if 'SerializationFailure' in e.__cause__.__class__.__name__:
                print('Serialization Failure while updating exchange rates')
                # We are just going to swallow this exception because the
                # exchange rates will be updated later by the scheduled task
            else:
                # Other operational errors probably are still show stoppers
                # so reraise them so that the log contains the stacktrace
                raise
Beispiel #7
0
    def get_context_data(self, **kwargs):
        """Add data for template."""
        ctx = super().get_context_data(**kwargs).copy()

        ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')

        ctx["base_currency"] = currency_code_default()
        ctx["currencies"] = currency_codes

        ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")

        ctx["categories"] = PartCategory.objects.all().order_by(
            'tree_id', 'lft', 'name')

        # When were the rates last updated?
        try:
            backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
            ctx["rates_updated"] = backend.last_update
        except Exception:
            ctx["rates_updated"] = None

        # load locale stats
        STAT_FILE = os.path.abspath(
            os.path.join(settings.BASE_DIR, 'InvenTree/locale_stats.json'))
        try:
            ctx["locale_stats"] = json.load(open(STAT_FILE, 'r'))
        except Exception:
            ctx["locale_stats"] = {}

        # Forms and context for allauth
        ctx['add_email_form'] = AddEmailForm
        ctx["can_add_email"] = EmailAddress.objects.can_add_email(
            self.request.user)

        # Form and context for allauth social-accounts
        ctx["request"] = self.request
        ctx['social_form'] = DisconnectForm(request=self.request)

        # user db sessions
        ctx['session_key'] = self.request.session.session_key
        ctx['session_list'] = self.request.user.session_set.filter(
            expire_date__gt=now()).order_by('-last_activity')

        return ctx
Beispiel #8
0
class PriceBreak(models.Model):
    """
    Represents a PriceBreak model
    """
    class Meta:
        abstract = True

    quantity = InvenTree.fields.RoundingDecimalField(
        max_digits=15,
        decimal_places=5,
        default=1,
        validators=[MinValueValidator(1)],
        verbose_name=_('Quantity'),
        help_text=_('Price break quantity'),
    )

    price = MoneyField(
        max_digits=19,
        decimal_places=4,
        default_currency=currency_code_default(),
        null=True,
        verbose_name=_('Price'),
        help_text=_('Unit price at specified quantity'),
    )

    def convert_to(self, currency_code):
        """
        Convert the unit-price at this price break to the specified currency code.

        Args:
            currency_code - The currency code to convert to (e.g "USD" or "AUD")
        """

        try:
            converted = convert_money(self.price, currency_code)
        except MissingRate:
            print(
                f"WARNING: No currency conversion rate available for {self.price_currency} -> {currency_code}"
            )
            return self.price.amount

        return converted.amount
Beispiel #9
0
class EditSupplierPartForm(HelperForm):
    """ Form for editing a SupplierPart object """

    field_prefix = {
        'link': 'fa-link',
        'SKU': 'fa-hashtag',
        'note': 'fa-pencil-alt',
    }

    single_pricing = MoneyField(
        label=_('Single Price'),
        default_currency=currency_code_default(),
        help_text=_('Single quantity price'),
        decimal_places=4,
        max_digits=19,
        required=False,
    )

    manufacturer = django.forms.ChoiceField(
        required=False,
        help_text=_('Select manufacturer'),
        choices=[],
    )

    MPN = django.forms.CharField(
        required=False,
        help_text=_('Manufacturer Part Number'),
        max_length=100,
        label=_('MPN'),
    )

    class Meta:
        model = SupplierPart
        fields = [
            'part',
            'supplier',
            'SKU',
            'manufacturer',
            'MPN',
            'description',
            'link',
            'note',
            'single_pricing',
            # 'base_cost',
            # 'multiple',
            'packaging',
        ]

    def get_manufacturer_choices(self):
        """ Returns tuples for all manufacturers """
        empty_choice = [('', '----------')]

        manufacturers = [
            (manufacturer.id, manufacturer.name)
            for manufacturer in Company.objects.filter(is_manufacturer=True)
        ]

        return empty_choice + manufacturers

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.fields['manufacturer'].choices = self.get_manufacturer_choices()
Beispiel #10
0
def default_currency(*args, **kwargs):
    """ Returns the default currency code """
    return currency_code_default()
Beispiel #11
0
 def price_converted_currency(self):
     return currency_code_default()
Beispiel #12
0
class PurchaseOrderLineItem(OrderLineItem):
    """ Model for a purchase order line item.

    Attributes:
        order: Reference to a PurchaseOrder object

    """
    class Meta:
        unique_together = (('order', 'part'))

    def __str__(self):
        return "{n} x {part} from {supplier} (for {po})".format(
            n=decimal2string(self.quantity),
            part=self.part.SKU if self.part else 'unknown part',
            supplier=self.order.supplier.name,
            po=self.order)

    order = models.ForeignKey(PurchaseOrder,
                              on_delete=models.CASCADE,
                              related_name='lines',
                              verbose_name=_('Order'),
                              help_text=_('Purchase Order'))

    def get_base_part(self):
        """ Return the base-part for the line item """
        return self.part.part

    # TODO - Function callback for when the SupplierPart is deleted?

    part = models.ForeignKey(
        SupplierPart,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='purchase_order_line_items',
        verbose_name=_('Part'),
        help_text=_("Supplier part"),
    )

    received = models.DecimalField(decimal_places=5,
                                   max_digits=15,
                                   default=0,
                                   verbose_name=_('Received'),
                                   help_text=_('Number of items received'))

    purchase_price = MoneyField(
        max_digits=19,
        decimal_places=4,
        default_currency=currency_code_default(),
        null=True,
        blank=True,
        verbose_name=_('Purchase Price'),
        help_text=_('Unit purchase price'),
    )

    destination = TreeForeignKey(
        'stock.StockLocation',
        on_delete=models.DO_NOTHING,
        verbose_name=_('Destination'),
        related_name='po_lines',
        blank=True,
        null=True,
        help_text=_('Where does the Purchaser want this item to be stored?'))

    def get_destination(self):
        """Show where the line item is or should be placed"""
        # NOTE: If a line item gets split when recieved, only an arbitrary
        # stock items location will be reported as the location for the
        # entire line.
        for stock in stock_models.StockItem.objects.filter(
                supplier_part=self.part, purchase_order=self.order):
            if stock.location:
                return stock.location
        if self.destination:
            return self.destination
        if self.part and self.part.part and self.part.part.default_location:
            return self.part.part.default_location

    def remaining(self):
        """ Calculate the number of items remaining to be received """
        r = self.quantity - self.received
        return max(r, 0)
Beispiel #13
0
def get_price(instance, quantity, moq=True, multiples=True, currency=None, break_name: str = 'price_breaks'):
    """ Calculate the price based on quantity price breaks.

    - Don't forget to add in flat-fee cost (base_cost field)
    - If MOQ (minimum order quantity) is required, bump quantity
    - If order multiples are to be observed, then we need to calculate based on that, too
    """
    from common.settings import currency_code_default

    if hasattr(instance, break_name):
        price_breaks = getattr(instance, break_name).all()
    else:
        price_breaks = []

    # No price break information available?
    if len(price_breaks) == 0:
        return None

    # Check if quantity is fraction and disable multiples
    multiples = (quantity % 1 == 0)

    # Order multiples
    if multiples:
        quantity = int(math.ceil(quantity / instance.multiple) * instance.multiple)

    pb_found = False
    pb_quantity = -1
    pb_cost = 0.0

    if currency is None:
        # Default currency selection
        currency = currency_code_default()

    pb_min = None
    for pb in price_breaks:
        # Store smallest price break
        if not pb_min:
            pb_min = pb

        # Ignore this pricebreak (quantity is too high)
        if pb.quantity > quantity:
            continue

        pb_found = True

        # If this price-break quantity is the largest so far, use it!
        if pb.quantity > pb_quantity:
            pb_quantity = pb.quantity

            # Convert everything to the selected currency
            pb_cost = pb.convert_to(currency)

    # Use smallest price break
    if not pb_found and pb_min:
        # Update price break information
        pb_quantity = pb_min.quantity
        pb_cost = pb_min.convert_to(currency)
        # Trigger cost calculation using smallest price break
        pb_found = True

    # Convert quantity to decimal.Decimal format
    quantity = decimal.Decimal(f'{quantity}')

    if pb_found:
        cost = pb_cost * quantity
        return InvenTree.helpers.normalize(cost + instance.base_cost)
    else:
        return None
Beispiel #14
0
    def get_total_price(self, target_currency=currency_code_default()):
        """
        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.
        """

        total = Money(0, target_currency)

        # gather name reference
        price_ref_tag = 'sale_price' if isinstance(
            self, SalesOrder) else 'purchase_price'

        # order items
        for line in self.lines.all():

            price_ref = getattr(line, price_ref_tag)

            if not price_ref:
                continue

            try:
                total += line.quantity * convert_money(price_ref,
                                                       target_currency)
            except MissingRate:
                # Record the error, try to press on
                kind, info, data = sys.exc_info()

                Error.objects.create(
                    kind=kind.__name__,
                    info=info,
                    data='\n'.join(traceback.format_exception(
                        kind, info, data)),
                    path='order.get_total_price',
                )

                logger.error(f"Missing exchange rate for '{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
                kind, info, data = sys.exc_info()

                Error.objects.create(
                    kind=kind.__name__,
                    info=info,
                    data='\n'.join(traceback.format_exception(
                        kind, info, data)),
                    path='order.get_total_price',
                )

                logger.error(f"Missing exchange rate for '{target_currency}'")

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

        # set decimal-places
        total.decimal_places = 4

        return total
Beispiel #15
0
class SalesOrderLineItem(OrderLineItem):
    """
    Model for a single LineItem in a SalesOrder

    Attributes:
        order: Link to the SalesOrder that this line item belongs to
        part: Link to a Part object (may be null)
        sale_price: The unit sale price for this OrderLineItem
    """

    order = models.ForeignKey(SalesOrder,
                              on_delete=models.CASCADE,
                              related_name='lines',
                              verbose_name=_('Order'),
                              help_text=_('Sales Order'))

    part = models.ForeignKey('part.Part',
                             on_delete=models.SET_NULL,
                             related_name='sales_order_line_items',
                             null=True,
                             verbose_name=_('Part'),
                             help_text=_('Part'),
                             limit_choices_to={'salable': True})

    sale_price = MoneyField(
        max_digits=19,
        decimal_places=4,
        default_currency=currency_code_default(),
        null=True,
        blank=True,
        verbose_name=_('Sale Price'),
        help_text=_('Unit sale price'),
    )

    class Meta:
        unique_together = []

    def fulfilled_quantity(self):
        """
        Return the total stock quantity fulfilled against this line item.
        """

        query = self.order.stock_items.filter(part=self.part).aggregate(
            fulfilled=Coalesce(Sum('quantity'), Decimal(0)))

        return query['fulfilled']

    def allocated_quantity(self):
        """ Return the total stock quantity allocated to this LineItem.

        This is a summation of the quantity of each attached StockItem
        """

        query = self.allocations.aggregate(
            allocated=Coalesce(Sum('quantity'), Decimal(0)))

        return query['allocated']

    def is_fully_allocated(self):
        """ Return True if this line item is fully allocated """

        if self.order.status == SalesOrderStatus.SHIPPED:
            return self.fulfilled_quantity() >= self.quantity

        return self.allocated_quantity() >= self.quantity

    def is_over_allocated(self):
        """ Return True if this line item is over allocated """
        return self.allocated_quantity() > self.quantity
Beispiel #16
0
    def get_pricing(self, quantity=1, currency=None):
        """Returns context with pricing information."""
        ctx = PartPricing.get_pricing(self, quantity, currency)
        part = self.get_part()
        default_currency = inventree_settings.currency_code_default()

        # Stock history
        if part.total_stock > 1:
            price_history = []
            stock = part.stock_entries(include_variants=False, in_stock=True).\
                order_by('purchase_order__issue_date').prefetch_related('purchase_order', 'supplier_part')

            for stock_item in stock:
                if None in [stock_item.purchase_price, stock_item.quantity]:
                    continue

                # convert purchase price to current currency - only one currency in the graph
                try:
                    price = convert_money(stock_item.purchase_price,
                                          default_currency)
                except MissingRate:
                    continue

                line = {'price': price.amount, 'qty': stock_item.quantity}
                # Supplier Part Name  # TODO use in graph
                if stock_item.supplier_part:
                    line['name'] = stock_item.supplier_part.pretty_name

                    if stock_item.supplier_part.unit_pricing and price:
                        line[
                            'price_diff'] = price.amount - stock_item.supplier_part.unit_pricing
                        line[
                            'price_part'] = stock_item.supplier_part.unit_pricing

                # set date for graph labels
                if stock_item.purchase_order and stock_item.purchase_order.issue_date:
                    line[
                        'date'] = stock_item.purchase_order.issue_date.isoformat(
                        )
                elif stock_item.tracking_info.count() > 0:
                    line['date'] = stock_item.tracking_info.first().date.date(
                    ).isoformat()
                else:
                    # Not enough information
                    continue

                price_history.append(line)

            ctx['price_history'] = price_history

        # BOM Information for Pie-Chart
        if part.has_bom:
            # get internal price setting
            use_internal = InvenTreeSetting.get_setting(
                'PART_BOM_USE_INTERNAL_PRICE', False)
            ctx_bom_parts = []
            # iterate over all bom-items
            for item in part.bom_items.all():
                ctx_item = {'name': str(item.sub_part)}
                price, qty = item.sub_part.get_price_range(
                    quantity, internal=use_internal), item.quantity

                price_min, price_max = 0, 0
                if price:  # check if price available
                    price_min = str((price[0] * qty) / quantity)
                    if len(set(price)) == 2:  # min and max-price present
                        price_max = str((price[1] * qty) / quantity)
                        ctx['bom_pie_max'] = True  # enable showing max prices in bom

                ctx_item['max_price'] = price_min
                ctx_item['min_price'] = price_max if price_max else price_min
                ctx_bom_parts.append(ctx_item)

            # add to global context
            ctx['bom_parts'] = ctx_bom_parts

        # Sale price history
        sale_items = PurchaseOrderLineItem.objects.filter(part__part=part).order_by('order__issue_date').\
            prefetch_related('order', ).all()

        if sale_items:
            sale_history = []

            for sale_item in sale_items:
                # check for not fully defined elements
                if None in [sale_item.purchase_price, sale_item.quantity]:
                    continue

                try:
                    price = convert_money(sale_item.purchase_price,
                                          default_currency)
                except MissingRate:
                    continue

                line = {
                    'price': price.amount if price else 0,
                    'qty': sale_item.quantity,
                }

                # set date for graph labels
                if sale_item.order.issue_date:
                    line['date'] = sale_item.order.issue_date.isoformat()
                elif sale_item.order.creation_date:
                    line['date'] = sale_item.order.creation_date.isoformat()
                else:
                    line['date'] = _('None')

                sale_history.append(line)

            ctx['sale_history'] = sale_history

        return ctx
Beispiel #17
0
 def price_converted(self):
     return convert_money(self.price, currency_code_default())