Skip to content

Instantly share code, notes, and snippets.

@jev-odoo
Last active July 13, 2020 12:03
Show Gist options
  • Select an option

  • Save jev-odoo/b36cbaf094c8b2a6c43444bffc754717 to your computer and use it in GitHub Desktop.

Select an option

Save jev-odoo/b36cbaf094c8b2a6c43444bffc754717 to your computer and use it in GitHub Desktop.
import datetime
import time
QUANT_NUMBER = 0
QUANT_ERRORS = 0
QUANT_RESET = 0
QUANT_ML_NUMBER = 0
QUANT_ML_ERRORS = 0
QUANT_ML_RESET = 0
ML_NUMBER = 0
ML_ERRORS = 0
ML_UNRESERVED = 0
class Timer(object):
"""Class who tries to estimate the remaining time of a definite loop"""
def __init__(self, total: int, update_every: int = 1):
"""
:param total: The total number of iteration for the loop
:type total: int
:param update_every: Optional. Will recalculate the remaining time every n iterations.
:type
"""
self.start = datetime.datetime.now()
self.total = total
def remains(self, done):
now = datetime.datetime.now()
eslaped = (now - self.start).total_seconds()
left = (self.total - done) * (now - self.start) / done
speed = int(done / eslaped)
speed = speed if 10000 > speed > 0 else 0
seconds = int(left.total_seconds())
string_eslaped = f'{self.convert_seconds(eslaped)} eslaped'
string_remaining = f'{self.convert_seconds(seconds)} remaining'
speed_string = f'≃ {speed:4d} records/s'
return f'{string_eslaped} - {string_remaining} - {speed_string}'
@staticmethod
def convert_seconds(seconds):
(minutes, seconds) = divmod(seconds, 60)
(hours, minutes) = divmod(minutes, 60)
return f'{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}'
def timeinit(method):
def timed(*args, **kwargs):
ts = time.time()
result = method(*args, **kwargs)
te = time.time()
total_seconds = int(te - ts)
if 'log_time' in kwargs:
name = kwargs.get('log_name', method.__name__.upper())
kwargs['log_time'][name] = total_seconds
else:
(minutes, seconds) = divmod(total_seconds, 60)
print(f'{method.__name__!r}: {total_seconds}s - {minutes}:{seconds}m')
return result
return timed
@timeinit
def process(env, products: list = None, pickings: list = None, mrps: list = None, quants: list = None, mls: list = None, repair: bool = False, print_detailed_report: bool = True, print_ids_list: bool = False) -> dict:
"""
Check and/or repair inconsistencies in quants
:param env: The environment variable
:param products: Optionnal. List of the product.product to be checked. Is added to other lists.
:type products: list
:param pickings: Optionnal. List of the stock.picking to be checked. Is added to other lists.
:type pickings: list
:param mrps: Optionnal. List of the mrp.production to be checked. Is added to other lists.
:type mrps: list
:param quants: Optionnal. List of the stock.quants to be checked. Is added to other lists.
:type quants: list
:param mls: Optionnal. List of move_lines to be checked.
:type mls: list
:param repair: If true, will reset and unreserve what should be.
:type repair: bool
:param print_detailed_report: Print the detail for each errors.
:type print_detailed_report: bool
:param print_ids_list: Print the ids of problematic quants and move_lines
:type print_ids_list: bool
:return: dict with the list of ids of problematic quants and move_lines
"""
def check_no_parameters_set():
return not quant_ids and not product_product_ids and not stock_picking_ids and not mrp_production_ids
def reset_reserved_quantity_on_quant(record):
global QUANT_RESET
QUANT_RESET += 1
record.write({'reserved_quantity': 0})
return "Reserved Quantity reset to 0"
def reset_uom_qty_on_move_lines(records):
global QUANT_ML_RESET
QUANT_ML_RESET += len(records)
records.with_context(bypass_reservation_update=True).write({'product_uom_qty': 0})
def unreserve_move_lines(lines: list):
if len(move_lines_to_unreserve) > 1:
env.cr.execute(""" UPDATE stock_move_line SET product_uom_qty = 0, product_qty = 0 WHERE id in %s ;""" % (tuple(move_lines_to_unreserve),))
elif len(move_lines_to_unreserve) == 1:
env.cr.execute(""" UPDATE stock_move_line SET product_uom_qty = 0, product_qty = 0 WHERE id = %s ;""" % (move_lines_to_unreserve[0]))
def get_model(model, ids):
if check_model(model) and ids:
result = env[model]
result = result.browse(ids).exists()
if not result:
print(f'No {model} found with ids: {ids}')
return result
return env['product.product'] # Empty recordset
def get_products(p_ids, sp_ids, mrp_ids):
product_ids = get_model('product.product', p_ids)
sp_ids = get_model('stock.picking', sp_ids)
if sp_ids:
product_ids |= get_products_from_stock_picking(sp_ids)
mrp_ids = get_model('mrp.production', mrp_ids)
if mrp_ids:
product_ids |= get_products_from_mrp_production(mrp_ids)
return product_ids
def get_products_from_stock_picking(stock_picking):
return stock_picking.mapped('move_lines.product_id')
def get_products_from_mrp_production(mrp_production):
product_ids = env['product.product']
move_raw_product_ids = mrp_production.mapped('move_raw_ids.product_id')
move_finished_product_ids = mrp_production.mapped('move_finished_ids.product_id')
product_ids |= move_raw_product_ids
product_ids |= move_finished_product_ids
return product_ids
def check_model(model):
model = env['ir.model'].search([('model', '=', model)])
return model
def _compute_quant_report(string, quant, move_lines):
res_on_mls = _get_ml_sum(move_lines)
mls_str = str.join(', ', [str(move_line_id) for move_line_id in move_lines.ids])
diff = res_on_mls - quant.reserved_quantity
result = list()
result.append(f"Problematic quant found: {quant.id} (quantity: {quant.quantity}, reserved_quantity: {quant.reserved_quantity})")
result.append(f'{res_qty_str} {string}')
if move_lines:
global QUANT_ML_ERRORS
QUANT_ML_ERRORS += len(move_lines)
result.append(f"These move lines are reserved on it: {mls_str} (sum of the reservation: {res_on_mls}, difference: {diff})")
for ml in move_lines:
if ml.product_uom_id != quant.product_uom_id:
result.append(f'This move line has inconsistent UOM: {ml.id} (ml uom: {ml.product_uom_id.name}/{ml.product_uom_id.id}, quant uom: {quant.product_uom_id.name}/{quant.product_uom_id.id}')
else:
result.append(f'No move lines are reserved on it. (sum of the reservation: 0, difference: {diff})')
result += _get_product_warning(quant.product_id)
result.append('******************')
return result
def _get_product_warning(product):
result = list()
product_string = f"Product: {product.name} (id: {product.id})"
if product.default_code:
product_string = f"{product_string} - Internal Ref: {product.default_code}"
result.append(product_string)
result.append(f"UOM: {product.uom_id.name} (id: {product.uom_id.id}) - Accuracy: {product.uom_id.rounding}")
return result
def _get_ml_sum(ml):
return sum(ml.mapped('product_qty'))
global QUANT_NUMBER
global QUANT_ERRORS
global QUANT_RESET
global QUANT_ML_NUMBER
global QUANT_ML_ERRORS
global QUANT_ML_RESET
global ML_NUMBER
global ML_ERRORS
global ML_UNRESERVED
QUANT_NUMBER = 0
QUANT_ERRORS = 0
QUANT_RESET = 0
QUANT_ML_NUMBER = 0
QUANT_ML_ERRORS = 0
QUANT_ML_RESET = 0
ML_NUMBER = 0
ML_ERRORS = 0
ML_UNRESERVED = 0
product_product_ids = products or []
stock_picking_ids = pickings or []
mrp_production_ids = mrps or []
ml_ids = mls or []
quant_ids = quants or []
domain = []
product_ids = get_products(product_product_ids, stock_picking_ids, mrp_production_ids)
quants = env['stock.quant']
if quant_ids:
quants = quants.browse(quant_ids).exists()
if product_ids:
domain.append(('product_id', 'in', product_ids.ids))
quants |= quants.search(domain)
if check_no_parameters_set():
quants = quants.search([])
precision = env.ref('product.decimal_product_uom')
res_qty_str = 'Its `reserved_quantity` field'
QUANT_NUMBER = len(quants)
move_line_ids = []
warning = list()
processed_quant_ids = []
processed_ml_ids = []
timer = Timer(QUANT_NUMBER)
for index, quant in enumerate(quants):
remaining_time = ''
if index % 50 == 0:
remaining_time = timer.remains(index + 1)
print(f'Processing quant {index + 1:6d}/{QUANT_NUMBER} - Quants errors: {QUANT_ERRORS:3d} - Move_lines in quants errors: {QUANT_ML_ERRORS:3d} - {remaining_time}', end='\r', flush=True)
move_lines = env["stock.move.line"].search([
('product_id', '=', quant.product_id.id),
('location_id', '=', quant.location_id.id),
('lot_id', '=', quant.lot_id.id),
('package_id', '=', quant.package_id.id),
('owner_id', '=', quant.owner_id.id),
('product_qty', '!=', 0)
])
QUANT_ML_NUMBER += len(move_lines)
move_line_ids += move_lines.ids
product_uom_accuracy = abs(int(('%e' % quant.product_id.uom_id.rounding).split('e')[-1]))
warning_string = ''
reserved_qty = round(quant.reserved_quantity, product_uom_accuracy)
reset_quant = False
reset_move_lines = False
if quant.location_id.should_bypass_reservation():
# If a quant is in a location that should bypass the reservation, its `reserved_quantity` field
# should be 0.
if reserved_qty != 0:
reset_quant = True
warning_string = "is not 0 while its location should bypass the reservation"
else:
# If a quant is in a reservable location, its `reserved_quantity` should be exactly the sum
# of the `product_qty` of all the partially_available / assigned move lines with the same
# characteristics.
if reserved_qty == 0:
if move_lines:
reset_move_lines = True
warning_string = "is 0 while there is move lines reserved on it"
elif reserved_qty < 0:
reset_quant = True
warning_string = "is negative while it should not happen"
if move_lines:
reset_move_lines = True
else:
if reserved_qty != round(_get_ml_sum(move_lines), product_uom_accuracy):
reset_quant = True
reset_move_lines = True
warning_string = "does not reflect the move lines reservation"
else:
if any(move_line.product_qty < 0 for move_line in move_lines):
reset_quant = True
reset_move_lines = True
warning_string = "correctly reflects the move lines reservation but some are negatives"
if warning_string:
QUANT_ERRORS += 1
processed_quant_ids.append(quant.id)
if print_detailed_report:
warning += _compute_quant_report(warning_string, quant, move_lines)
if repair:
if reset_quant:
reset_reserved_quantity_on_quant(quant)
if reset_move_lines:
reset_uom_qty_on_move_lines(move_lines)
if ml_ids or product_ids or check_no_parameters_set():
move_lines = env['stock.move.line']
if ml_ids:
move_lines |= move_lines.browse(ml_ids).exists()
if quant_ids:
product_ids |= quants.mapped('product_id')
move_lines_domain = [('product_id.type', '=', 'product'), ('product_qty', '!=', 0), ('id', 'not in', move_line_ids)]
if product_ids:
move_lines_domain.append(('product_id', 'in', product_ids.ids))
move_lines |= move_lines.search(move_lines_domain)
ML_NUMBER += len(move_lines)
move_lines_to_unreserve = []
for move_line in move_lines:
if not move_line.location_id.should_bypass_reservation():
processed_ml_ids.append(move_line.id)
warning.append(f"Problematic move line found: {move_line.id} (reserved_quantity: {move_line.product_qty})")
warning.append("There is no existing quants despite its `reserved_quantity`")
warning.append('******************')
move_lines_to_unreserve.append(move_line.id)
unreserve_move_lines(move_lines_to_unreserve)
prepend = list()
if print_detailed_report:
if not warning:
warning.append('nothing seems wrong')
else:
prepend.append('Decimal Accuracy: %s digits' % precision.digits)
prepend.append(f'Processed {QUANT_NUMBER} quants in total. Found {QUANT_ERRORS} errors')
if repair:
prepend.append(f'Reset {QUANT_RESET} quants in total.')
prepend.append(f'Processed {QUANT_ML_NUMBER} move_lines in quants in total. Found {QUANT_ML_ERRORS} errors')
if repair:
prepend.append(f'Reset {QUANT_ML_RESET} move_lines in quants in total.')
prepend.append(f'Processed {ML_NUMBER} move_lines in total. Found {ML_ERRORS} errors')
if repair:
prepend.append(f'Unreserved {ML_UNRESERVED} move_lines in total.')
prepend.append('\n')
to_output = prepend
if print_detailed_report:
to_output = to_output + warning
if print_ids_list:
to_output = to_output + [f'quant_ids: {quant_ids}'] + [f'ml_ids: {ml_ids}']
for line in to_output:
print(line)
return {'quant_ids': processed_quant_ids, 'move_line_ids': processed_ml_ids}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment