feat: suggest product default locations when parsing purchase orders from PDF
This commit is contained in:
parent
1d212194ad
commit
03fa3adaf6
7 changed files with 347 additions and 9 deletions
|
|
@ -337,4 +337,57 @@ class MobilePurchaseOrderFromPdfTests(TestCase):
|
|||
self.assertEqual(po.documents.first().name, "Photo_mobile_1.jpg")
|
||||
self.assertFalse(os.path.exists(tmp_p))
|
||||
|
||||
def test_mobile_purchase_order_from_pdf_suggests_product_default_location(self):
|
||||
from unittest.mock import patch
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from stock.models import WarehouseLocation
|
||||
|
||||
loc_mob_specific = WarehouseLocation.objects.create(
|
||||
warehouse=self.warehouse,
|
||||
name="Zone Rayon Spécifique Mob",
|
||||
code="LOC-MOB-SPECIFIC",
|
||||
is_default=False,
|
||||
)
|
||||
self.product.authorized_locations.add(loc_mob_specific)
|
||||
|
||||
url = reverse("mobile:purchase_order_from_pdf_mobile")
|
||||
file1 = SimpleUploadedFile("photo_mob.jpg", b"\xff\xd8\xff\xe0mockjpg", content_type="image/jpeg")
|
||||
|
||||
mock_res = {
|
||||
'is_ocr': True,
|
||||
'is_image': True,
|
||||
'is_multi_image': False,
|
||||
'image_count': 1,
|
||||
'total_items': 1,
|
||||
'metadata': {'supplier_name': 'Fournisseur Mobile Test', 'quote_number': 'MOB-OF-99'},
|
||||
'matched_supplier': self.supplier,
|
||||
'items': [{
|
||||
'reference': 'REF-1',
|
||||
'name': 'Mètre Ruban Test Mobile',
|
||||
'quantity': 2,
|
||||
'unit': 'piece',
|
||||
'raw_unit': 'Piece',
|
||||
'gross_unit_price': 15.50,
|
||||
'discount': '0%',
|
||||
'unit_price': 15.50,
|
||||
'total_amount': 31.00,
|
||||
'vat_rate': 21.0,
|
||||
'matched_product_id': self.product.id,
|
||||
'matched_product_name': self.product.name,
|
||||
'matched_product_code': self.product.code,
|
||||
}],
|
||||
}
|
||||
|
||||
with patch("stock.pdf_parser.PdfQuoteParser.parse", return_value=mock_res):
|
||||
response = self.client.post(url, {
|
||||
"action": "parse_pdf",
|
||||
"warehouse": self.warehouse.pk,
|
||||
"pdf_file": [file1],
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
items = response.context["parse_result"]["items"]
|
||||
self.assertEqual(items[0]["suggested_location_id"], loc_mob_specific.id)
|
||||
self.assertIn(f'value="{loc_mob_specific.id}" selected', response.content.decode())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1270,6 +1270,12 @@ def purchase_order_from_pdf_mobile(request):
|
|||
|
||||
location_id = request.POST.get(f'item_{i}_location')
|
||||
location = WarehouseLocation.objects.filter(warehouse=warehouse, pk=location_id).first()
|
||||
if not location:
|
||||
from stock.models import get_product_default_locations_map
|
||||
prod_locs = get_product_default_locations_map(warehouse, [product.id])
|
||||
def_loc_id = prod_locs.get(product.id)
|
||||
if def_loc_id:
|
||||
location = WarehouseLocation.objects.filter(warehouse=warehouse, pk=def_loc_id).first()
|
||||
if not location:
|
||||
location = warehouse.locations.filter(is_default=True).first() or warehouse.locations.first()
|
||||
|
||||
|
|
@ -1332,9 +1338,32 @@ def purchase_order_from_pdf_mobile(request):
|
|||
|
||||
locations = []
|
||||
default_location = None
|
||||
product_locations_map = {}
|
||||
product_locations_json = "{}"
|
||||
if warehouse:
|
||||
from stock.models import get_product_default_locations_map
|
||||
locations = warehouse.locations.all().order_by('name')
|
||||
default_location = warehouse.locations.filter(is_default=True).first() or locations.first()
|
||||
product_locations_map = get_product_default_locations_map(warehouse)
|
||||
product_locations_json = json.dumps(product_locations_map)
|
||||
|
||||
if parse_result and 'items' in parse_result:
|
||||
for item in parse_result['items']:
|
||||
matched_pid = item.get('matched_product_id')
|
||||
if not matched_pid and item.get('matched_product'):
|
||||
matched_obj = item['matched_product']
|
||||
matched_pid = getattr(matched_obj, 'id', None)
|
||||
item['matched_product_id'] = matched_pid
|
||||
if 'matched_product_name' not in item:
|
||||
item['matched_product_name'] = getattr(matched_obj, 'name', '')
|
||||
if 'matched_product_code' not in item:
|
||||
item['matched_product_code'] = getattr(matched_obj, 'code', '')
|
||||
if matched_pid and matched_pid in product_locations_map:
|
||||
item['suggested_location_id'] = product_locations_map[matched_pid]
|
||||
elif default_location:
|
||||
item['suggested_location_id'] = default_location.id
|
||||
else:
|
||||
item['suggested_location_id'] = None
|
||||
|
||||
return render(request, 'stock/mobile/purchase_order_from_pdf_mobile.html', {
|
||||
'step': step,
|
||||
|
|
@ -1342,6 +1371,8 @@ def purchase_order_from_pdf_mobile(request):
|
|||
'selected_warehouse': warehouse,
|
||||
'locations': locations,
|
||||
'default_location': default_location,
|
||||
'product_locations_map': product_locations_map,
|
||||
'product_locations_json': product_locations_json,
|
||||
'suppliers': suppliers,
|
||||
'categories': categories,
|
||||
'products': products,
|
||||
|
|
|
|||
|
|
@ -367,6 +367,18 @@ class Product(models.Model):
|
|||
product=self, purchase_order__status__in=["pending", "ordered"]
|
||||
).exists()
|
||||
|
||||
def get_default_location(self, warehouse):
|
||||
"""
|
||||
Retourne l'emplacement de stockage par défaut pour ce produit dans un entrepôt donné.
|
||||
"""
|
||||
if not warehouse or not self.pk:
|
||||
return None
|
||||
loc_map = get_product_default_locations_map(warehouse, product_ids=[self.pk])
|
||||
loc_id = loc_map.get(self.pk)
|
||||
if loc_id:
|
||||
return warehouse.locations.filter(pk=loc_id).first()
|
||||
return None
|
||||
|
||||
|
||||
def notify_supervisors_pending_product(product, created_by=None):
|
||||
"""Notifie les superviseurs / admins qu'un nouveau produit attend validation."""
|
||||
|
|
@ -441,6 +453,97 @@ class Stock(models.Model):
|
|||
self.product.update_total_stock()
|
||||
|
||||
|
||||
def get_product_default_locations_map(warehouse, product_ids=None):
|
||||
"""
|
||||
Retourne un dictionnaire {product_id: location_id} pour les produits donnés
|
||||
(ou tous les produits actifs si non spécifié) dans l'entrepôt spécifié.
|
||||
|
||||
Règles de détermination de l'emplacement par défaut :
|
||||
1. Si le produit possède des emplacements autorisés dans cet entrepôt :
|
||||
a. L'emplacement autorisé marqué is_default=True pour l'entrepôt s'il existe.
|
||||
b. L'emplacement autorisé ayant le plus de stock (> 0).
|
||||
c. Le premier emplacement autorisé (ordre alphabétique).
|
||||
2. Si le produit n'a pas d'emplacement autorisé restreint dans cet entrepôt :
|
||||
a. L'emplacement de cet entrepôt où le produit a déjà du stock (> 0, quantité max).
|
||||
b. L'emplacement par défaut de l'entrepôt (is_default=True).
|
||||
c. Le premier emplacement de l'entrepôt.
|
||||
"""
|
||||
if not warehouse:
|
||||
return {}
|
||||
|
||||
locations = list(warehouse.locations.all().order_by('name'))
|
||||
if not locations:
|
||||
return {}
|
||||
|
||||
wh_default_loc = next((l for l in locations if l.is_default), locations[0])
|
||||
wh_default_loc_id = wh_default_loc.id if wh_default_loc else None
|
||||
wh_loc_ids = {l.id for l in locations}
|
||||
loc_is_default = {l.id: l.is_default for l in locations}
|
||||
|
||||
# 1. Emplacements autorisés par produit pour cet entrepôt
|
||||
auth_m2m = Product.authorized_locations.through.objects.filter(
|
||||
warehouselocation_id__in=wh_loc_ids
|
||||
)
|
||||
if product_ids is not None:
|
||||
auth_m2m = auth_m2m.filter(product_id__in=product_ids)
|
||||
|
||||
product_auth_locs = {}
|
||||
for entry in auth_m2m.values('product_id', 'warehouselocation_id'):
|
||||
p_id = entry['product_id']
|
||||
l_id = entry['warehouselocation_id']
|
||||
product_auth_locs.setdefault(p_id, []).append(l_id)
|
||||
|
||||
# 2. Stocks existants dans cet entrepôt
|
||||
stock_qs = Stock.objects.filter(
|
||||
location__warehouse=warehouse,
|
||||
quantity__gt=0
|
||||
)
|
||||
if product_ids is not None:
|
||||
stock_qs = stock_qs.filter(product_id__in=product_ids)
|
||||
|
||||
# Organiser les stocks par produit triés par quantité décroissante
|
||||
stock_by_product = {}
|
||||
for s in stock_qs.values('product_id', 'location_id', 'quantity').order_by('-quantity'):
|
||||
stock_by_product.setdefault(s['product_id'], []).append((s['location_id'], s['quantity']))
|
||||
|
||||
# 3. Liste des identifiants produits cibles
|
||||
if product_ids is not None:
|
||||
target_pids = list(product_ids)
|
||||
else:
|
||||
target_pids = list(Product.objects.filter(is_active=True).values_list('id', flat=True))
|
||||
|
||||
result_map = {}
|
||||
for p_id in target_pids:
|
||||
auth_ids = product_auth_locs.get(p_id)
|
||||
if auth_ids:
|
||||
# 1a. Emplacement autorisé avec is_default
|
||||
def_auth = next((l_id for l_id in auth_ids if loc_is_default.get(l_id)), None)
|
||||
if def_auth:
|
||||
result_map[p_id] = def_auth
|
||||
continue
|
||||
|
||||
# 1b. Emplacement autorisé avec stock > 0
|
||||
stocks = stock_by_product.get(p_id, [])
|
||||
stock_auth = next((l_id for (l_id, qty) in stocks if l_id in auth_ids), None)
|
||||
if stock_auth:
|
||||
result_map[p_id] = stock_auth
|
||||
continue
|
||||
|
||||
# 1c. Premier emplacement autorisé
|
||||
result_map[p_id] = auth_ids[0]
|
||||
else:
|
||||
# 2a. Emplacement de cet entrepôt avec stock > 0
|
||||
stocks = stock_by_product.get(p_id, [])
|
||||
if stocks:
|
||||
result_map[p_id] = stocks[0][0]
|
||||
continue
|
||||
|
||||
# 2b / 2c. Emplacement par défaut de l'entrepôt
|
||||
result_map[p_id] = wh_default_loc_id
|
||||
|
||||
return result_map
|
||||
|
||||
|
||||
class StockMovementBatch(models.Model):
|
||||
"""Regroupe un ensemble de mouvements de stock créés simultanément."""
|
||||
TYPE_IN = "in"
|
||||
|
|
|
|||
|
|
@ -348,9 +348,9 @@
|
|||
|
||||
<!-- Rapprochement produit du catalogue -->
|
||||
<div class="mb-2">
|
||||
{% if item.matched_product %}
|
||||
{% if item.matched_product_id %}
|
||||
<div class="badge bg-success-subtle text-success border border-success-subtle mb-1 w-100 text-start text-truncate" id="matched-label-{{ idx }}">
|
||||
<i class="bi bi-check-circle me-1"></i>{% translate "Associé à :" %} {{ item.matched_product.name }}
|
||||
<i class="bi bi-check-circle me-1"></i>{% translate "Associé à :" %} {{ item.matched_product_name }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle mb-1 w-100 text-start text-truncate" id="matched-label-{{ idx }}">
|
||||
|
|
@ -361,7 +361,7 @@
|
|||
<select name="item_{{ idx }}_product_id" id="item_{{ idx }}_product_id" class="form-select form-select-sm" data-idx="{{ idx }}" onchange="handleMobileProductChange({{ idx }})">
|
||||
<option value="">{% translate "— Choisir un produit du catalogue —" %}</option>
|
||||
{% for p in products %}
|
||||
<option value="{{ p.id }}" {% if item.matched_product and item.matched_product.id == p.id %}selected{% endif %}>
|
||||
<option value="{{ p.id }}" {% if item.matched_product_id == p.id %}selected{% endif %}>
|
||||
{{ p.name }} ({{ p.code }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
|
@ -384,9 +384,9 @@
|
|||
</div>
|
||||
<div class="col-12 mt-1">
|
||||
<label class="form-label text-muted mb-0" style="font-size: 0.75rem;">{% translate "Emplacement de stockage" %}</label>
|
||||
<select name="item_{{ idx }}_location" class="form-select form-select-sm">
|
||||
<select name="item_{{ idx }}_location" id="item_{{ idx }}_location" class="form-select form-select-sm">
|
||||
{% for loc in locations %}
|
||||
<option value="{{ loc.id }}" {% if default_location and default_location.id == loc.id %}selected{% endif %}>{{ loc.name }}</option>
|
||||
<option value="{{ loc.id }}" {% if loc.id == item.suggested_location_id or not item.suggested_location_id and default_location and default_location.id == loc.id %}selected{% endif %}>{{ loc.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -419,6 +419,8 @@
|
|||
{% endif %}
|
||||
|
||||
<script>
|
||||
const PRODUCT_LOCATIONS_MAP = {{ product_locations_json|default:'{}'|safe }};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// ── GESTION DE L'UPLOAD EN ÉTAPE 1 ──
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
|
@ -521,6 +523,14 @@ function handleMobileProductChange(idx) {
|
|||
label.className = 'badge bg-success-subtle text-success border border-success-subtle mb-1 w-100 text-start text-truncate';
|
||||
label.innerHTML = '<i class="bi bi-check-circle me-1"></i>{% translate "Associé à :" %} ' + selectedText;
|
||||
}
|
||||
|
||||
// Adapter automatiquement l'emplacement de destination au produit sélectionné
|
||||
const pid = parseInt(select.value, 10);
|
||||
const targetLocId = PRODUCT_LOCATIONS_MAP[pid];
|
||||
const locSelect = document.getElementById('item_' + idx + '_location');
|
||||
if (locSelect && targetLocId) {
|
||||
locSelect.value = targetLocId;
|
||||
}
|
||||
} else {
|
||||
if (card) card.classList.add('unassociated');
|
||||
if (label) {
|
||||
|
|
|
|||
|
|
@ -476,9 +476,9 @@
|
|||
|
||||
<!-- Emplacement de stock -->
|
||||
<td>
|
||||
<select name="item_{{ idx }}_location" class="form-select form-select-sm">
|
||||
<select name="item_{{ idx }}_location" id="item_{{ idx }}_location" class="form-select form-select-sm">
|
||||
{% for loc in locations %}
|
||||
<option value="{{ loc.id }}" {% if loc.id == default_location.id %}selected{% endif %}>
|
||||
<option value="{{ loc.id }}" {% if loc.id == item.suggested_location_id or not item.suggested_location_id and loc.id == default_location.id %}selected{% endif %}>
|
||||
{{ loc.name }} {% if loc.is_default %}(par défaut){% endif %}
|
||||
</option>
|
||||
{% empty %}
|
||||
|
|
@ -528,6 +528,8 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
const PRODUCT_LOCATIONS_MAP = {{ product_locations_json|default:'{}'|safe }};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Zone drag & drop
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
|
@ -641,6 +643,14 @@ function handleProductSelection(idx) {
|
|||
select.classList.remove('is-invalid', 'border-warning');
|
||||
if (row) row.classList.remove('table-warning-subtle');
|
||||
if (help) help.classList.add('d-none');
|
||||
|
||||
// Adapter automatiquement l'emplacement de destination au produit sélectionné
|
||||
const pid = parseInt(select.value, 10);
|
||||
const targetLocId = PRODUCT_LOCATIONS_MAP[pid];
|
||||
const locSelect = document.getElementById('item_' + idx + '_location');
|
||||
if (locSelect && targetLocId) {
|
||||
locSelect.value = targetLocId;
|
||||
}
|
||||
} else {
|
||||
if (cb) cb.checked = false;
|
||||
select.classList.add('border-warning');
|
||||
|
|
|
|||
|
|
@ -2938,3 +2938,103 @@ class PurchaseOrderFromDocumentTests(TestCase):
|
|||
self.assertEqual(response.context["parse_result"]["image_count"], 2)
|
||||
self.assertEqual(len(response.context["saved_temp_files"]), 2)
|
||||
|
||||
def test_get_product_default_locations_map_and_model_method(self):
|
||||
from .models import WarehouseLocation, Stock, get_product_default_locations_map
|
||||
|
||||
# loc1 est l'emplacement par défaut de l'entrepôt (self.location)
|
||||
# Créer loc2 (non par défaut)
|
||||
loc2 = WarehouseLocation.objects.create(
|
||||
warehouse=self.warehouse,
|
||||
name="Rayon B Étagère 2",
|
||||
code="LOC-WH-DOC-2",
|
||||
is_default=False,
|
||||
)
|
||||
# Créer loc3 (non par défaut)
|
||||
loc3 = WarehouseLocation.objects.create(
|
||||
warehouse=self.warehouse,
|
||||
name="Rayon C Étagère 3",
|
||||
code="LOC-WH-DOC-3",
|
||||
is_default=False,
|
||||
)
|
||||
|
||||
# Produit 1 : a un emplacement autorisé spécifique (loc2)
|
||||
self.product.authorized_locations.add(loc2)
|
||||
loc_map = get_product_default_locations_map(self.warehouse, [self.product.id])
|
||||
self.assertEqual(loc_map[self.product.id], loc2.id)
|
||||
self.assertEqual(self.product.get_default_location(self.warehouse), loc2)
|
||||
|
||||
# Produit 2 : sans emplacement autorisé mais avec du stock existant en loc3
|
||||
from .models import Product
|
||||
prod2 = Product.objects.create(
|
||||
code="PROD-DOC-STOCK",
|
||||
name="Forets SDS",
|
||||
sku="SKU-DOC-SDS",
|
||||
)
|
||||
Stock.objects.create(product=prod2, location=loc3, quantity=40)
|
||||
loc_map2 = get_product_default_locations_map(self.warehouse, [prod2.id])
|
||||
self.assertEqual(loc_map2[prod2.id], loc3.id)
|
||||
self.assertEqual(prod2.get_default_location(self.warehouse), loc3)
|
||||
|
||||
# Produit 3 : sans emplacement autorisé et sans stock -> fallback vers l'emplacement par défaut de l'entrepôt
|
||||
prod3 = Product.objects.create(
|
||||
code="PROD-DOC-NONE",
|
||||
name="Scie Égoïne",
|
||||
sku="SKU-DOC-SAW",
|
||||
)
|
||||
loc_map3 = get_product_default_locations_map(self.warehouse, [prod3.id])
|
||||
self.assertEqual(loc_map3[prod3.id], self.location.id)
|
||||
self.assertEqual(prod3.get_default_location(self.warehouse), self.location)
|
||||
|
||||
def test_view_parse_pdf_suggests_product_default_location(self):
|
||||
from unittest.mock import patch
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.urls import reverse
|
||||
from .models import WarehouseLocation
|
||||
|
||||
loc_specific = WarehouseLocation.objects.create(
|
||||
warehouse=self.warehouse,
|
||||
name="Zone Outillage Spécial",
|
||||
code="LOC-SPECIFIC",
|
||||
is_default=False,
|
||||
)
|
||||
self.product.authorized_locations.add(loc_specific)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
url = reverse("stock:purchase_order_from_pdf")
|
||||
file1 = SimpleUploadedFile("devis.pdf", b"%PDF-1.4...", content_type="application/pdf")
|
||||
|
||||
mock_parse_result = {
|
||||
'is_ocr': False,
|
||||
'is_image': False,
|
||||
'total_items': 1,
|
||||
'metadata': {'supplier_name': 'Test Sup', 'quote_number': 'OF999'},
|
||||
'matched_supplier': self.supplier,
|
||||
'items': [{
|
||||
'reference': 'ZE 45525',
|
||||
'name': 'Mètre ruban',
|
||||
'quantity': 3,
|
||||
'unit': 'piece',
|
||||
'raw_unit': 'Piece',
|
||||
'gross_unit_price': 9.28,
|
||||
'discount': '0%',
|
||||
'unit_price': 9.28,
|
||||
'total_amount': 27.84,
|
||||
'vat_rate': 21.0,
|
||||
'matched_product_id': self.product.id,
|
||||
'matched_product_name': self.product.name,
|
||||
'matched_product_code': self.product.code,
|
||||
}],
|
||||
}
|
||||
|
||||
with patch("stock.pdf_parser.PdfQuoteParser.parse", return_value=mock_parse_result):
|
||||
response = self.client.post(url, {
|
||||
"action": "parse_pdf",
|
||||
"warehouse": self.warehouse.pk,
|
||||
"pdf_file": [file1],
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
items = response.context["parse_result"]["items"]
|
||||
self.assertEqual(items[0]["suggested_location_id"], loc_specific.id)
|
||||
self.assertIn(f'value="{loc_specific.id}" selected', response.content.decode())
|
||||
|
||||
|
|
|
|||
|
|
@ -1810,6 +1810,12 @@ def purchase_order_from_pdf(request):
|
|||
|
||||
location_id = request.POST.get(f'item_{i}_location')
|
||||
location = WarehouseLocation.objects.filter(warehouse=warehouse, pk=location_id).first()
|
||||
if not location:
|
||||
from .models import get_product_default_locations_map
|
||||
prod_locs = get_product_default_locations_map(warehouse, [product.id])
|
||||
def_loc_id = prod_locs.get(product.id)
|
||||
if def_loc_id:
|
||||
location = WarehouseLocation.objects.filter(warehouse=warehouse, pk=def_loc_id).first()
|
||||
if not location:
|
||||
location = warehouse.locations.filter(is_default=True).first() or warehouse.locations.first()
|
||||
|
||||
|
|
@ -1875,9 +1881,32 @@ def purchase_order_from_pdf(request):
|
|||
|
||||
locations = []
|
||||
default_location = None
|
||||
product_locations_map = {}
|
||||
product_locations_json = "{}"
|
||||
if warehouse:
|
||||
from .models import get_product_default_locations_map
|
||||
locations = warehouse.locations.all().order_by('name')
|
||||
default_location = warehouse.locations.filter(is_default=True).first() or locations.first()
|
||||
product_locations_map = get_product_default_locations_map(warehouse)
|
||||
product_locations_json = json.dumps(product_locations_map)
|
||||
|
||||
if parse_result and 'items' in parse_result:
|
||||
for item in parse_result['items']:
|
||||
matched_pid = item.get('matched_product_id')
|
||||
if not matched_pid and item.get('matched_product'):
|
||||
matched_obj = item['matched_product']
|
||||
matched_pid = getattr(matched_obj, 'id', None)
|
||||
item['matched_product_id'] = matched_pid
|
||||
if 'matched_product_name' not in item:
|
||||
item['matched_product_name'] = getattr(matched_obj, 'name', '')
|
||||
if 'matched_product_code' not in item:
|
||||
item['matched_product_code'] = getattr(matched_obj, 'code', '')
|
||||
if matched_pid and matched_pid in product_locations_map:
|
||||
item['suggested_location_id'] = product_locations_map[matched_pid]
|
||||
elif default_location:
|
||||
item['suggested_location_id'] = default_location.id
|
||||
else:
|
||||
item['suggested_location_id'] = None
|
||||
|
||||
return render(request, 'stock/purchase_order_from_pdf.html', {
|
||||
'step': step,
|
||||
|
|
@ -1885,6 +1914,8 @@ def purchase_order_from_pdf(request):
|
|||
'selected_warehouse': warehouse,
|
||||
'locations': locations,
|
||||
'default_location': default_location,
|
||||
'product_locations_map': product_locations_map,
|
||||
'product_locations_json': product_locations_json,
|
||||
'suppliers': suppliers,
|
||||
'categories': categories,
|
||||
'products': products,
|
||||
|
|
|
|||
Loading…
Reference in a new issue