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
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()
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)
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}")
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}")
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
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
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
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()
def default_currency(*args, **kwargs): """ Returns the default currency code """ return currency_code_default()
def price_converted_currency(self): return currency_code_default()
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)
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
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
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
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
def price_converted(self): return convert_money(self.price, currency_code_default())