From 23db7a89a9ef6958169b6d3ba9833b8f640d2482 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Mon, 19 Jul 2021 14:20:54 -0400
Subject: [PATCH 1/6] Updated PO upload template, moved call to button,
 improved cleaned_decimal method to handle comma separator

---
 InvenTree/InvenTree/helpers.py                | 29 ++++--
 .../order/order_wizard/po_upload.html         | 92 ++++++++++++-------
 .../order/templates/order/po_navbar.html      |  8 --
 .../order/purchase_order_detail.html          |  3 +
 InvenTree/order/views.py                      | 11 +--
 5 files changed, 85 insertions(+), 58 deletions(-)

diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 330bd2bb68..0b091efd02 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -631,13 +631,30 @@ def clean_decimal(number):
     """ Clean-up decimal value """
 
     # Check if empty
-    if number is None or number == '':
+    if number is None or number == '' or number == 0:
         return Decimal(0)
 
-    # Check if decimal type
-    try:
-        clean_number = Decimal(number)
-    except InvalidOperation:
-        clean_number = number
+    # Convert to string and remove spaces
+    number = str(number).replace(' ', '')
+
+    # Guess what type of decimal and thousands separators are used
+    count_comma = number.count(',')
+    count_point = number.count('.')
+
+    if count_comma == 1:
+        # Comma is used as decimal separator
+        if count_point > 0:
+            # Points are used as thousands separators: remove them
+            number = number.replace('.', '')
+        # Replace decimal separator with point
+        number = number.replace(',', '.')
+    elif count_point == 1:
+        # Point is used as decimal separator
+        if count_comma > 0:
+            # Commas are used as thousands separators: remove them
+            number = number.replace(',', '')
+
+    # Convert to Decimal type
+    clean_number = Decimal(number)
 
     return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize()
diff --git a/InvenTree/order/templates/order/order_wizard/po_upload.html b/InvenTree/order/templates/order/order_wizard/po_upload.html
index 4357bce2c6..7c2dea1af9 100644
--- a/InvenTree/order/templates/order/order_wizard/po_upload.html
+++ b/InvenTree/order/templates/order/order_wizard/po_upload.html
@@ -1,50 +1,74 @@
-{% extends "order/order_base.html" %}
+{% extends "order/purchase_order_detail.html" %}
 {% load inventree_extras %}
 {% load i18n %}
 {% load static %}
 
-{% block heading %}
-{% trans "Upload File for Purchase Order" %}
-{{ wizard.form.media }}
+{% block menubar %}
+<ul class='list-group'>
+    <li class='list-group-item'>
+        <a href='#' id='po-menu-toggle'>
+            <span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
+        </a>
+    </li>
+    <li class='list-group-item' title='{% trans "Return To Order" %}'>
+        <a href='{% url "po-detail" order.id %}' id='select-upload-file' class='nav-toggle'>
+            <span class='fas fa-undo side-icon'></span>
+            {% trans "Return To Order" %}
+        </a>
+    </li>
+</ul>
 {% endblock %}
 
-{% block details %}
-{% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
+{% block page_content %}
 
-<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
-{% if description %}- {{ description }}{% endif %}</p>
+<div class='panel panel-default panel-inventree' id='panel-upload-file'>
+    <div class='panel-heading'>
+        {% block heading %}
+        <h4>{% trans "Upload File for Purchase Order" %}</h4>
+        {{ wizard.form.media }}
+        {% endblock %}
+    </div>
+    <div class='panel-content'>
+        {% block details %}
+        {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
 
-{% block form_alert %}
-{% endblock form_alert %}
+        <p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
+        {% if description %}- {{ description }}{% endif %}</p>
 
-<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
-{% csrf_token %}
-{% load crispy_forms_tags %}
+        {% block form_alert %}
+        {% endblock form_alert %}
 
-{% block form_buttons_top %}
-{% endblock form_buttons_top %}
+        <form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
+        {% csrf_token %}
+        {% load crispy_forms_tags %}
 
-<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
-{{ wizard.management_form }}
-{% block form_content %}
-{% crispy wizard.form %}
-{% endblock form_content %}
-</table>
+        {% block form_buttons_top %}
+        {% endblock form_buttons_top %}
 
-{% block form_buttons_bottom %}
-{% if wizard.steps.prev %}
-<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
-{% endif %}
-<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
-</form>
-{% endblock form_buttons_bottom %}
+        <table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
+        {{ wizard.management_form }}
+        {% block form_content %}
+        {% crispy wizard.form %}
+        {% endblock form_content %}
+        </table>
 
-{% else %}
-<div class='alert alert-danger alert-block' role='alert'>
-    {% trans "Order is already processed. Files cannot be uploaded." %}
-</div>
-{% endif %}
-{% endblock details %}
+        {% block form_buttons_bottom %}
+        {% if wizard.steps.prev %}
+        <button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
+        {% endif %}
+        <button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
+        </form>
+        {% endblock form_buttons_bottom %}
+
+        {% else %}
+        <div class='alert alert-danger alert-block' role='alert'>
+            {% trans "Order is already processed. Files cannot be uploaded." %}
+        </div>
+        {% endif %}
+        {% endblock details %}
+    </div>
+
+{% endblock %}
 
 {% block js_ready %}
 {{ block.super }}
diff --git a/InvenTree/order/templates/order/po_navbar.html b/InvenTree/order/templates/order/po_navbar.html
index 9f7967810d..bddcce4ba3 100644
--- a/InvenTree/order/templates/order/po_navbar.html
+++ b/InvenTree/order/templates/order/po_navbar.html
@@ -15,14 +15,6 @@
             {% trans "Order Items" %}
         </a>
     </li>
-    {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %}
-    <li class='list-group-item' title='{% trans "Upload File" %}'>
-        <a href='{% url "po-upload" order.id %}'>
-            <span class='fas fa-file-upload side-icon'></span>
-            {% trans "Upload File" %}
-        </a>
-    </li>
-    {% endif %}
     <li class='list-group-item' title='{% trans "Received Stock Items" %}'>
         <a href='#' id='select-received-items' class='nav-toggle'>
             <span class='fas fa-sign-in-alt side-icon'></span>
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index 193fd75daa..b05bfa7cc2 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -22,6 +22,9 @@
             <button type='button' class='btn btn-primary' id='new-po-line'>
                 <span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}
             </button>
+            <a class='btn btn-primary' href='{% url "po-upload" order.id %}' role='button'>
+                <span class='fas fa-file-upload side-icon'></span> {% trans "Upload File" %}
+            </a>
             {% endif %}
         </div>
         
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index be1107f17b..a109b036e0 100644
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -393,16 +393,7 @@ class PurchaseOrderUpload(FileManagementFormView):
                 p_val = row['data'][p_idx]['cell']
 
                 if p_val:
-                    # Delete commas
-                    p_val = p_val.replace(',', '')
-
-                    try:
-                        # Attempt to extract a valid decimal value from the field
-                        purchase_price = Decimal(p_val)
-                        # Store the 'purchase_price' value
-                        row['purchase_price'] = purchase_price
-                    except (ValueError, InvalidOperation):
-                        pass
+                    row['purchase_price'] = p_val
 
             # Check if there is a column corresponding to "reference"
             if r_idx >= 0:

From 3ab058e84b460dc98a4aa4019595b7d79ea28cee Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Mon, 19 Jul 2021 14:49:55 -0400
Subject: [PATCH 2/6] Fixed default currency selection

---
 InvenTree/InvenTree/fields.py  | 3 ++-
 InvenTree/InvenTree/helpers.py | 2 +-
 InvenTree/common/models.py     | 5 ++---
 InvenTree/common/settings.py   | 8 ++++----
 4 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py
index 462d2b0e0e..65664eee6b 100644
--- a/InvenTree/InvenTree/fields.py
+++ b/InvenTree/InvenTree/fields.py
@@ -44,7 +44,7 @@ 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
+    kwargs['default_currency'] = common.settings.currency_code_default()
     return kwargs
 
 
@@ -86,6 +86,7 @@ class InvenTreeMoneyField(MoneyField):
     def __init__(self, *args, **kwargs):
         # override initial values with the real info from database
         kwargs.update(money_kwargs())
+        print(kwargs)
         super().__init__(*args, **kwargs)
 
 
diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 0b091efd02..374e4e5a59 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -8,7 +8,7 @@ import json
 import os.path
 from PIL import Image
 
-from decimal import Decimal, InvalidOperation
+from decimal import Decimal
 
 from wsgiref.util import FileWrapper
 from django.http import StreamingHttpResponse
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 3d18d56880..dcdeea5292 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -18,8 +18,6 @@ from djmoney.settings import CURRENCY_CHOICES
 from djmoney.contrib.exchange.models import convert_money
 from djmoney.contrib.exchange.exceptions import MissingRate
 
-import common.settings
-
 from django.utils.translation import ugettext_lazy as _
 from django.core.validators import MinValueValidator, URLValidator
 from django.core.exceptions import ValidationError
@@ -781,6 +779,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
     - 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()
@@ -804,7 +803,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
 
     if currency is None:
         # Default currency selection
-        currency = common.settings.currency_code_default()
+        currency = currency_code_default()
 
     pb_min = None
     for pb in price_breaks:
diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py
index b34cf0b785..67dd751154 100644
--- a/InvenTree/common/settings.py
+++ b/InvenTree/common/settings.py
@@ -8,15 +8,14 @@ from __future__ import unicode_literals
 from moneyed import CURRENCIES
 from django.conf import settings
 
-import common.models
-
 
 def currency_code_default():
     """
     Returns the default currency code (or USD if not specified)
     """
+    from common.models import InvenTreeSetting
 
-    code = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
+    code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
 
     if code not in CURRENCIES:
         code = 'USD'
@@ -42,5 +41,6 @@ def stock_expiry_enabled():
     """
     Returns True if the stock expiry feature is enabled
     """
+    from common.models import InvenTreeSetting
 
-    return common.models.InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY')
+    return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY')

From c1db4c7b3daf16a7c866c0a8af30ea01f323aa49 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Mon, 19 Jul 2021 15:14:08 -0400
Subject: [PATCH 3/6] Try to catch encoding error, fixed CI

---
 InvenTree/InvenTree/fields.py |  7 ++++---
 InvenTree/common/files.py     | 25 ++++++++++++++-----------
 2 files changed, 18 insertions(+), 14 deletions(-)

diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py
index 65664eee6b..81c4f7ba0b 100644
--- a/InvenTree/InvenTree/fields.py
+++ b/InvenTree/InvenTree/fields.py
@@ -20,7 +20,6 @@ from djmoney.forms.fields import MoneyField
 from djmoney.models.validators import MinMoneyValidator
 
 import InvenTree.helpers
-import common.settings
 
 
 class InvenTreeURLFormField(FormURLField):
@@ -42,9 +41,11 @@ class InvenTreeURLField(models.URLField):
 
 def money_kwargs():
     """ returns the database settings for MoneyFields """
+    from common.settings import currency_code_mappings, currency_code_default
+
     kwargs = {}
-    kwargs['currency_choices'] = common.settings.currency_code_mappings()
-    kwargs['default_currency'] = common.settings.currency_code_default()
+    kwargs['currency_choices'] = currency_code_mappings()
+    kwargs['default_currency'] = currency_code_default()
     return kwargs
 
 
diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py
index 45b6c80050..39c97dca6c 100644
--- a/InvenTree/common/files.py
+++ b/InvenTree/common/files.py
@@ -53,17 +53,20 @@ class FileManager:
 
         ext = os.path.splitext(file.name)[-1].lower().replace('.', '')
 
-        if ext in ['csv', 'tsv', ]:
-            # These file formats need string decoding
-            raw_data = file.read().decode('utf-8')
-            # Reset stream position to beginning of file
-            file.seek(0)
-        elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
-            raw_data = file.read()
-            # Reset stream position to beginning of file
-            file.seek(0)
-        else:
-            raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
+        try:
+            if ext in ['csv', 'tsv', ]:
+                # These file formats need string decoding
+                raw_data = file.read().decode('utf-8')
+                # Reset stream position to beginning of file
+                file.seek(0)
+            elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
+                raw_data = file.read()
+                # Reset stream position to beginning of file
+                file.seek(0)
+            else:
+                raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
+        except UnicodeEncodeError:
+            raise ValidationError(_('Error reading file (invalid encoding)'))
 
         try:
             cleaned_data = tablib.Dataset().load(raw_data, format=ext)

From 9acd57f8e0ba5ea1996ec74dc6619e2ce9809175 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Mon, 19 Jul 2021 15:29:04 -0400
Subject: [PATCH 4/6] CI was not completely fixed

---
 InvenTree/InvenTree/fields.py | 1 -
 InvenTree/common/settings.py  | 6 +++++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py
index 81c4f7ba0b..71b31c55a8 100644
--- a/InvenTree/InvenTree/fields.py
+++ b/InvenTree/InvenTree/fields.py
@@ -87,7 +87,6 @@ class InvenTreeMoneyField(MoneyField):
     def __init__(self, *args, **kwargs):
         # override initial values with the real info from database
         kwargs.update(money_kwargs())
-        print(kwargs)
         super().__init__(*args, **kwargs)
 
 
diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py
index 67dd751154..592277657f 100644
--- a/InvenTree/common/settings.py
+++ b/InvenTree/common/settings.py
@@ -15,7 +15,11 @@ def currency_code_default():
     """
     from common.models import InvenTreeSetting
 
-    code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
+    try:
+        code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
+    except django.db.utils.ProgrammingError:
+        # database is not initialized yet
+        code = ''
 
     if code not in CURRENCIES:
         code = 'USD'

From 53f2aa107a6ec44596c96ad156eedfa3323b2146 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Mon, 19 Jul 2021 15:51:04 -0400
Subject: [PATCH 5/6] Umm watch out for the true fix!

---
 InvenTree/common/settings.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py
index 592277657f..80f80b886f 100644
--- a/InvenTree/common/settings.py
+++ b/InvenTree/common/settings.py
@@ -13,11 +13,12 @@ def currency_code_default():
     """
     Returns the default currency code (or USD if not specified)
     """
+    from django.db.utils import ProgrammingError
     from common.models import InvenTreeSetting
 
     try:
         code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
-    except django.db.utils.ProgrammingError:
+    except ProgrammingError:
         # database is not initialized yet
         code = ''
 

From 456710c5ce06ea5f5512b4732449c4a8d6a1ff64 Mon Sep 17 00:00:00 2001
From: eeintech <eeintech@eeinte.ch>
Date: Mon, 19 Jul 2021 15:57:51 -0400
Subject: [PATCH 6/6] clean_decimal should also check if the string can be
 converted to Decimal type

---
 InvenTree/InvenTree/helpers.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 374e4e5a59..628fd2e646 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -8,7 +8,7 @@ import json
 import os.path
 from PIL import Image
 
-from decimal import Decimal
+from decimal import Decimal, InvalidOperation
 
 from wsgiref.util import FileWrapper
 from django.http import StreamingHttpResponse
@@ -655,6 +655,10 @@ def clean_decimal(number):
             number = number.replace(',', '')
 
     # Convert to Decimal type
-    clean_number = Decimal(number)
+    try:
+        clean_number = Decimal(number)
+    except InvalidOperation:
+        # Number cannot be converted to Decimal (eg. a string containing letters)
+        return Decimal(0)
 
     return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize()