Last active
July 13, 2020 12:03
-
-
Save jev-odoo/b36cbaf094c8b2a6c43444bffc754717 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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