Merge pull request #1736 from matmair/money-update

Money updates
This commit is contained in:
Oliver 2021-07-02 08:34:20 +10:00 committed by GitHub
commit d243356324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 240 additions and 54 deletions

View File

@ -4,7 +4,6 @@ import logging
from django.apps import AppConfig from django.apps import AppConfig
from django.core.exceptions import AppRegistryNotReady from django.core.exceptions import AppRegistryNotReady
from django.conf import settings
from InvenTree.ready import isInTestMode, canAppAccessDatabase from InvenTree.ready import isInTestMode, canAppAccessDatabase
import InvenTree.tasks import InvenTree.tasks
@ -66,10 +65,11 @@ class InvenTreeConfig(AppConfig):
from djmoney.contrib.exchange.models import ExchangeBackend from djmoney.contrib.exchange.models import ExchangeBackend
from datetime import datetime, timedelta from datetime import datetime, timedelta
from InvenTree.tasks import update_exchange_rates from InvenTree.tasks import update_exchange_rates
from common.settings import currency_code_default
except AppRegistryNotReady: except AppRegistryNotReady:
pass pass
base_currency = settings.BASE_CURRENCY base_currency = currency_code_default()
update = False update = False

View File

@ -1,4 +1,4 @@
from django.conf import settings as inventree_settings from common.settings import currency_code_default, currency_codes
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
@ -22,8 +22,8 @@ class InvenTreeExchange(SimpleExchangeBackend):
return { return {
} }
def update_rates(self, base_currency=inventree_settings.BASE_CURRENCY): def update_rates(self, base_currency=currency_code_default()):
symbols = ','.join(inventree_settings.CURRENCIES) symbols = ','.join(currency_codes())
super().update_rates(base=base_currency, symbols=symbols) super().update_rates(base=base_currency, symbols=symbols)

View File

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import sys
from .validators import allowable_url_schemes from .validators import allowable_url_schemes
@ -13,8 +14,11 @@ from django.core import validators
from django import forms from django import forms
from decimal import Decimal from decimal import Decimal
from djmoney.models.fields import MoneyField as ModelMoneyField
from djmoney.forms.fields import MoneyField
import InvenTree.helpers import InvenTree.helpers
import common.settings
class InvenTreeURLFormField(FormURLField): class InvenTreeURLFormField(FormURLField):
@ -34,6 +38,42 @@ class InvenTreeURLField(models.URLField):
}) })
def money_kwargs():
""" returns the database settings for MoneyFields """
kwargs = {}
kwargs['currency_choices'] = common.settings.currency_code_mappings()
kwargs['default_currency'] = common.settings.currency_code_default
return kwargs
class InvenTreeModelMoneyField(ModelMoneyField):
""" custom MoneyField for clean migrations while using dynamic currency settings """
def __init__(self, **kwargs):
# detect if creating migration
if 'makemigrations' in sys.argv:
# remove currency information for a clean migration
kwargs['default_currency'] = ''
kwargs['currency_choices'] = []
else:
# set defaults
kwargs.update(money_kwargs())
super().__init__(**kwargs)
def formfield(self, **kwargs):
""" override form class to use own function """
kwargs['form_class'] = InvenTreeMoneyField
return super().formfield(**kwargs)
class InvenTreeMoneyField(MoneyField):
""" custom MoneyField for clean migrations while using dynamic currency settings """
def __init__(self, *args, **kwargs):
# override initial values with the real info from database
kwargs.update(money_kwargs())
super().__init__(*args, **kwargs)
class DatePickerFormField(forms.DateField): class DatePickerFormField(forms.DateField):
""" """
Custom date-picker field Custom date-picker field

View File

@ -522,10 +522,6 @@ for currency in CURRENCIES:
print(f"Currency code '{currency}' is not supported") print(f"Currency code '{currency}' is not supported")
sys.exit(1) sys.exit(1)
BASE_CURRENCY = get_setting(
'INVENTREE_BASE_CURRENCY',
CONFIG.get('base_currency', 'USD')
)
# Custom currency exchange backend # Custom currency exchange backend
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange' EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange'

View File

@ -170,7 +170,7 @@ def update_exchange_rates():
try: try:
from InvenTree.exchange import InvenTreeExchange from InvenTree.exchange import InvenTreeExchange
from djmoney.contrib.exchange.models import ExchangeBackend, Rate from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from django.conf import settings from common.settings import currency_code_default, currency_codes
except AppRegistryNotReady: except AppRegistryNotReady:
# Apps not yet loaded! # Apps not yet loaded!
logger.info("Could not perform 'update_exchange_rates' - App registry not ready") logger.info("Could not perform 'update_exchange_rates' - App registry not ready")
@ -192,14 +192,14 @@ def update_exchange_rates():
backend = InvenTreeExchange() backend = InvenTreeExchange()
print(f"Updating exchange rates from {backend.url}") print(f"Updating exchange rates from {backend.url}")
base = settings.BASE_CURRENCY base = currency_code_default()
print(f"Using base currency '{base}'") print(f"Using base currency '{base}'")
backend.update_rates(base_currency=base) backend.update_rates(base_currency=base)
# Remove any exchange rates which are not in the provided currencies # Remove any exchange rates which are not in the provided currencies
Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=settings.CURRENCIES).delete() Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete()
def send_email(subject, body, recipients, from_email=None): def send_email(subject, body, recipients, from_email=None):

View File

@ -5,8 +5,6 @@ from django.test import TestCase
import django.core.exceptions as django_exceptions import django.core.exceptions as django_exceptions
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.conf import settings
from djmoney.money import Money from djmoney.money import Money
from djmoney.contrib.exchange.models import Rate, convert_money from djmoney.contrib.exchange.models import Rate, convert_money
from djmoney.contrib.exchange.exceptions import MissingRate from djmoney.contrib.exchange.exceptions import MissingRate
@ -22,6 +20,7 @@ from decimal import Decimal
import InvenTree.tasks import InvenTree.tasks
from stock.models import StockLocation from stock.models import StockLocation
from common.settings import currency_codes
class ValidatorTest(TestCase): class ValidatorTest(TestCase):
@ -337,13 +336,11 @@ class CurrencyTests(TestCase):
with self.assertRaises(MissingRate): with self.assertRaises(MissingRate):
convert_money(Money(100, 'AUD'), 'USD') convert_money(Money(100, 'AUD'), 'USD')
currencies = settings.CURRENCIES
InvenTree.tasks.update_exchange_rates() InvenTree.tasks.update_exchange_rates()
rates = Rate.objects.all() rates = Rate.objects.all()
self.assertEqual(rates.count(), len(currencies)) self.assertEqual(rates.count(), len(currency_codes()))
# Now that we have some exchange rate information, we can perform conversions # Now that we have some exchange rate information, we can perform conversions

View File

@ -12,7 +12,6 @@ from django.utils.translation import gettext_lazy as _
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
@ -21,6 +20,7 @@ from django.views.generic import ListView, DetailView, CreateView, FormView, Del
from django.views.generic.base import RedirectView, TemplateView from django.views.generic.base import RedirectView, TemplateView
from djmoney.contrib.exchange.models import ExchangeBackend, Rate from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from common.settings import currency_code_default, currency_codes
from part.models import Part, PartCategory from part.models import Part, PartCategory
from stock.models import StockLocation, StockItem from stock.models import StockLocation, StockItem
@ -820,8 +820,8 @@ class CurrencySettingsView(TemplateView):
ctx = super().get_context_data(**kwargs).copy() ctx = super().get_context_data(**kwargs).copy()
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key') ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
ctx["base_currency"] = settings.BASE_CURRENCY ctx["base_currency"] = currency_code_default()
ctx["currencies"] = settings.CURRENCIES ctx["currencies"] = currency_codes
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange") ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-07-01 15:39
from django.db import migrations
from common.models import InvenTreeSetting
from InvenTree.settings import get_setting, CONFIG
def set_default_currency(apps, schema_editor):
""" migrate the currency setting from config.yml to db """
# get value from settings-file
base_currency = get_setting('INVENTREE_BASE_CURRENCY', CONFIG.get('base_currency', 'USD'))
# write to database
InvenTreeSetting.set_setting('INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True)
class Migration(migrations.Migration):
dependencies = [
('common', '0009_delete_currency'),
]
operations = [
migrations.RunPython(set_default_currency),
]

View File

@ -14,11 +14,11 @@ from django.db import models, transaction
from django.db.utils import IntegrityError, OperationalError from django.db.utils import IntegrityError, OperationalError
from django.conf import settings from django.conf import settings
from djmoney.models.fields import MoneyField from djmoney.settings import CURRENCY_CHOICES
from djmoney.contrib.exchange.models import convert_money from djmoney.contrib.exchange.models import convert_money
from djmoney.contrib.exchange.exceptions import MissingRate from djmoney.contrib.exchange.exceptions import MissingRate
from common.settings import currency_code_default import common.settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, URLValidator from django.core.validators import MinValueValidator, URLValidator
@ -81,6 +81,13 @@ class InvenTreeSetting(models.Model):
'default': '', 'default': '',
}, },
'INVENTREE_DEFAULT_CURRENCY': {
'name': _('Default Currency'),
'description': _('Default currency'),
'default': 'USD',
'choices': CURRENCY_CHOICES,
},
'INVENTREE_DOWNLOAD_FROM_URL': { 'INVENTREE_DOWNLOAD_FROM_URL': {
'name': _('Download from URL'), 'name': _('Download from URL'),
'description': _('Allow download of remote images and files from external URL'), 'description': _('Allow download of remote images and files from external URL'),
@ -735,10 +742,9 @@ class PriceBreak(models.Model):
help_text=_('Price break quantity'), help_text=_('Price break quantity'),
) )
price = MoneyField( price = InvenTree.fields.InvenTreeModelMoneyField(
max_digits=19, max_digits=19,
decimal_places=4, decimal_places=4,
default_currency=currency_code_default(),
null=True, null=True,
verbose_name=_('Price'), verbose_name=_('Price'),
help_text=_('Unit price at specified quantity'), help_text=_('Unit price at specified quantity'),
@ -791,7 +797,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
if currency is None: if currency is None:
# Default currency selection # Default currency selection
currency = currency_code_default() currency = common.settings.currency_code_default()
pb_min = None pb_min = None
for pb in price_breaks: for pb in price_breaks:

View File

@ -6,9 +6,9 @@ User-configurable settings for the common app
from __future__ import unicode_literals from __future__ import unicode_literals
from moneyed import CURRENCIES from moneyed import CURRENCIES
from django.conf import settings
import common.models import common.models
from django.conf import settings
def currency_code_default(): def currency_code_default():
@ -16,7 +16,7 @@ def currency_code_default():
Returns the default currency code (or USD if not specified) Returns the default currency code (or USD if not specified)
""" """
code = settings.BASE_CURRENCY code = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
if code not in CURRENCIES: if code not in CURRENCIES:
code = 'USD' code = 'USD'
@ -24,6 +24,20 @@ def currency_code_default():
return code return code
def currency_code_mappings():
"""
Returns the current currency choices
"""
return [(a, a) for a in settings.CURRENCIES]
def currency_codes():
"""
Returns the current currency codes
"""
return [a for a in settings.CURRENCIES]
def stock_expiry_enabled(): def stock_expiry_enabled():
""" """
Returns True if the stock expiry feature is enabled Returns True if the stock expiry feature is enabled

View File

@ -6,13 +6,12 @@ Django Forms for interacting with Company app
from __future__ import unicode_literals from __future__ import unicode_literals
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django.forms import django.forms
import djmoney.settings import djmoney.settings
from djmoney.forms.fields import MoneyField
from common.settings import currency_code_default from common.settings import currency_code_default
@ -129,9 +128,8 @@ class EditSupplierPartForm(HelperForm):
'note': 'fa-pencil-alt', 'note': 'fa-pencil-alt',
} }
single_pricing = MoneyField( single_pricing = InvenTreeMoneyField(
label=_('Single Price'), label=_('Single Price'),
default_currency=currency_code_default(),
help_text=_('Single quantity price'), help_text=_('Single quantity price'),
decimal_places=4, decimal_places=4,
max_digits=19, max_digits=19,

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.4 on 2021-07-01 05:09
import InvenTree.fields
from django.db import migrations
import djmoney.models.fields
class Migration(migrations.Migration):
dependencies = [
('company', '0038_manufacturerpartparameter'),
]
operations = [
migrations.AlterField(
model_name='supplierpricebreak',
name='price',
field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
),
migrations.AlterField(
model_name='supplierpricebreak',
name='price_currency',
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
),
]

View File

@ -10,15 +10,12 @@ from django.utils.translation import ugettext_lazy as _
from mptt.fields import TreeNodeChoiceField from mptt.fields import TreeNodeChoiceField
from djmoney.forms.fields import MoneyField
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
from InvenTree.fields import DatePickerFormField from InvenTree.fields import DatePickerFormField
from InvenTree.helpers import clean_decimal from InvenTree.helpers import clean_decimal
from common.models import InvenTreeSetting
from common.forms import MatchItemForm from common.forms import MatchItemForm
import part.models import part.models
@ -321,9 +318,8 @@ class OrderMatchItemForm(MatchItemForm):
) )
# set price field # set price field
elif 'price' in col_guess.lower(): elif 'price' in col_guess.lower():
return MoneyField( return InvenTreeMoneyField(
label=_(col_guess), label=_(col_guess),
default_currency=InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY'),
decimal_places=5, decimal_places=5,
max_digits=19, max_digits=19,
required=False, required=False,

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.4 on 2021-07-01 05:09
import InvenTree.fields
from django.db import migrations
import djmoney.models.fields
class Migration(migrations.Migration):
dependencies = [
('order', '0046_purchaseorderlineitem_destination'),
]
operations = [
migrations.AlterField(
model_name='purchaseorderlineitem',
name='purchase_price',
field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
),
migrations.AlterField(
model_name='purchaseorderlineitem',
name='purchase_price_currency',
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
),
migrations.AlterField(
model_name='salesorderlineitem',
name='sale_price',
field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'),
),
migrations.AlterField(
model_name='salesorderlineitem',
name='sale_price_currency',
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
),
]

View File

@ -17,19 +17,15 @@ from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.settings import currency_code_default
from markdownx.models import MarkdownxField from markdownx.models import MarkdownxField
from mptt.models import TreeForeignKey from mptt.models import TreeForeignKey
from djmoney.models.fields import MoneyField
from users import models as UserModels from users import models as UserModels
from part import models as PartModels from part import models as PartModels
from stock import models as stock_models from stock import models as stock_models
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from InvenTree.fields import RoundingDecimalField from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField
from InvenTree.helpers import decimal2string, increment, getSetting from InvenTree.helpers import decimal2string, increment, getSetting
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode
from InvenTree.models import InvenTreeAttachment from InvenTree.models import InvenTreeAttachment
@ -664,10 +660,9 @@ class PurchaseOrderLineItem(OrderLineItem):
received = models.DecimalField(decimal_places=5, max_digits=15, default=0, verbose_name=_('Received'), help_text=_('Number of items received')) received = models.DecimalField(decimal_places=5, max_digits=15, default=0, verbose_name=_('Received'), help_text=_('Number of items received'))
purchase_price = MoneyField( purchase_price = InvenTreeModelMoneyField(
max_digits=19, max_digits=19,
decimal_places=4, decimal_places=4,
default_currency=currency_code_default(),
null=True, blank=True, null=True, blank=True,
verbose_name=_('Purchase Price'), verbose_name=_('Purchase Price'),
help_text=_('Unit purchase price'), help_text=_('Unit purchase price'),
@ -716,10 +711,9 @@ class SalesOrderLineItem(OrderLineItem):
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}) 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( sale_price = InvenTreeModelMoneyField(
max_digits=19, max_digits=19,
decimal_places=4, decimal_places=4,
default_currency=currency_code_default(),
null=True, blank=True, null=True, blank=True,
verbose_name=_('Sale Price'), verbose_name=_('Sale Price'),
help_text=_('Unit sale price'), help_text=_('Unit sale price'),

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.4 on 2021-07-01 05:09
import InvenTree.fields
from django.db import migrations
import djmoney.models.fields
class Migration(migrations.Migration):
dependencies = [
('part', '0068_part_unique_part'),
]
operations = [
migrations.AlterField(
model_name='partinternalpricebreak',
name='price',
field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
),
migrations.AlterField(
model_name='partinternalpricebreak',
name='price_currency',
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
),
migrations.AlterField(
model_name='partsellpricebreak',
name='price',
field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
),
migrations.AlterField(
model_name='partsellpricebreak',
name='price_currency',
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
),
]

View File

@ -2949,7 +2949,7 @@ class PartSalePriceBreakCreate(AjaxCreateView):
initials['part'] = self.get_part() initials['part'] = self.get_part()
default_currency = settings.BASE_CURRENCY default_currency = inventree_settings.currency_code_default()
currency = CURRENCIES.get(default_currency, None) currency = CURRENCIES.get(default_currency, None)
if currency is not None: if currency is not None:

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.4 on 2021-07-01 05:09
import InvenTree.fields
from django.db import migrations
import djmoney.models.fields
class Migration(migrations.Migration):
dependencies = [
('stock', '0064_auto_20210621_1724'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='purchase_price',
field=InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=4, default_currency='', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
),
migrations.AlterField(
model_name='stockitem',
name='purchase_price_currency',
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
),
]

View File

@ -20,14 +20,10 @@ from django.contrib.auth.models import User
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from common.settings import currency_code_default
from markdownx.models import MarkdownxField from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from djmoney.models.fields import MoneyField
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from datetime import datetime, timedelta from datetime import datetime, timedelta
from InvenTree import helpers from InvenTree import helpers
@ -38,7 +34,7 @@ import label.models
from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.status_codes import StockStatus, StockHistoryCode
from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
from users.models import Owner from users.models import Owner
@ -533,10 +529,9 @@ class StockItem(MPTTModel):
help_text=_('Stock Item Notes') help_text=_('Stock Item Notes')
) )
purchase_price = MoneyField( purchase_price = InvenTreeModelMoneyField(
max_digits=19, max_digits=19,
decimal_places=4, decimal_places=4,
default_currency=currency_code_default(),
blank=True, blank=True,
null=True, null=True,
verbose_name=_('Purchase Price'), verbose_name=_('Purchase Price'),

View File

@ -12,6 +12,13 @@
{% block settings %} {% block settings %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %}
</tbody>
</table>
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tbody> <tbody>
<tr> <tr>