Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
T
third-party
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
2
Merge Requests
2
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
cooperatic-foodcoops
third-party
Commits
5a937781
Commit
5a937781
authored
Apr 15, 2022
by
François C.
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '2470-alc-update-price-and-stock' into 'dev_cooperatic'
2470 alc update price and stock See merge request
!156
parents
1e561340
cb2d1c59
Pipeline
#2119
passed with stage
in 1 minute 30 seconds
Changes
6
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
195 additions
and
48 deletions
+195
-48
models.py
inventory/models.py
+2
-2
oders_helper_style.css
orders/static/css/oders_helper_style.css
+36
-3
orders_helper.js
orders/static/js/orders_helper.js
+86
-28
models.py
products/models.py
+20
-7
views.py
products/views.py
+24
-5
helper.html
templates/orders/helper.html
+27
-3
No files found.
inventory/models.py
View file @
5a937781
...
...
@@ -368,10 +368,10 @@ class CagetteInventory(models.Model):
return
{
'missed'
:
missed
,
'unchanged'
:
unchanged
,
'done'
:
done
}
@staticmethod
def
update_products_stock
(
inventory_data
):
def
update_products_stock
(
inventory_data
,
precision
=
2
):
""" Updates Odoo stock after a shelf inventory or another action"""
TWOPLACES
=
Decimal
(
10
)
**
-
2
TWOPLACES
=
Decimal
(
10
)
**
-
precision
api
=
OdooAPI
()
missed
=
[]
unchanged
=
[]
...
...
orders/static/css/oders_helper_style.css
View file @
5a937781
...
...
@@ -264,7 +264,8 @@
padding
:
.5rem
.5rem
;
}
.supplier_package_qty
{
.supplier_package_qty
,
.supplier_price
{
font-style
:
italic
;
font-size
:
1.3rem
;
}
...
...
@@ -381,14 +382,46 @@
display
:
block
;
}
.modal_product_actions_section
{
.product_actions_container
{
display
:
flex
;
flex-direction
:
column
;
}
.product_actions_section
{
width
:
100%
;
display
:
flex
;
margin
:
1em
0
;
}
.modal_product_actions_section
.tooltip
{
.product_actions_column
{
width
:
50%
;
}
.product_actions_full_column
{
width
:
100%
;
}
.product_actions_column
.tooltip
{
margin-left
:
5px
;
}
.product_prices_title
{
margin-bottom
:
0
!important
;
}
.product_prices_area
{
margin
:
20px
0
;
display
:
flex
;
flex-direction
:
column
;
gap
:
10px
;
}
.product_price_action
{
display
:
flex
;
justify-content
:
center
;
align-items
:
center
;
gap
:
10px
;
}
.modal_product_actions_title
{
font-weight
:
bold
;
font-size
:
2.2rem
;
...
...
orders/static/js/orders_helper.js
View file @
5a937781
...
...
@@ -30,12 +30,9 @@ var dbc = null,
fingerprint
=
null
;
var
clicked_order_pill
=
null
;
let
userAgent
=
navigator
.
userAgent
;
var
timerId
=
null
;
var
timerId
;
/* - UTILS */
/**
...
...
@@ -275,8 +272,8 @@ function compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty,
purchase_package_qty_for_coverage
*=
coeff
;
}
}
// return Round up to unit for all products
return
Math
.
ceil
(
purchase_package_qty_for_coverage
);
return
Math
.
ceil
(
purchase_package_qty_for_coverage
);
// return Round up to unit for all products
}
function
compute_and_affect_product_supplier_quantities
(
coeff
,
days
)
{
...
...
@@ -285,22 +282,19 @@ function compute_and_affect_product_supplier_quantities(coeff, days) {
product
]
of
Object
.
entries
(
products
))
{
if
(
'suppliersinfo'
in
product
&&
product
.
suppliersinfo
.
length
>
0
)
{
let
purchase_qty_for_coverage
=
null
;
// Durée couverture produit = (stock + qté entrante + qté commandée ) / conso quotidienne
const
stock
=
product
.
qty_available
;
const
incoming_qty
=
product
.
incoming_qty
;
const
daily_conso
=
product
.
daily_conso
;
purchase_package_qty_for_coverage
=
compute_purchase_qty_for_coverage
(
product
,
coeff
,
stock
,
incoming_qty
,
daily_conso
,
days
);
let
purchase_package_qty_for_coverage
=
compute_purchase_qty_for_coverage
(
product
,
coeff
,
stock
,
incoming_qty
,
daily_conso
,
days
);
// Set qty to purchase for first supplier only
products
[
key
].
suppliersinfo
[
0
].
qty
=
purchase_package_qty_for_coverage
;
}
}
}
/**
* Compute the qty to buy for each product, depending the coverage days.
* Set the computed qty for the first supplier only.
...
...
@@ -669,13 +663,14 @@ function save_supplier_product_association(product, supplier, cell) {
traditional
:
true
,
contentType
:
"application/json; charset=utf-8"
,
data
:
JSON
.
stringify
(
data
),
success
:
()
=>
{
success
:
(
res_data
)
=>
{
// Save supplierinfo in product
if
(
!
(
'suppliersinfo'
in
product
))
{
product
.
suppliersinfo
=
[];
}
product
.
suppliersinfo
.
push
({
id
:
res_data
.
res
.
psi_id
,
supplier_id
:
supplier
.
id
,
package_qty
:
package_qty
,
product_code
:
false
,
...
...
@@ -859,8 +854,10 @@ function commit_actions_on_product(product, inputs) {
npa
:
[],
to_archive
:
false
,
minimal_stock
:
0
,
qty_available
:
0
,
id
:
product
.
id
,
name
:
product
.
name
name
:
product
.
name
,
suppliersinfo
:
[]
};
inputs
.
each
(
function
(
i
,
e
)
{
...
...
@@ -876,6 +873,13 @@ function commit_actions_on_product(product, inputs) {
if
(
input
.
prop
(
'checked'
)
==
true
&&
product
.
incoming_qty
===
0
)
{
actions
.
to_archive
=
true
;
}
}
else
if
(
input
.
attr
(
'name'
)
==
"actual_stock"
)
{
actions
.
qty_available
=
parseFloat
(
input
.
val
());
}
else
if
(
input
.
attr
(
'class'
)
!==
undefined
&&
input
.
attr
(
'class'
).
includes
(
"product_supplier_price"
))
{
actions
.
suppliersinfo
.
push
({
supplierinfo_id
:
parseInt
(
input
.
attr
(
'supplierinfo_id'
)),
price
:
parseFloat
(
input
.
val
())
});
}
});
...
...
@@ -1424,12 +1428,23 @@ function display_suppliers() {
});
}
/**
* Compute data to display in products table
*
* Package qties & prices are related to suppliers,
* so 1 product can have multiple values for these.
* In case of different values, display value under input in supplier column
*
* @param {Object} product
* @returns Object of computed data to add to product object
*/
function
_compute_product_data
(
product
)
{
let
item
=
{};
/* Supplier related data */
let
purchase_qty
=
0
;
// Calculate product's total purchase qty
let
p_package_qties
=
[];
// Look for differences in package qties
let
p_price
=
[];
for
(
let
p_supplierinfo
of
product
.
suppliersinfo
)
{
// Preset qty for input if product related to supplier: existing qty or null (null -> qty to be set, display an empty input)
...
...
@@ -1444,6 +1459,7 @@ function _compute_product_data(product) {
// Store temporarily product package qties
p_package_qties
.
push
(
p_supplierinfo
.
package_qty
);
p_price
.
push
(
p_supplierinfo
.
price
);
}
item
.
purchase_qty
=
purchase_qty
;
...
...
@@ -1456,13 +1472,19 @@ function _compute_product_data(product) {
}
if
(
p_package_qties
.
length
==
0
||
!
p_package_qties
.
every
((
val
,
i
,
arr
)
=>
val
===
arr
[
0
]))
{
// Don't display package qty if no supplierinf
or if not all package qties are equals,
// Don't display package qty if no supplierinf
o or if not all package qties are equals
item
.
package_qty
=
'X'
;
}
else
{
// If all package qties are equals, display it
item
.
package_qty
=
p_package_qties
[
0
];
}
if
(
p_price
.
length
==
0
||
!
p_price
.
every
((
val
,
i
,
arr
)
=>
val
===
arr
[
0
]))
{
item
.
price
=
'X'
;
}
else
{
item
.
price
=
p_price
[
0
];
}
/* Coverage related data */
const
coverage_days
=
(
order_doc
.
coverage_days
!==
null
)
?
order_doc
.
coverage_days
:
0
;
let
qty_not_covered
=
0
;
...
...
@@ -1600,6 +1622,7 @@ function prepare_datatable_columns() {
let
content
=
`<div id="
${
base_id
}
_cell_content" class="custom_cell_content">
<input type="number" class="product_qty_input" id="
${
base_id
}
_qty_input" min="-1" value=
${
data
}
>`
;
// Add package qty & price data if they differ between suppliers
if
(
full
.
package_qty
===
'X'
)
{
let
product_data
=
products
.
find
(
p
=>
p
.
id
==
full
.
id
);
...
...
@@ -1610,6 +1633,16 @@ function prepare_datatable_columns() {
}
}
if
(
full
.
price
===
'X'
)
{
let
product_data
=
products
.
find
(
p
=>
p
.
id
==
full
.
id
);
if
(
product_data
!==
undefined
)
{
let
supplierinfo
=
product_data
.
suppliersinfo
.
find
(
psi
=>
psi
.
supplier_id
==
supplier
.
id
);
content
+=
`<span class="supplier_price">Prix HT :
${
supplierinfo
.
price
}
€</span>`
;
}
}
content
+=
`</div>`
;
return
content
;
...
...
@@ -1633,19 +1666,30 @@ function prepare_datatable_columns() {
});
columns
.
push
({
data
:
"p
urchase_qty
"
,
title
:
"
Qté Achat
"
,
data
:
"p
rice
"
,
title
:
"
Prix HT
"
,
className
:
"dt-body-center"
,
render
:
function
(
data
)
{
return
(
data
===
'X'
)
?
data
:
`
${
data
}
€`
;
},
width
:
"4%"
});
columns
.
push
({
data
:
"
qty_not_covered
"
,
title
:
"
Besoin non couvert (qté)
"
,
data
:
"
purchase_qty
"
,
title
:
"
Qté Achat
"
,
className
:
"dt-body-center"
,
width
:
"4%"
});
// Not in use for now
// columns.push({
// data: "qty_not_covered",
// title: "Besoin non couvert (qté)",
// className: "dt-body-center",
// width: "4%"
// });
columns
.
push
({
data
:
"days_covered"
,
title
:
"Jours de couverture"
,
...
...
@@ -1657,7 +1701,7 @@ function prepare_datatable_columns() {
title
:
``
,
className
:
"dt-body-center"
,
orderable
:
false
,
render
:
function
(
data
)
{
render
:
function
()
{
return
`<button type="button" class="btn--primary product_actions">Actions</button>`
;
},
width
:
"4%"
...
...
@@ -1804,7 +1848,7 @@ function display_products(params) {
.
focus
();
}
})
.
on
(
'click'
,
'tbody td .product_actions'
,
function
(
e
)
{
.
on
(
'click'
,
'tbody td .product_actions'
,
function
()
{
// Save / unsave selected row
const
p_id
=
products_table
.
row
(
$
(
this
).
closest
(
'tr'
)).
data
().
id
;
const
product
=
products
.
find
(
p
=>
p
.
id
==
p_id
);
...
...
@@ -1812,6 +1856,7 @@ function display_products(params) {
let
modal_product_actions
=
$
(
'#templates #modal_product_actions'
);
modal_product_actions
.
find
(
".product_name"
).
text
(
product
.
name
);
modal_product_actions
.
find
(
".actual_stock_input"
).
val
(
product
.
qty_available
);
const
product_can_be_archived
=
product
.
incoming_qty
===
0
;
...
...
@@ -1825,6 +1870,18 @@ function display_products(params) {
.
addClass
(
"checkbox_action_disabled"
);
}
let
product_price_action_template
=
$
(
'#templates #product_price_action_template'
);
modal_product_actions
.
find
(
".product_prices_area"
).
empty
();
for
(
let
supplierinfo
of
product
.
suppliersinfo
)
{
let
supplier
=
suppliers_list
.
find
(
s
=>
s
.
id
==
supplierinfo
.
supplier_id
);
product_price_action_template
.
find
(
".supplier_name"
).
text
(
supplier
.
display_name
);
product_price_action_template
.
find
(
".product_supplier_price"
).
attr
(
'supplierinfo_id'
,
supplierinfo
.
id
);
modal_product_actions
.
find
(
".product_prices_area"
).
append
(
product_price_action_template
.
html
());
}
openModal
(
modal_product_actions
.
html
(),
()
=>
{
...
...
@@ -1835,7 +1892,13 @@ function display_products(params) {
'Valider'
,
false
);
// Set inputs val after modal is displayed
modal
.
find
(
'input[name="minimal_stock"]'
).
val
(
product
.
minimal_stock
);
modal
.
find
(
'input[name="actual_stock"]'
).
val
(
product
.
qty_available
);
for
(
let
supplierinfo
of
product
.
suppliersinfo
)
{
modal
.
find
(
`input[supplierinfo_id="
${
supplierinfo
.
id
}
"]`
).
val
(
supplierinfo
.
price
);
}
});
...
...
@@ -2616,17 +2679,12 @@ $(document).ready(function() {
// Have to capture mousedown and mouseup events, instead of using only click event
// Indeed, capturing click only remove the ability to click to have focus on the input to type a number.
$
(
document
).
on
(
"mousedown"
,
'[type="number"]'
,
function
()
{
const
clicked
=
this
;
qties_values
[
$
(
clicked
).
attr
(
'id'
)]
=
$
(
clicked
).
val
();
qties_values
[
$
(
this
).
attr
(
'id'
)]
=
$
(
this
).
val
();
});
$
(
document
).
on
(
"mouseup"
,
'[type="number"]'
,
function
()
{
const
clicked
=
this
;
try
{
if
(
$
(
clicked
).
val
()
!=
qties_values
[
$
(
clicked
).
attr
(
'id'
)])
{
process_new_product_qty
(
clicked
);
if
(
$
(
this
).
val
()
!=
qties_values
[
$
(
this
).
attr
(
'id'
)])
{
process_new_product_qty
(
this
);
}
}
catch
(
err
)
{
console
.
log
(
err
);
...
...
products/models.py
View file @
5a937781
...
...
@@ -160,6 +160,7 @@ class CagetteProduct(models.Model):
try
:
res
[
"update"
]
=
api
.
update
(
'product.supplierinfo'
,
psi_id
,
f
)
res
[
"psi_id"
]
=
psi_id
except
Exception
as
e
:
res
[
'error'
]
=
str
(
e
)
else
:
...
...
@@ -185,6 +186,7 @@ class CagetteProduct(models.Model):
try
:
res
[
'create'
]
=
api
.
create
(
'product.supplierinfo'
,
f
)
res
[
'psi_id'
]
=
res
[
'create'
]
# consistency between update & create res
except
Exception
as
e
:
res
[
'error'
]
=
str
(
e
)
...
...
@@ -259,13 +261,17 @@ class CagetteProduct(models.Model):
- NPA (ne pas acheter)
- Product is active
- Minimal stock
- price /supplier
"""
res
=
{}
try
:
api
=
OdooAPI
()
# Minimal stock
f
=
{
'minimal_stock'
:
data
[
'minimal_stock'
]}
# Minimal & Actual stock, Active
f
=
{
'minimal_stock'
:
float
(
data
[
'minimal_stock'
]),
'active'
:
not
data
[
'to_archive'
]
}
# NPA
if
'simple-npa'
in
data
[
'npa'
]:
...
...
@@ -287,13 +293,19 @@ class CagetteProduct(models.Model):
if
len
(
data
[
'npa'
])
==
0
:
f
[
'purchase_ok'
]
=
1
# Active
f
[
"active"
]
=
not
data
[
'to_archive'
]
res
[
"update"
]
=
api
.
update
(
'product.template'
,
int
(
data
[
'id'
]),
f
)
# Update suppliers info
res
[
"update_supplierinfo"
]
=
[]
for
supplierinfo
in
data
[
"suppliersinfo"
]:
f
=
{
'price'
:
supplierinfo
[
"price"
]}
res_update_si
=
api
.
update
(
'product.supplierinfo'
,
int
(
supplierinfo
[
'supplierinfo_id'
]),
f
)
res
[
"update_supplierinfo"
]
.
append
(
res_update_si
)
res
[
"update"
]
=
api
.
update
(
'product.template'
,
data
[
'id'
],
f
)
except
Exception
as
e
:
res
[
"error"
]
=
str
(
e
)
coop_logger
.
error
(
"
update_npa_and_minimal_stock
:
%
s
%
s"
,
str
(
e
),
str
(
data
))
coop_logger
.
error
(
"
commit_actions_on_product
:
%
s
%
s"
,
str
(
e
),
str
(
data
))
return
res
class
CagetteProducts
(
models
.
Model
):
"""Initially used to make massive barcode update."""
...
...
@@ -593,7 +605,7 @@ class CagetteProducts(models.Model):
if
supplier_ids
is
not
None
and
len
(
supplier_ids
)
>
0
:
# Get products/supplier relation
f
=
[
"product_tmpl_id"
,
'date_start'
,
'date_end'
,
'package_qty'
,
'price'
,
'name'
,
'product_code'
]
f
=
[
"
id"
,
"
product_tmpl_id"
,
'date_start'
,
'date_end'
,
'package_qty'
,
'price'
,
'name'
,
'product_code'
]
c
=
[[
'name'
,
'in'
,
[
int
(
x
)
for
x
in
supplier_ids
]]]
psi
=
api
.
search_read
(
'product.supplierinfo'
,
c
,
f
)
...
...
@@ -651,6 +663,7 @@ class CagetteProducts(models.Model):
for
psi_item
in
valid_psi
:
if
psi_item
[
"product_tmpl_id"
]
is
not
False
and
psi_item
[
"product_tmpl_id"
][
0
]
==
fp
[
"id"
]:
filtered_products_t
[
i
][
'suppliersinfo'
]
.
append
({
'id'
:
int
(
psi_item
[
"id"
]),
'supplier_id'
:
int
(
psi_item
[
"name"
][
0
]),
'package_qty'
:
psi_item
[
"package_qty"
],
'price'
:
psi_item
[
"price"
],
...
...
products/views.py
View file @
5a937781
...
...
@@ -149,13 +149,13 @@ def commit_actions_on_product(request):
res
=
CagetteProduct
.
commit_actions_on_product
(
data
)
# If stock > 0: do inventory to set stock to 0
do_stock_update
=
False
# If product to archive and stock > 0: do inventory to set stock to 0
if
data
[
"to_archive"
]
is
True
and
product_data
[
"qty_available"
]
!=
0
:
try
:
p
=
{
'id'
:
product_data
[
'product_variant_ids'
][
0
],
# Need product id
'uom_id'
:
product_data
[
'uom_id'
],
'qty'
:
-
product_data
[
"qty_available"
]
'qty'
:
0
}
inventory_data
=
{
...
...
@@ -163,15 +163,34 @@ def commit_actions_on_product(request):
'products'
:
[
p
]
}
res_inventory
=
CagetteInventory
.
update_products_stock
(
inventory_data
)
do_stock_update
=
True
# Else update actual stock if changed
elif
data
[
"qty_available"
]
!=
product_data
[
"qty_available"
]:
p
=
{
'id'
:
product_data
[
'product_variant_ids'
][
0
],
# Need product id
'uom_id'
:
product_data
[
'uom_id'
],
'qty'
:
data
[
"qty_available"
]
}
inventory_data
=
{
'name'
:
'MAJ stock depuis Aide à la Commande - '
+
product_data
[
'name'
],
'products'
:
[
p
]
}
do_stock_update
=
True
if
do_stock_update
is
True
:
try
:
res_inventory
=
CagetteInventory
.
update_products_stock
(
inventory_data
,
3
)
if
res_inventory
[
'errors'
]
or
res_inventory
[
'missed'
]:
res
[
"code"
]
=
"error_stock_update"
res
[
"error"
]
=
res_inventory
[
'errors'
]
return
JsonResponse
(
res
,
status
=
500
)
except
Exception
as
e
:
res
[
"code"
]
=
"error_stock_update"
return
JsonResponse
(
res
,
status
=
500
)
except
Exception
as
e
:
res
[
'error'
]
=
str
(
e
)
coop_logger
.
error
(
"Update npa and minimal stock :
%
s"
,
res
[
'error'
])
...
...
templates/orders/helper.html
View file @
5a937781
...
...
@@ -276,9 +276,18 @@
<hr/>
</div>
<div
id=
"product_price_action_template"
>
<div
class=
"product_price_action"
>
<span
class=
"supplier_name"
></span>
<input
type=
"number"
class=
"product_supplier_price"
name=
""
value=
""
/>
</div>
</div>
<div
id=
"modal_product_actions"
>
Actions sur
<h3><span
class=
"product_name"
></span></h3>
<div
class=
"modal_product_actions_section"
>
<div
class=
"product_actions_container"
>
<div
class=
"product_actions_section"
>
<div
class=
"product_actions_column"
>
<h4
class=
"modal_product_actions_title"
>
NPA
</h4>
<div
class=
"npa-options"
>
<label><input
type=
"checkbox"
name=
"npa-actions"
value=
"simple-npa"
/>
Mettre le produit en NPA
</label>
...
...
@@ -286,7 +295,7 @@
<label><input
type=
"checkbox"
name=
"npa-actions"
value=
"fds-in-name"
/>
Mettre le produit en NPA et afficher FDS
</label>
</div>
</div>
<div
class=
"modal_product_actions_sectio
n"
>
<div
class=
"product_actions_colum
n"
>
<h4
class=
"modal_product_actions_title"
>
Archiver le produit
</h4>
<label
class=
"checkbox_action_disabled"
><input
type=
"checkbox"
name=
"archive-action"
value=
"archive"
disabled
/>
Archiver
</label>
<div
class=
"tooltip"
>
...
...
@@ -296,10 +305,25 @@
</span>
</div>
</div>
<div
class=
"modal_product_actions_section"
>
</div>
<div
class=
"product_actions_section"
>
<div
class=
"product_actions_column"
>
<h4
class=
"modal_product_actions_title"
>
Stock minimum
</h4>
<input
type=
"number"
name=
"minimal_stock"
value=
""
/>
</div>
<div
class=
"product_actions_column"
>
<h4
class=
"modal_product_actions_title"
>
Stock réel
</h4>
<input
type=
"number"
name=
"actual_stock"
value=
""
/>
</div>
</div>
<div
class=
"product_actions_section"
>
<div
class=
"product_actions_full_column"
>
<h4
class=
"modal_product_actions_title product_prices_title"
>
Prix
</h4>
<i
class=
"product_prices_title_label"
>
(par fournisseur dans cette commande)
</i>
<div
class=
"product_prices_area"
></div>
</div>
</div>
</div>
</div>
<div
id=
"modal_create_order"
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment