From e28b83603b512bcbbf3c1ec3d491a71d89d2c822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 22 Mar 2016 13:49:48 +0100 Subject: [MOV] stock: barcode, procurement, product, res_* Move models not located in stock.py file and their views: - barcode - procurement - product - product.putaway, product.removal and stock.fixed.putaway.strat are extracted from product.py to their own product_strategy file to separate product and product.template code from stock specific code related to products - res_* - settings - web_planner This commit only moves files. No change is done. --- addons/stock/__init__.py | 14 +- addons/stock/__openerp__.py | 8 +- addons/stock/models/__init__.py | 11 + addons/stock/models/barcode.py | 19 + addons/stock/models/procurement.py | 414 +++++++++++++++ addons/stock/models/product.py | 535 +++++++++++++++++++ addons/stock/models/product_strategy.py | 53 ++ addons/stock/models/res_company.py | 45 ++ addons/stock/models/res_partner.py | 26 + addons/stock/models/stock_config_settings.py | 164 ++++++ addons/stock/models/web_planner.py | 17 + addons/stock/partner.py | 26 - addons/stock/partner_view.xml | 41 -- addons/stock/procurement.py | 414 --------------- addons/stock/product.py | 583 --------------------- addons/stock/product_view.xml | 332 ------------ addons/stock/res_company.py | 45 -- addons/stock/res_config.py | 164 ------ addons/stock/res_config_view.xml | 101 ---- addons/stock/stock.py | 14 - addons/stock/stock_view.xml | 157 ------ addons/stock/views/procurement_views.xml | 101 ++++ addons/stock/views/product_strategy_views.xml | 38 ++ addons/stock/views/product_views.xml | 375 +++++++++++++ addons/stock/views/res_partner_views.xml | 41 ++ addons/stock/views/stock_config_settings_views.xml | 101 ++++ addons/stock/views/stock_menu_views.xml | 20 +- addons/stock/web_planner.py | 17 - 28 files changed, 1952 insertions(+), 1924 deletions(-) create mode 100644 addons/stock/models/__init__.py create mode 100644 addons/stock/models/barcode.py create mode 100644 addons/stock/models/procurement.py create mode 100644 addons/stock/models/product.py create mode 100644 addons/stock/models/product_strategy.py create mode 100644 addons/stock/models/res_company.py create mode 100644 addons/stock/models/res_partner.py create mode 100644 addons/stock/models/stock_config_settings.py create mode 100644 addons/stock/models/web_planner.py delete mode 100644 addons/stock/partner.py delete mode 100644 addons/stock/partner_view.xml delete mode 100644 addons/stock/procurement.py delete mode 100644 addons/stock/product.py delete mode 100644 addons/stock/product_view.xml delete mode 100644 addons/stock/res_company.py delete mode 100644 addons/stock/res_config.py delete mode 100644 addons/stock/res_config_view.xml create mode 100644 addons/stock/views/procurement_views.xml create mode 100644 addons/stock/views/product_strategy_views.xml create mode 100644 addons/stock/views/product_views.xml create mode 100644 addons/stock/views/res_partner_views.xml create mode 100644 addons/stock/views/stock_config_settings_views.xml delete mode 100644 addons/stock/web_planner.py diff --git a/addons/stock/__init__.py b/addons/stock/__init__.py index c2e60b4ca55..95b05d6a96a 100644 --- a/addons/stock/__init__.py +++ b/addons/stock/__init__.py @@ -1,14 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from stock import * -import partner -import product -import procurement -import report -import wizard -import res_config -import res_company -import web_planner - import controllers +import models +import report +import stock +import wizard \ No newline at end of file diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index 67a75e6eb34..124573e3049 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -69,10 +69,12 @@ Dashboard / Reports for Inventory Management will include: 'wizard/stock_immediate_transfer.xml', 'wizard/stock_backorder_confirmation.xml', + 'views/res_partner_views.xml', + 'views/product_strategy_views.xml', 'stock_view.xml', - 'product_view.xml', - 'partner_view.xml', - 'res_config_view.xml', + 'views/procurement_views.xml', + 'views/product_views.xml', + 'views/stock_config_settings_views.xml', 'stock_dashboard.xml', 'data/default_barcode_patterns.xml', diff --git a/addons/stock/models/__init__.py b/addons/stock/models/__init__.py new file mode 100644 index 00000000000..49511e3eba0 --- /dev/null +++ b/addons/stock/models/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import barcode +import product +import procurement +import product_strategy +import res_company +import res_partner +import stock_config_settings +import web_planner \ No newline at end of file diff --git a/addons/stock/models/barcode.py b/addons/stock/models/barcode.py new file mode 100644 index 00000000000..627d2016a46 --- /dev/null +++ b/addons/stock/models/barcode.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models +from odoo.tools.translate import _ + + +class barcode_rule(models.Model): + _inherit = 'barcode.rule' + + def _get_type_selection(self): + types = set(super(barcode_rule, self)._get_type_selection()) + types.update([ + ('weight', _('Weighted Product')), + ('location', _('Location')), + ('lot', _('Lot')), + ('package', _('Package')) + ]) + return list(types) diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py new file mode 100644 index 00000000000..3ffc77bc223 --- /dev/null +++ b/addons/stock/models/procurement.py @@ -0,0 +1,414 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round +from openerp import SUPERUSER_ID + +from collections import defaultdict +from dateutil.relativedelta import relativedelta +from datetime import datetime +from psycopg2 import OperationalError +import openerp + +class procurement_group(osv.osv): + _inherit = 'procurement.group' + _columns = { + 'partner_id': fields.many2one('res.partner', 'Partner') + } + +class procurement_rule(osv.osv): + _inherit = 'procurement.rule' + + def _get_action(self, cr, uid, context=None): + result = super(procurement_rule, self)._get_action(cr, uid, context=context) + return result + [('move', _('Move From Another Location'))] + + def _get_rules(self, cr, uid, ids, context=None): + res = [] + for route in self.browse(cr, uid, ids): + res += [x.id for x in route.pull_ids] + return res + + _columns = { + 'location_id': fields.many2one('stock.location', 'Procurement Location'), + 'location_src_id': fields.many2one('stock.location', 'Source Location', + help="Source location is action=move"), + 'route_id': fields.many2one('stock.location.route', 'Route', + help="If route_id is False, the rule is global"), + 'procure_method': fields.selection([('make_to_stock', 'Take From Stock'), ('make_to_order', 'Create Procurement')], 'Move Supply Method', required=True, + help="""Determines the procurement method of the stock move that will be generated: whether it will need to 'take from the available stock' in its source location or needs to ignore its stock and create a procurement over there."""), + 'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence', + store={ + 'stock.location.route': (_get_rules, ['sequence'], 10), + 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10), + }), + 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', + help="Picking Type determines the way the picking should be shown in the view, reports, ..."), + 'delay': fields.integer('Number of Days'), + 'partner_address_id': fields.many2one('res.partner', 'Partner Address'), + 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too'), + 'warehouse_id': fields.many2one('stock.warehouse', 'Served Warehouse', help='The warehouse this rule is for'), + 'propagate_warehouse_id': fields.many2one('stock.warehouse', 'Warehouse to Propagate', help="The warehouse to propagate on the created move/procurement, which can be different of the warehouse this rule is for (e.g for resupplying rules from another warehouse)"), + } + + _defaults = { + 'procure_method': 'make_to_stock', + 'propagate': True, + 'delay': 0, + } + +class procurement_order(osv.osv): + _inherit = "procurement.order" + _columns = { + 'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with sale_service + 'partner_dest_id': fields.many2one('res.partner', 'Customer Address', help="In case of dropshipping, we need to know the destination address more precisely"), + 'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"), + 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"), + 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Preferred Routes', help="Preferred route to be followed by the procurement order. Usually copied from the generating document (SO) but could be set up manually."), + 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', help="Warehouse to consider for the route selection"), + 'orderpoint_id': fields.many2one('stock.warehouse.orderpoint', 'Minimum Stock Rule'), + } + + def propagate_cancels(self, cr, uid, ids, context=None): + move_cancel = [] + for procurement in self.browse(cr, uid, ids, context=context): + if procurement.rule_id.action == 'move' and procurement.move_ids: + move_cancel += [m.id for m in procurement.move_ids] + if move_cancel: + self.pool.get('stock.move').action_cancel(cr, uid, move_cancel, context=context) + return True + + def cancel(self, cr, uid, ids, context=None): + if context is None: + context = {} + to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context) + ctx = context.copy() + #set the context for the propagation of the procurement cancellation + ctx['cancel_procurement'] = True + self.propagate_cancels(cr, uid, to_cancel_ids, context=ctx) + return super(procurement_order, self).cancel(cr, uid, to_cancel_ids, context=ctx) + + def _find_parent_locations(self, cr, uid, ids, context=None): + procurement = self.browse(cr, uid, ids[0], context=context) + location = procurement.location_id + res = [location.id] + while location.location_id: + location = location.location_id + res.append(location.id) + return res + + def change_warehouse_id(self, cr, uid, ids, warehouse_id, context=None): + if warehouse_id: + warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context) + return {'value': {'location_id': warehouse.lot_stock_id.id}} + return {} + + def _search_suitable_rule(self, cr, uid, ids, domain, context=None): + '''we try to first find a rule among the ones defined on the procurement order group and if none is found, we try on the routes defined for the product, and finally we fallback on the default behavior''' + procurement = self.browse(cr, uid, ids[0], context=context) + pull_obj = self.pool.get('procurement.rule') + warehouse_route_ids = [] + if procurement.warehouse_id: + domain += ['|', ('warehouse_id', '=', procurement.warehouse_id.id), ('warehouse_id', '=', False)] + warehouse_route_ids = [x.id for x in procurement.warehouse_id.route_ids] + product_route_ids = [x.id for x in procurement.product_id.route_ids + procurement.product_id.categ_id.total_route_ids] + procurement_route_ids = [x.id for x in procurement.route_ids] + res = pull_obj.search(cr, uid, domain + [('route_id', 'in', procurement_route_ids)], order='route_sequence, sequence', context=context) + if not res: + res = pull_obj.search(cr, uid, domain + [('route_id', 'in', product_route_ids)], order='route_sequence, sequence', context=context) + if not res: + res = warehouse_route_ids and pull_obj.search(cr, uid, domain + [('route_id', 'in', warehouse_route_ids)], order='route_sequence, sequence', context=context) or [] + if not res: + res = pull_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context) + return res + + def _find_suitable_rule(self, cr, uid, ids, context=None): + procurement = self.browse(cr, uid, ids[0], context=context) + rule_id = super(procurement_order, self)._find_suitable_rule(cr, uid, ids, context=context) + if not rule_id: + #a rule defined on 'Stock' is suitable for a procurement in 'Stock\Bin A' + all_parent_location_ids = procurement._find_parent_locations() + rule_id = procurement._search_suitable_rule([('location_id', 'in', all_parent_location_ids)]) + rule_id = rule_id and rule_id[0] or False + return rule_id + + def _run_move_create(self, cr, uid, ids, context=None): + ''' Returns a dictionary of values that will be used to create a stock move from a procurement. + This function assumes that the given procurement has a rule (action == 'move') set on it. + + :param procurement: browse record + :rtype: dictionary + ''' + procurement = self.browse(cr, uid, ids[0], context=context) + newdate = (datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - relativedelta(days=procurement.rule_id.delay or 0)).strftime('%Y-%m-%d %H:%M:%S') + group_id = False + if procurement.rule_id.group_propagation_option == 'propagate': + group_id = procurement.group_id and procurement.group_id.id or False + elif procurement.rule_id.group_propagation_option == 'fixed': + group_id = procurement.rule_id.group_id and procurement.rule_id.group_id.id or False + #it is possible that we've already got some move done, so check for the done qty and create + #a new move with the correct qty + already_done_qty = 0 + for move in procurement.move_ids: + already_done_qty += move.product_uom_qty if move.state == 'done' else 0 + qty_left = max(procurement.product_qty - already_done_qty, 0) + vals = { + 'name': procurement.name, + 'company_id': procurement.rule_id.company_id.id or procurement.rule_id.location_src_id.company_id.id or procurement.rule_id.location_id.company_id.id or procurement.company_id.id, + 'product_id': procurement.product_id.id, + 'product_uom': procurement.product_uom.id, + 'product_uom_qty': qty_left, + 'partner_id': procurement.rule_id.partner_address_id.id or (procurement.group_id and procurement.group_id.partner_id.id) or False, + 'location_id': procurement.rule_id.location_src_id.id, + 'location_dest_id': procurement.location_id.id, + 'move_dest_id': procurement.move_dest_id and procurement.move_dest_id.id or False, + 'procurement_id': procurement.id, + 'rule_id': procurement.rule_id.id, + 'procure_method': procurement.rule_id.procure_method, + 'origin': procurement.origin, + 'picking_type_id': procurement.rule_id.picking_type_id.id, + 'group_id': group_id, + 'route_ids': [(4, x.id) for x in procurement.route_ids], + 'warehouse_id': procurement.rule_id.propagate_warehouse_id.id or procurement.rule_id.warehouse_id.id, + 'date': newdate, + 'date_expected': newdate, + 'propagate': procurement.rule_id.propagate, + 'priority': procurement.priority, + } + return vals + + def _run(self, cr, uid, ids, context=None): + procurement = self.browse(cr, uid, ids[0], context=context) + if procurement.rule_id and procurement.rule_id.action == 'move': + if not procurement.rule_id.location_src_id: + self.message_post(cr, uid, [procurement.id], body=_('No source location defined!'), context=context) + return False + move_obj = self.pool.get('stock.move') + move_dict = procurement._run_move_create() + #create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) + move_obj.create(cr, SUPERUSER_ID, move_dict, context=context) + return True + return super(procurement_order, self)._run(cr, uid, ids, context=context) + + def run(self, cr, uid, ids, autocommit=False, context=None): + new_ids = [x.id for x in self.browse(cr, uid, ids, context=context) if x.state not in ('running', 'done', 'cancel')] + context = dict(context or {}, procurement_auto_defer=True) #When creating + res = super(procurement_order, self).run(cr, uid, new_ids, autocommit=autocommit, context=context) + + #after all the procurements are run, check if some created a draft stock move that needs to be confirmed + #(we do that in batch because it fasts the picking assignation and the picking state computation) + move_to_confirm_ids = [] + for procurement in self.browse(cr, uid, new_ids, context=context): + if procurement.state == "running" and procurement.rule_id and procurement.rule_id.action == "move": + move_to_confirm_ids += [m.id for m in procurement.move_ids if m.state == 'draft'] + if move_to_confirm_ids: + self.pool.get('stock.move').action_confirm(cr, uid, move_to_confirm_ids, context=context) + # If procurements created other procurements, run the created in batch + procurement_ids = self.search(cr, uid, [('move_dest_id.procurement_id', 'in', new_ids)], order='id', context=context) + if procurement_ids: + res = res and self.run(cr, uid, procurement_ids, autocommit=autocommit, context=context) + return res + + def _check(self, cr, uid, ids, context=None): + ''' Implement the procurement checking for rules of type 'move'. The procurement will be satisfied only if all related + moves are done/cancel and if the requested quantity is moved. + ''' + procurement = self.browse(cr, uid, ids[0], context=context) + if procurement.rule_id and procurement.rule_id.action == 'move': + uom_obj = self.pool.get('product.uom') + # In case Phantom BoM splits only into procurements + if not procurement.move_ids: + return True + cancel_test_list = [x.state == 'cancel' for x in procurement.move_ids] + done_cancel_test_list = [x.state in ('done', 'cancel') for x in procurement.move_ids] + at_least_one_cancel = any(cancel_test_list) + all_done_or_cancel = all(done_cancel_test_list) + all_cancel = all(cancel_test_list) + if not all_done_or_cancel: + return False + elif all_done_or_cancel and not all_cancel: + return True + elif all_cancel: + self.message_post(cr, uid, [procurement.id], body=_('All stock moves have been cancelled for this procurement.'), context=context) + self.write(cr, uid, [procurement.id], {'state': 'cancel'}, context=context) + return False + + return super(procurement_order, self)._check(cr, uid, procurement, context) + + def do_view_pickings(self, cr, uid, ids, context=None): + ''' + This function returns an action that display the pickings of the procurements belonging + to the same procurement group of given ids. + ''' + mod_obj = self.pool.get('ir.model.data') + act_obj = self.pool.get('ir.actions.act_window') + result = mod_obj.get_object_reference(cr, uid, 'stock', 'do_view_pickings') + id = result and result[1] or False + result = act_obj.read(cr, uid, [id], context=context)[0] + group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id]) + result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]" + return result + + def run_scheduler(self, cr, uid, use_new_cursor=False, company_id=False, context=None): + ''' + Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules + and the availability of moves. This function is intended to be run for all the companies at the same time, so + we run functions as SUPERUSER to avoid intercompanies and access rights issues. + + @param self: The object pointer + @param cr: The current row, from the database cursor, + @param uid: The current user ID for security checks + @param ids: List of selected IDs + @param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement. + This is appropriate for batch jobs only. + @param context: A standard dictionary for contextual values + @return: Dictionary of values + ''' + super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, company_id=company_id, context=context) + if context is None: + context = {} + try: + if use_new_cursor: + cr = openerp.registry(cr.dbname).cursor() + + move_obj = self.pool.get('stock.move') + + #Minimum stock rules + self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=use_new_cursor, company_id=company_id, context=context) + + #Search all confirmed stock_moves and try to assign them + confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc', context=context) + for x in xrange(0, len(confirmed_ids), 100): + move_obj.action_assign(cr, uid, confirmed_ids[x:x + 100], context=context) + if use_new_cursor: + cr.commit() + + if use_new_cursor: + cr.commit() + finally: + if use_new_cursor: + try: + cr.close() + except Exception: + pass + return {} + + def _procurement_from_orderpoint_get_order(self, cr, uid, context=None): + return 'location_id' + + def _procurement_from_orderpoint_get_grouping_key(self, cr, uid, orderpoint_ids, context=None): + orderpoint = self.pool['stock.warehouse.orderpoint'].browse(cr, uid, orderpoint_ids[0], context=context) + return orderpoint.location_id.id + + def _procurement_from_orderpoint_get_groups(self, cr, uid, orderpoint_ids, context=None): + """ Make groups for a given orderpoint; by default schedule all operations in one without date """ + return [{'to_date': False, 'procurement_values': dict()}] + + def _procurement_from_orderpoint_post_process(self, cr, uid, orderpoint_ids, context=None): + return True + + def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id=False, context=None): + ''' + Create procurement based on Orderpoint + + :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement. + This is appropriate for batch jobs only. + ''' + if context is None: + context = {} + procurement_context = dict(context, procurement_autorun_defer=True) + if use_new_cursor: + cr = openerp.registry(cr.dbname).cursor() + orderpoint_obj = self.pool.get('stock.warehouse.orderpoint') + procurement_obj = self.pool.get('procurement.order') + product_obj = self.pool.get('product.product') + + orderpoint_ids = orderpoint_obj.search( + cr, uid, + company_id and [('company_id', '=', company_id)] or [], + order=self._procurement_from_orderpoint_get_order(cr, uid, context=context)) + procurement_ids = [] + while orderpoint_ids: + current_ids = orderpoint_ids[:1000] + orderpoint_ids = orderpoint_ids[1000:] + orderpoints = orderpoint_obj.browse(cr, uid, current_ids, context=context) + + # Calculate groups that can be executed together + location_data = defaultdict(lambda: dict(products=list(), orderpoints=list(), groups=list())) + + for orderpoint in orderpoints: + key = self._procurement_from_orderpoint_get_grouping_key(cr, uid, [orderpoint.id], context=context) + location_data[key]['products'] += orderpoint.product_id + location_data[key]['orderpoints'] += orderpoint + location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups(cr, uid, [orderpoint.id], context=context) + + for location_id, location_data in location_data.iteritems(): + location_orderpoints = location_data['orderpoints'] + product_context = dict(context, location=location_orderpoints[0].location_id.id) + substract_quantity = self.pool['stock.warehouse.orderpoint'].subtract_procurements_from_orderpoints( + cr, uid, + [op.id for op in location_orderpoints], + context=context) + + for group in location_data['groups']: + if group['to_date']: + product_context['to_date'] = group['to_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT) + product_quantity = product_obj._product_available(cr, uid, [prod.id for prod in location_data['products']], context=product_context) + for orderpoint in location_orderpoints: + try: + op_product_virtual = product_quantity[orderpoint.product_id.id]['virtual_available'] + if op_product_virtual is None: + continue + if float_compare(op_product_virtual, orderpoint.product_min_qty, precision_rounding=orderpoint.product_uom.rounding) <= 0: + qty = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - op_product_virtual + remainder = orderpoint.qty_multiple > 0 and qty % orderpoint.qty_multiple or 0.0 + + if float_compare(remainder, 0.0, precision_rounding=orderpoint.product_uom.rounding) > 0: + qty += orderpoint.qty_multiple - remainder + + if float_compare(qty, 0.0, precision_rounding=orderpoint.product_uom.rounding) < 0: + continue + + qty -= substract_quantity[orderpoint.id] + qty_rounded = float_round(qty, precision_rounding=orderpoint.product_uom.rounding) + if qty_rounded > 0: + new_procurement_id = procurement_obj.create( + cr, uid, + orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values']), + context=procurement_context) + procurement_ids.append(new_procurement_id) + self._procurement_from_orderpoint_post_process(cr, uid, [orderpoint.id], context=context) + if use_new_cursor: + cr.commit() + + except OperationalError: + if use_new_cursor: + orderpoint_ids += orderpoint.id + cr.rollback() + continue + else: + raise + + try: + procurement_ids.reverse() + self.run(cr, uid, procurement_ids, context=context) + procurement_ids = [] + if use_new_cursor: + cr.commit() + except OperationalError: + if use_new_cursor: + cr.rollback() + continue + else: + raise + + if use_new_cursor: + cr.commit() + + if use_new_cursor: + cr.commit() + cr.close() + return {} diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py new file mode 100644 index 00000000000..91a587fd14a --- /dev/null +++ b/addons/stock/models/product.py @@ -0,0 +1,535 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.osv import fields, osv +from openerp.tools.translate import _ +from openerp.tools.safe_eval import safe_eval as eval +import openerp.addons.decimal_precision as dp +from openerp.tools.float_utils import float_round +from openerp.exceptions import UserError + + +class product_product(osv.osv): + _inherit = "product.product" + + def _stock_move_count(self, cr, uid, ids, field_name, arg, context=None): + res = dict([(id, {'reception_count': 0, 'delivery_count': 0}) for id in ids]) + move_pool=self.pool.get('stock.move') + moves = move_pool.read_group(cr, uid, [ + ('product_id', 'in', ids), + ('location_id.usage', '!=', 'internal'), + ('location_dest_id.usage', '=', 'internal'), + ('state','in',('confirmed','assigned','pending')) + ], ['product_id'], ['product_id']) + for move in moves: + product_id = move['product_id'][0] + res[product_id]['reception_count'] = move['product_id_count'] + moves = move_pool.read_group(cr, uid, [ + ('product_id', 'in', ids), + ('location_id.usage', '=', 'internal'), + ('location_dest_id.usage', '!=', 'internal'), + ('state','in',('confirmed','assigned','pending')) + ], ['product_id'], ['product_id']) + for move in moves: + product_id = move['product_id'][0] + res[product_id]['delivery_count'] = move['product_id_count'] + return res + + def view_header_get(self, cr, user, view_id, view_type, context=None): + if context is None: + context = {} + res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context) + if res: return res + if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'): + return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name + return res + + def _get_domain_locations(self, cr, uid, ids, context=None): + ''' + Parses the context and returns a list of location_ids based on it. + It will return all stock locations when no parameters are given + Possible parameters are shop, warehouse, location, force_company, compute_child + ''' + context = context or {} + + location_obj = self.pool.get('stock.location') + warehouse_obj = self.pool.get('stock.warehouse') + + location_ids = [] + if context.get('location', False): + if isinstance(context['location'], (int, long)): + location_ids = [context['location']] + elif isinstance(context['location'], basestring): + domain = [('complete_name','ilike',context['location'])] + if context.get('force_company', False): + domain += [('company_id', '=', context['force_company'])] + location_ids = location_obj.search(cr, uid, domain, context=context) + else: + location_ids = context['location'] + else: + if context.get('warehouse', False): + if isinstance(context['warehouse'], (int, long)): + wids = [context['warehouse']] + elif isinstance(context['warehouse'], basestring): + domain = [('name', 'ilike', context['warehouse'])] + if context.get('force_company', False): + domain += [('company_id', '=', context['force_company'])] + wids = warehouse_obj.search(cr, uid, domain, context=context) + else: + wids = context['warehouse'] + else: + wids = warehouse_obj.search(cr, uid, [], context=context) + + for w in warehouse_obj.browse(cr, uid, wids, context=context): + location_ids.append(w.view_location_id.id) + + operator = context.get('compute_child', True) and 'child_of' or 'in' + domain = context.get('force_company', False) and ['&', ('company_id', '=', context['force_company'])] or [] + locations = location_obj.browse(cr, uid, location_ids, context=context) + # TDE FIXME: should move the support of child_of + auto_join directly in expression + # The code has been modified because having one location with parent_left being + # 0 make the whole domain unusable + hierarchical_locations = [location for location in locations if location.parent_left != 0 and operator == "child_of"] + other_locations = [location for location in locations if location not in hierarchical_locations] + loc_domain = [] + dest_loc_domain = [] + for location in hierarchical_locations: + loc_domain = loc_domain and ['|'] + loc_domain or loc_domain + loc_domain += ['&', + ('location_id.parent_left', '>=', location.parent_left), + ('location_id.parent_left', '<', location.parent_right)] + dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain + dest_loc_domain += ['&', + ('location_dest_id.parent_left', '>=', location.parent_left), + ('location_dest_id.parent_left', '<', location.parent_right)] + if other_locations: + loc_domain = loc_domain and ['|'] + loc_domain or loc_domain + loc_domain = loc_domain + [('location_id', operator, [location.id for location in other_locations])] + dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain + dest_loc_domain = dest_loc_domain + [('location_dest_id', operator, [location.id for location in other_locations])] + return ( + domain + loc_domain, + domain + ['&'] + dest_loc_domain + ['!'] + loc_domain, + domain + ['&'] + loc_domain + ['!'] + dest_loc_domain + ) + + def _get_domain_dates(self, cr, uid, ids, context): + from_date = context.get('from_date', False) + to_date = context.get('to_date', False) + domain = [] + if from_date: + domain.append(('date', '>=', from_date)) + if to_date: + domain.append(('date', '<=', to_date)) + return domain + + def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None): + context = context or {} + field_names = field_names or [] + + domain_products = [('product_id', 'in', ids)] + domain_quant, domain_move_in, domain_move_out = [], [], [] + domain_quant_loc, domain_move_in_loc, domain_move_out_loc = self._get_domain_locations(cr, uid, ids, context=context) + domain_move_in += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_products + domain_move_out += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_products + domain_quant += domain_products + + if context.get('lot_id'): + domain_quant.append(('lot_id', '=', context['lot_id'])) + if context.get('owner_id'): + domain_quant.append(('owner_id', '=', context['owner_id'])) + owner_domain = ('restrict_partner_id', '=', context['owner_id']) + domain_move_in.append(owner_domain) + domain_move_out.append(owner_domain) + if context.get('package_id'): + domain_quant.append(('package_id', '=', context['package_id'])) + + domain_move_in += domain_move_in_loc + domain_move_out += domain_move_out_loc + moves_in = self.pool.get('stock.move').read_group(cr, uid, domain_move_in, ['product_id', 'product_qty'], ['product_id'], context=context) + moves_out = self.pool.get('stock.move').read_group(cr, uid, domain_move_out, ['product_id', 'product_qty'], ['product_id'], context=context) + + domain_quant += domain_quant_loc + quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context) + quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants)) + + moves_in = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_in)) + moves_out = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_out)) + res = {} + ctx = context.copy() + ctx.update({'prefetch_fields': False}) + for product in self.browse(cr, uid, ids, context=ctx): + id = product.id + qty_available = float_round(quants.get(id, 0.0), precision_rounding=product.uom_id.rounding) + incoming_qty = float_round(moves_in.get(id, 0.0), precision_rounding=product.uom_id.rounding) + outgoing_qty = float_round(moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding) + virtual_available = float_round(quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding) + res[id] = { + 'qty_available': qty_available, + 'incoming_qty': incoming_qty, + 'outgoing_qty': outgoing_qty, + 'virtual_available': virtual_available, + } + return res + + def _search_product_quantity(self, cr, uid, obj, name, domain, context): + res = [] + for field, operator, value in domain: + #to prevent sql injections + assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand' + assert operator in ('<', '>', '=', '!=', '<=', '>='), 'Invalid domain operator' + assert isinstance(value, (float, int)), 'Invalid domain right operand' + + if operator == '=': + operator = '==' + + ids = [] + if name == 'qty_available' and (value != 0.0 or operator not in ('==', '>=', '<=')): + res.append(('id', 'in', self._search_qty_available(cr, uid, operator, value, context))) + else: + product_ids = self.search(cr, uid, [], context=context) + if product_ids: + #TODO: Still optimization possible when searching virtual quantities + for element in self.browse(cr, uid, product_ids, context=context): + if eval(str(element[field]) + operator + str(value)): + ids.append(element.id) + res.append(('id', 'in', ids)) + return res + + def _search_qty_available(self, cr, uid, operator, value, context): + domain_quant = [] + if context.get('lot_id'): + domain_quant.append(('lot_id', '=', context['lot_id'])) + if context.get('owner_id'): + domain_quant.append(('owner_id', '=', context['owner_id'])) + if context.get('package_id'): + domain_quant.append(('package_id', '=', context['package_id'])) + domain_quant += self._get_domain_locations(cr, uid, [], context=context)[0] + quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context) + quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants)) + quants = dict((k, v) for k, v in quants.iteritems() if eval(str(v) + operator + str(value))) + return(list(quants)) + + def _compute_nbr_reordering_rules(self, cr, uid, ids, field_names=None, arg=None, context=None): + res = dict.fromkeys(ids, {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0}) + product_data = self.pool['stock.warehouse.orderpoint'].read_group(cr, uid, [('product_id', 'in', ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id'], context=context) + for data in product_data: + res[data['product_id'][0]]['nbr_reordering_rules'] = int(data['product_id_count']) + res[data['product_id'][0]]['reordering_min_qty'] = data['product_min_qty'] + res[data['product_id'][0]]['reordering_max_qty'] = data['product_max_qty'] + return res + + _columns = { + 'reception_count': fields.function(_stock_move_count, string="Receipt", type='integer', multi='pickings'), + 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'), + 'qty_available': fields.function(_product_available, multi='qty_available', + type='float', digits_compute=dp.get_precision('Product Unit of Measure'), + string='Quantity On Hand', + fnct_search=_search_product_quantity, + help="Current quantity of products.\n" + "In a context with a single Stock Location, this includes " + "goods stored at this Location, or any of its children.\n" + "In a context with a single Warehouse, this includes " + "goods stored in the Stock Location of this Warehouse, or any " + "of its children.\n" + "stored in the Stock Location of the Warehouse of this Shop, " + "or any of its children.\n" + "Otherwise, this includes goods stored in any Stock Location " + "with 'internal' type."), + 'virtual_available': fields.function(_product_available, multi='qty_available', + type='float', digits_compute=dp.get_precision('Product Unit of Measure'), + string='Forecast Quantity', + fnct_search=_search_product_quantity, + help="Forecast quantity (computed as Quantity On Hand " + "- Outgoing + Incoming)\n" + "In a context with a single Stock Location, this includes " + "goods stored in this location, or any of its children.\n" + "In a context with a single Warehouse, this includes " + "goods stored in the Stock Location of this Warehouse, or any " + "of its children.\n" + "Otherwise, this includes goods stored in any Stock Location " + "with 'internal' type."), + 'incoming_qty': fields.function(_product_available, multi='qty_available', + type='float', digits_compute=dp.get_precision('Product Unit of Measure'), + string='Incoming', + fnct_search=_search_product_quantity, + help="Quantity of products that are planned to arrive.\n" + "In a context with a single Stock Location, this includes " + "goods arriving to this Location, or any of its children.\n" + "In a context with a single Warehouse, this includes " + "goods arriving to the Stock Location of this Warehouse, or " + "any of its children.\n" + "Otherwise, this includes goods arriving to any Stock " + "Location with 'internal' type."), + 'outgoing_qty': fields.function(_product_available, multi='qty_available', + type='float', digits_compute=dp.get_precision('Product Unit of Measure'), + string='Outgoing', + fnct_search=_search_product_quantity, + help="Quantity of products that are planned to leave.\n" + "In a context with a single Stock Location, this includes " + "goods leaving this Location, or any of its children.\n" + "In a context with a single Warehouse, this includes " + "goods leaving the Stock Location of this Warehouse, or " + "any of its children.\n" + "Otherwise, this includes goods leaving any Stock " + "Location with 'internal' type."), + 'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules'), + 'nbr_reordering_rules': fields.function(_compute_nbr_reordering_rules, string='Reordering Rules', type='integer', multi=True), + 'reordering_min_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True), + 'reordering_max_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True), + } + + def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): + res = super(product_product, self).fields_view_get( + cr, uid, view_id=view_id, view_type=view_type, context=context, + toolbar=toolbar, submenu=submenu) + if context is None: + context = {} + if context.get('location') and isinstance(context['location'], int): + location_info = self.pool.get('stock.location').browse(cr, uid, context['location']) + fields=res.get('fields',{}) + if fields: + if location_info.usage == 'supplier': + if fields.get('virtual_available'): + res['fields']['virtual_available']['string'] = _('Future Receipts') + if fields.get('qty_available'): + res['fields']['qty_available']['string'] = _('Received Qty') + + if location_info.usage == 'internal': + if fields.get('virtual_available'): + res['fields']['virtual_available']['string'] = _('Forecasted Quantity') + + if location_info.usage == 'customer': + if fields.get('virtual_available'): + res['fields']['virtual_available']['string'] = _('Future Deliveries') + if fields.get('qty_available'): + res['fields']['qty_available']['string'] = _('Delivered Qty') + + if location_info.usage == 'inventory': + if fields.get('virtual_available'): + res['fields']['virtual_available']['string'] = _('Future P&L') + if fields.get('qty_available'): + res['fields']['qty_available']['string'] = _('P&L Qty') + + if location_info.usage == 'procurement': + if fields.get('virtual_available'): + res['fields']['virtual_available']['string'] = _('Future Qty') + if fields.get('qty_available'): + res['fields']['qty_available']['string'] = _('Unplanned Qty') + + if location_info.usage == 'production': + if fields.get('virtual_available'): + res['fields']['virtual_available']['string'] = _('Future Productions') + if fields.get('qty_available'): + res['fields']['qty_available']['string'] = _('Produced Qty') + return res + + + def action_view_routes(self, cr, uid, ids, context=None): + template_obj = self.pool.get("product.template") + templ_ids = list(set([x.product_tmpl_id.id for x in self.browse(cr, uid, ids, context=context)])) + return template_obj.action_view_routes(cr, uid, templ_ids, context=context) + + def onchange_tracking(self, cr, uid, ids, tracking, context=None): + if not tracking or tracking == 'none': + return {} + unassigned_quants = self.pool['stock.quant'].search_count(cr, uid, [('product_id','in', ids), ('lot_id','=', False), ('location_id.usage','=', 'internal')], context=context) + if unassigned_quants: + return {'warning' : { + 'title': _('Warning!'), + 'message' : _("You have products in stock that have no lot number. You can assign serial numbers by doing an inventory. ") + }} + return {} + + +class product_template(osv.osv): + _name = 'product.template' + _inherit = 'product.template' + + def _product_available(self, cr, uid, ids, name, arg, context=None): + prod_available = {} + product_ids = self.browse(cr, uid, ids, context=context) + var_ids = [] + for product in product_ids: + var_ids += [p.id for p in product.product_variant_ids] + variant_available= self.pool['product.product']._product_available(cr, uid, var_ids, context=context) + + for product in product_ids: + qty_available = 0 + virtual_available = 0 + incoming_qty = 0 + outgoing_qty = 0 + for p in product.product_variant_ids: + qty_available += variant_available[p.id]["qty_available"] + virtual_available += variant_available[p.id]["virtual_available"] + incoming_qty += variant_available[p.id]["incoming_qty"] + outgoing_qty += variant_available[p.id]["outgoing_qty"] + prod_available[product.id] = { + "qty_available": qty_available, + "virtual_available": virtual_available, + "incoming_qty": incoming_qty, + "outgoing_qty": outgoing_qty, + } + return prod_available + + def _search_product_quantity(self, cr, uid, obj, name, domain, context): + prod = self.pool.get("product.product") + product_variant_ids = prod.search(cr, uid, domain, context=context) + return [('product_variant_ids', 'in', product_variant_ids)] + + def _compute_nbr_reordering_rules(self, cr, uid, ids, field_names=None, arg=None, context=None): + res = {k: {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0} for k in ids} + product_data = self.pool['stock.warehouse.orderpoint'].read_group(cr, uid, [('product_id.product_tmpl_id', 'in', ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id'], context=context) + for data in product_data: + p = self.pool['product.product'].browse(cr, uid, [data['product_id'][0]], context=context) + product_tmpl_id = p.product_tmpl_id.id + res[product_tmpl_id]['nbr_reordering_rules'] += int(data['product_id_count']) + res[product_tmpl_id]['reordering_min_qty'] = data['product_min_qty'] + res[product_tmpl_id]['reordering_max_qty'] = data['product_max_qty'] + return res + + def _get_product_template_type(self, cr, uid, context=None): + res = super(product_template, self)._get_product_template_type(cr, uid, context=context) + if 'product' not in [item[0] for item in res]: + res.append(('product', _('Stockable Product'))) + return res + + _columns = { + 'property_stock_procurement': fields.property( + type='many2one', + relation='stock.location', + string="Procurement Location", + domain=[('usage','like','procurement')], + help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."), + 'property_stock_production': fields.property( + type='many2one', + relation='stock.location', + string="Production Location", + domain=[('usage','like','production')], + help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."), + 'property_stock_inventory': fields.property( + type='many2one', + relation='stock.location', + string="Inventory Location", + domain=[('usage','like','inventory')], + help="This stock location will be used, instead of the default one, as the source location for stock moves generated when you do an inventory."), + 'sale_delay': fields.float('Customer Lead Time', help="The average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."), + 'tracking': fields.selection(selection=[('serial', 'By Unique Serial Number'), ('lot', 'By Lots'), ('none', 'No Tracking')], string="Tracking", required=True), + 'description_picking': fields.text('Description on Picking', translate=True), + # sum of product variant qty + # 'reception_count': fields.function(_product_available, multi='qty_available', + # fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'), + # 'delivery_count': fields.function(_product_available, multi='qty_available', + # fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'), + 'qty_available': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'), + fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'), + 'virtual_available': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'), + fnct_search=_search_product_quantity, type='float', string='Forecasted Quantity'), + 'incoming_qty': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'), + fnct_search=_search_product_quantity, type='float', string='Incoming'), + 'outgoing_qty': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'), + fnct_search=_search_product_quantity, type='float', string='Outgoing'), + 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'), + 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'), + 'route_ids': fields.many2many('stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes', domain="[('product_selectable', '=', True)]", + help="Depending on the modules installed, this will allow you to define the route of the product: whether it will be bought, manufactured, MTO/MTS,..."), + 'nbr_reordering_rules': fields.function(_compute_nbr_reordering_rules, string='Reordering Rules', type='integer', multi=True), + 'reordering_min_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True), + 'reordering_max_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True), + 'route_from_categ_ids': fields.related('categ_id', 'total_route_ids', type="many2many", relation="stock.location.route", string="Category Routes"), + } + + _defaults = { + 'sale_delay': 0, + 'tracking': 'none', + } + + def action_view_routes(self, cr, uid, ids, context=None): + route_obj = self.pool.get("stock.location.route") + act_obj = self.pool.get('ir.actions.act_window') + mod_obj = self.pool.get('ir.model.data') + product_route_ids = set() + for product in self.browse(cr, uid, ids, context=context): + product_route_ids |= set([r.id for r in product.route_ids]) + product_route_ids |= set([r.id for r in product.categ_id.total_route_ids]) + route_ids = route_obj.search(cr, uid, ['|', ('id', 'in', list(product_route_ids)), ('warehouse_selectable', '=', True)], context=context) + result = mod_obj.xmlid_to_res_id(cr, uid, 'stock.action_routes_form', raise_if_not_found=True) + result = act_obj.read(cr, uid, [result], context=context)[0] + result['domain'] = "[('id','in',[" + ','.join(map(str, route_ids)) + "])]" + return result + + def onchange_tracking(self, cr, uid, ids, tracking, context=None): + if not tracking: + return {} + product_product = self.pool['product.product'] + variant_ids = product_product.search(cr, uid, [('product_tmpl_id', 'in', ids)], context=context) + return product_product.onchange_tracking(cr, uid, variant_ids, tracking, context=context) + + def _get_products(self, cr, uid, ids, context=None): + products = [] + for prodtmpl in self.browse(cr, uid, ids, context=None): + products += [x.id for x in prodtmpl.product_variant_ids] + return products + + def _get_act_window_dict(self, cr, uid, name, context=None): + mod_obj = self.pool.get('ir.model.data') + act_obj = self.pool.get('ir.actions.act_window') + result = mod_obj.xmlid_to_res_id(cr, uid, name, raise_if_not_found=True) + result = act_obj.read(cr, uid, [result], context=context)[0] + return result + + def action_open_quants(self, cr, uid, ids, context=None): + products = self._get_products(cr, uid, ids, context=context) + result = self._get_act_window_dict(cr, uid, 'stock.product_open_quants', context=context) + result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]" + result['context'] = "{'search_default_locationgroup': 1, 'search_default_internal_loc': 1}" + return result + + def action_view_orderpoints(self, cr, uid, ids, context=None): + products = self._get_products(cr, uid, ids, context=context) + result = self._get_act_window_dict(cr, uid, 'stock.product_open_orderpoint', context=context) + if len(ids) == 1 and len(products) == 1: + result['context'] = "{'default_product_id': " + str(products[0]) + ", 'search_default_product_id': " + str(products[0]) + "}" + else: + result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]" + result['context'] = "{}" + return result + + def action_view_stock_moves(self, cr, uid, ids, context=None): + products = self._get_products(cr, uid, ids, context=context) + result = self._get_act_window_dict(cr, uid, 'stock.act_product_stock_move_open', context=context) + if products: + result['context'] = "{'default_product_id': %d}" % products[0] + result['domain'] = "[('product_id.product_tmpl_id','in',[" + ','.join(map(str,ids)) + "])]" + return result + + def write(self, cr, uid, ids, vals, context=None): + if 'uom_id' in vals: + new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_id'], context=context) + for product in self.browse(cr, uid, ids, context=context): + old_uom = product.uom_id + if old_uom != new_uom: + if self.pool.get('stock.move').search(cr, uid, [('product_id', 'in', [x.id for x in product.product_variant_ids]), ('state', '=', 'done')], limit=1, context=context): + raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product.")) + return super(product_template, self).write(cr, uid, ids, vals, context=context) + + +class product_category(osv.osv): + _inherit = 'product.category' + + def calculate_total_routes(self, cr, uid, ids, name, args, context=None): + res = {} + for categ in self.browse(cr, uid, ids, context=context): + categ2 = categ + routes = [x.id for x in categ.route_ids] + while categ2.parent_id: + categ2 = categ2.parent_id + routes += [x.id for x in categ2.route_ids] + res[categ.id] = routes + return res + + _columns = { + 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes', domain="[('product_categ_selectable', '=', True)]"), + 'removal_strategy_id': fields.many2one('product.removal', 'Force Removal Strategy', help="Set a specific removal strategy that will be used regardless of the source location for this product category"), + 'total_route_ids': fields.function(calculate_total_routes, relation='stock.location.route', type='many2many', string='Total routes', readonly=True), + } diff --git a/addons/stock/models/product_strategy.py b/addons/stock/models/product_strategy.py new file mode 100644 index 00000000000..3b8d19e4d40 --- /dev/null +++ b/addons/stock/models/product_strategy.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.osv import fields, osv + + +class product_removal_strategy(osv.osv): + _name = 'product.removal' + _description = 'Removal Strategy' + + _columns = { + 'name': fields.char('Name', required=True), + 'method': fields.char("Method", required=True, help="FIFO, LIFO..."), + } + + +class product_putaway_strategy(osv.osv): + _name = 'product.putaway' + _description = 'Put Away Strategy' + + def _get_putaway_options(self, cr, uid, context=None): + return [('fixed', 'Fixed Location')] + + _columns = { + 'name': fields.char('Name', required=True), + 'method': fields.selection(_get_putaway_options, "Method", required=True), + 'fixed_location_ids': fields.one2many('stock.fixed.putaway.strat', 'putaway_id', 'Fixed Locations Per Product Category', help="When the method is fixed, this location will be used to store the products", copy=True), + } + + _defaults = { + 'method': 'fixed', + } + + def putaway_apply(self, cr, uid, ids, product, context=None): + putaway_strat = self.browse(cr, uid, ids[0], context=context) + if putaway_strat.method == 'fixed': + for strat in putaway_strat.fixed_location_ids: + categ = product.categ_id + while categ: + if strat.category_id.id == categ.id: + return strat.fixed_location_id.id + categ = categ.parent_id + + +class fixed_putaway_strat(osv.osv): + _name = 'stock.fixed.putaway.strat' + _order = 'sequence' + _columns = { + 'putaway_id': fields.many2one('product.putaway', 'Put Away Method', required=True), + 'category_id': fields.many2one('product.category', 'Product Category', required=True), + 'fixed_location_id': fields.many2one('stock.location', 'Location', required=True), + 'sequence': fields.integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list."), + } \ No newline at end of file diff --git a/addons/stock/models/res_company.py b/addons/stock/models/res_company.py new file mode 100644 index 00000000000..37e20b09225 --- /dev/null +++ b/addons/stock/models/res_company.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +class res_company(osv.osv): + _inherit = "res.company" + _columns = { + 'propagation_minimum_delta': fields.integer('Minimum Delta for Propagation of a Date Change on moves linked together'), + 'internal_transit_location_id': fields.many2one('stock.location', 'Internal Transit Location', help="Technical field used for resupply routes between warehouses that belong to this company", on_delete="restrict"), + } + + def create_transit_location(self, cr, uid, company_id, company_name, context=None): + '''Create a transit location with company_id being the given company_id. This is needed + in case of resuply routes between warehouses belonging to the same company, because + we don't want to create accounting entries at that time. + ''' + data_obj = self.pool.get('ir.model.data') + try: + parent_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1] + except: + parent_loc = False + location_vals = { + 'name': _('%s: Transit Location') % company_name, + 'usage': 'transit', + 'company_id': company_id, + 'location_id': parent_loc, + } + location_id = self.pool.get('stock.location').create(cr, uid, location_vals, context=context) + self.write(cr, uid, [company_id], {'internal_transit_location_id': location_id}, context=context) + + def create(self, cr, uid, vals, context=None): + company_id = super(res_company, self).create(cr, uid, vals, context=context) + self.pool['stock.warehouse'].create(cr, uid, { + 'name': vals['name'], + 'code': vals['name'][:5], + 'company_id': company_id, + }, context=context) + self.create_transit_location(cr, uid, company_id, vals['name'], context=context) + return company_id + + _defaults = { + 'propagation_minimum_delta': 1, + } diff --git a/addons/stock/models/res_partner.py b/addons/stock/models/res_partner.py new file mode 100644 index 00000000000..6b424f83deb --- /dev/null +++ b/addons/stock/models/res_partner.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.osv import fields, osv +from openerp.addons.base.res.res_partner import WARNING_MESSAGE, WARNING_HELP + +class res_partner(osv.osv): + _inherit = 'res.partner' + _columns = { + 'property_stock_customer': fields.property( + type='many2one', + relation='stock.location', + string="Customer Location", + help="This stock location will be used, instead of the default one, as the destination location for goods you send to this partner"), + 'property_stock_supplier': fields.property( + type='many2one', + relation='stock.location', + string="Vendor Location", + help="This stock location will be used, instead of the default one, as the source location for goods you receive from the current partner"), + 'picking_warn' : fields.selection(WARNING_MESSAGE, 'Stock Picking', help=WARNING_HELP, required=True), + 'picking_warn_msg' : fields.text('Message for Stock Picking'), + } + + _defaults = { + 'picking_warn': 'no-message', + } diff --git a/addons/stock/models/stock_config_settings.py b/addons/stock/models/stock_config_settings.py new file mode 100644 index 00000000000..565165e2738 --- /dev/null +++ b/addons/stock/models/stock_config_settings.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.osv import fields, osv + +class stock_config_settings(osv.osv_memory): + _name = 'stock.config.settings' + _inherit = 'res.config.settings' + + def set_group_stock_multi_locations(self, cr, uid, ids, context=None): + """ + If we are not in multiple locations, + we can deactivate the internal picking types of the warehouses. + That way, they won't appear in the dashboard. + """ + for obj in self.browse(cr, uid, ids, context=context): + wh_obj = self.pool['stock.warehouse'] + whs = wh_obj.search(cr, uid, [], context=context) + warehouses = wh_obj.browse(cr, uid, whs, context=context) + if obj.group_stock_multi_locations: + # Check inactive picking types and of warehouses make them active (by warehouse) + inttypes = [x.int_type_id.id for x in warehouses if not x.int_type_id.active] + if inttypes: + self.pool['stock.picking.type'].write(cr, uid, inttypes, {'active': True}, context=context) + else: + # Check active internal picking types of warehouses and make them inactive + inttypes = [x.int_type_id.id for x in warehouses if x.int_type_id.active and x.reception_steps == 'one_step' and x.delivery_steps == 'ship_only'] + if inttypes: + self.pool['stock.picking.type'].write(cr, uid, inttypes, {'active': False}, context=context) + return True + + def default_get(self, cr, uid, fields, context=None): + res = super(stock_config_settings, self).default_get(cr, uid, fields, context=context) + if 'warehouse_and_location_usage_level' in fields or not fields: + res['warehouse_and_location_usage_level'] = int(res.get('group_stock_multi_locations', False)) + int(res.get('group_stock_multi_warehouses', False)) + return res + + def onchange_warehouse_and_location_usage_level(self, cr, uid, ids, level, context=None): + return {'value': { + 'group_stock_multi_locations': level > 0, + 'group_stock_multi_warehouses': level > 1, + }} + + _columns = { + 'group_product_variant': fields.selection([ + (0, "No variants on products"), + (1, 'Products can have several attributes, defining variants (Example: size, color,...)') + ], "Product Variants", + help='Work with product variant allows you to define some variant of the same products, an ease the product management in the ecommerce for example', + implied_group='product.group_product_variant'), + 'company_id': fields.many2one('res.company', 'Company', required=True), + 'module_procurement_jit': fields.selection([ + (1, 'Reserve products immediately after the sale order confirmation'), + (0, 'Reserve products manually or based on automatic scheduler') + ], "Procurements", + help="""Allows you to automatically reserve the available + products when confirming a sale order. + This installs the module procurement_jit."""), + 'module_claim_from_delivery': fields.selection([ + (0, 'Do not manage claims'), + (1, 'Allow claims on deliveries') + ], "Claims", + help='Adds a Claim link to the delivery order.\n' + '-This installs the module claim_from_delivery.'), + 'module_product_expiry': fields.selection([ + (0, 'Do not use Expiration Date on serial numbers'), + (1, 'Define Expiration Date on serial numbers') + ], "Expiration Dates", + help="""Track different dates on products and serial numbers. + The following dates can be tracked: + - end of life + - best before date + - removal date + - alert date. + This installs the module product_expiry."""), + 'group_uom': fields.selection([ + (0, 'Products have only one unit of measure (easier)'), + (1, 'Some products may be sold/purchased in different units of measure (advanced)') + ], "Units of Measure", + implied_group='product.group_uom', + help="""Allows you to select and maintain different units of measure for products."""), + 'group_stock_packaging': fields.selection([ + (0, 'Do not manage packaging'), + (1, 'Manage available packaging options per products') + ], "Packaging Methods", + implied_group='product.group_stock_packaging', + help="""Allows you to create and manage your packaging dimensions and types you want to be maintained in your system."""), + 'group_stock_production_lot': fields.selection([ + (0, 'Do not track individual product items'), + (1, 'Track lots or serial numbers') + ], "Lots and Serial Numbers", + implied_group='stock.group_production_lot', + help="""This allows you to assign a lot (or serial number) to the pickings and moves. This can make it possible to know which production lot was sent to a certain client, ..."""), + 'group_stock_tracking_lot': fields.selection([ + (0, 'Do not manage packaging'), + (1, 'Record packages used on packing: pallets, boxes, ...') + ], "Packages", + implied_group='stock.group_tracking_lot', + help="""This allows to manipulate packages. You can put something in, take something from a package, but also move entire packages and put them even in another package. """), + 'group_warning': fields.selection([ + (0, 'All the partners can be used in pickings'), + (1, 'An informative or blocking warning can be set on a partner') + ], "Warning", implied_group='stock.group_warning'), + + 'group_stock_tracking_owner': fields.selection([ + (0, 'All products in your warehouse belong to your company'), + (1, 'Manage consignee stocks (advanced)') + ], "Product Owners", + implied_group='stock.group_tracking_owner', + help="""This way you can receive products attributed to a certain owner. """), + 'group_stock_adv_location': fields.selection([ + (0, 'No automatic routing of products'), + (1, 'Advanced routing of products using rules') + ], "Routes", + implied_group='stock.group_adv_location', + help="""This option supplements the warehouse application by effectively implementing Push and Pull inventory flows through Routes."""), + 'decimal_precision': fields.integer('Decimal precision on weight', help="As an example, a decimal precision of 2 will allow weights like: 9.99 kg, whereas a decimal precision of 4 will allow weights like: 0.0231 kg."), + 'propagation_minimum_delta': fields.related('company_id', 'propagation_minimum_delta', type='integer', string="Minimum days to trigger a propagation of date change in pushed/pull flows."), + 'module_stock_dropshipping': fields.selection([ + (0, 'Suppliers always deliver to your warehouse(s)'), + (1, "Allow suppliers to deliver directly to your customers") + ], "Dropshipping", + help='\nCreates the dropship route and add more complex tests\n' + '-This installs the module stock_dropshipping.'), + 'module_stock_picking_wave': fields.selection([ + (0, 'Manage pickings one at a time'), + (1, 'Manage picking in batch per worker') + ], "Picking Waves", + help='Install the picking wave module which will help you grouping your pickings and processing them in batch'), + 'module_stock_calendar': fields.selection([ + (0, 'Set lead times in calendar days (easy)'), + (1, "Adapt lead times using the suppliers' open days calendars (advanced)") + ], "Minimum Stock Rules", + help='This allows you to handle minimum stock rules differently by the possibility to take into account the purchase and delivery calendars \n-This installs the module stock_calendar.'), + 'module_stock_barcode': fields.boolean("Barcode scanner support"), + 'module_delivery_dhl': fields.boolean("DHL integration"), + 'module_delivery_fedex': fields.boolean("Fedex integration"), + 'module_delivery_temando': fields.boolean("Temando integration"), + 'module_delivery_ups': fields.boolean("UPS integration"), + 'module_delivery_usps': fields.boolean("USPS integration"), + # Warehouse and location usage_level : + 'warehouse_and_location_usage_level': fields.selection([ + (0, 'Manage only 1 Warehouse with only 1 stock location'), + (1, 'Manage only 1 Warehouse, composed by several stock locations'), + (2, 'Manage several Warehouses, each one composed by several stock locations') + ], "Warehouses and Locations usage level"), + 'group_stock_multi_locations': fields.boolean('Manage several stock locations', + implied_group='stock.group_stock_multi_locations'), + 'group_stock_multi_warehouses': fields.boolean('Manage several warehouses', + implied_group='stock.group_stock_multi_warehouses'), + } + + def onchange_adv_location(self, cr, uid, ids, group_stock_adv_location, context=None): + if group_stock_adv_location: + return {'value': {'warehouse_and_location_usage_level': 1}} + return {} + + def _default_company(self, cr, uid, context=None): + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + return user.company_id.id + + _defaults = { + 'company_id': _default_company, + } diff --git a/addons/stock/models/web_planner.py b/addons/stock/models/web_planner.py new file mode 100644 index 00000000000..9662aab8041 --- /dev/null +++ b/addons/stock/models/web_planner.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from openerp import models + + +class PlannerInventory(models.Model): + + _inherit = 'web.planner' + + def _get_planner_application(self): + planner = super(PlannerInventory, self)._get_planner_application() + planner.append(['planner_inventory', 'Inventory Planner']) + return planner + + def _prepare_planner_inventory_data(self): + # TDE note: void method kept (stable version), probably to remove in master + return { + } diff --git a/addons/stock/partner.py b/addons/stock/partner.py deleted file mode 100644 index 6b424f83deb..00000000000 --- a/addons/stock/partner.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -from openerp.addons.base.res.res_partner import WARNING_MESSAGE, WARNING_HELP - -class res_partner(osv.osv): - _inherit = 'res.partner' - _columns = { - 'property_stock_customer': fields.property( - type='many2one', - relation='stock.location', - string="Customer Location", - help="This stock location will be used, instead of the default one, as the destination location for goods you send to this partner"), - 'property_stock_supplier': fields.property( - type='many2one', - relation='stock.location', - string="Vendor Location", - help="This stock location will be used, instead of the default one, as the source location for goods you receive from the current partner"), - 'picking_warn' : fields.selection(WARNING_MESSAGE, 'Stock Picking', help=WARNING_HELP, required=True), - 'picking_warn_msg' : fields.text('Message for Stock Picking'), - } - - _defaults = { - 'picking_warn': 'no-message', - } diff --git a/addons/stock/partner_view.xml b/addons/stock/partner_view.xml deleted file mode 100644 index efb615bc3ef..00000000000 --- a/addons/stock/partner_view.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - res.partner.stock.property.form.inherit - res.partner - - - - - - - - - - - - - - - res.partner.stock.warning - res.partner - - - - - - - - - - - - - - diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py deleted file mode 100644 index 3ffc77bc223..00000000000 --- a/addons/stock/procurement.py +++ /dev/null @@ -1,414 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -from openerp.tools.translate import _ - -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round -from openerp import SUPERUSER_ID - -from collections import defaultdict -from dateutil.relativedelta import relativedelta -from datetime import datetime -from psycopg2 import OperationalError -import openerp - -class procurement_group(osv.osv): - _inherit = 'procurement.group' - _columns = { - 'partner_id': fields.many2one('res.partner', 'Partner') - } - -class procurement_rule(osv.osv): - _inherit = 'procurement.rule' - - def _get_action(self, cr, uid, context=None): - result = super(procurement_rule, self)._get_action(cr, uid, context=context) - return result + [('move', _('Move From Another Location'))] - - def _get_rules(self, cr, uid, ids, context=None): - res = [] - for route in self.browse(cr, uid, ids): - res += [x.id for x in route.pull_ids] - return res - - _columns = { - 'location_id': fields.many2one('stock.location', 'Procurement Location'), - 'location_src_id': fields.many2one('stock.location', 'Source Location', - help="Source location is action=move"), - 'route_id': fields.many2one('stock.location.route', 'Route', - help="If route_id is False, the rule is global"), - 'procure_method': fields.selection([('make_to_stock', 'Take From Stock'), ('make_to_order', 'Create Procurement')], 'Move Supply Method', required=True, - help="""Determines the procurement method of the stock move that will be generated: whether it will need to 'take from the available stock' in its source location or needs to ignore its stock and create a procurement over there."""), - 'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence', - store={ - 'stock.location.route': (_get_rules, ['sequence'], 10), - 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10), - }), - 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', - help="Picking Type determines the way the picking should be shown in the view, reports, ..."), - 'delay': fields.integer('Number of Days'), - 'partner_address_id': fields.many2one('res.partner', 'Partner Address'), - 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too'), - 'warehouse_id': fields.many2one('stock.warehouse', 'Served Warehouse', help='The warehouse this rule is for'), - 'propagate_warehouse_id': fields.many2one('stock.warehouse', 'Warehouse to Propagate', help="The warehouse to propagate on the created move/procurement, which can be different of the warehouse this rule is for (e.g for resupplying rules from another warehouse)"), - } - - _defaults = { - 'procure_method': 'make_to_stock', - 'propagate': True, - 'delay': 0, - } - -class procurement_order(osv.osv): - _inherit = "procurement.order" - _columns = { - 'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with sale_service - 'partner_dest_id': fields.many2one('res.partner', 'Customer Address', help="In case of dropshipping, we need to know the destination address more precisely"), - 'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"), - 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"), - 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Preferred Routes', help="Preferred route to be followed by the procurement order. Usually copied from the generating document (SO) but could be set up manually."), - 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', help="Warehouse to consider for the route selection"), - 'orderpoint_id': fields.many2one('stock.warehouse.orderpoint', 'Minimum Stock Rule'), - } - - def propagate_cancels(self, cr, uid, ids, context=None): - move_cancel = [] - for procurement in self.browse(cr, uid, ids, context=context): - if procurement.rule_id.action == 'move' and procurement.move_ids: - move_cancel += [m.id for m in procurement.move_ids] - if move_cancel: - self.pool.get('stock.move').action_cancel(cr, uid, move_cancel, context=context) - return True - - def cancel(self, cr, uid, ids, context=None): - if context is None: - context = {} - to_cancel_ids = self.get_cancel_ids(cr, uid, ids, context=context) - ctx = context.copy() - #set the context for the propagation of the procurement cancellation - ctx['cancel_procurement'] = True - self.propagate_cancels(cr, uid, to_cancel_ids, context=ctx) - return super(procurement_order, self).cancel(cr, uid, to_cancel_ids, context=ctx) - - def _find_parent_locations(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - location = procurement.location_id - res = [location.id] - while location.location_id: - location = location.location_id - res.append(location.id) - return res - - def change_warehouse_id(self, cr, uid, ids, warehouse_id, context=None): - if warehouse_id: - warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context) - return {'value': {'location_id': warehouse.lot_stock_id.id}} - return {} - - def _search_suitable_rule(self, cr, uid, ids, domain, context=None): - '''we try to first find a rule among the ones defined on the procurement order group and if none is found, we try on the routes defined for the product, and finally we fallback on the default behavior''' - procurement = self.browse(cr, uid, ids[0], context=context) - pull_obj = self.pool.get('procurement.rule') - warehouse_route_ids = [] - if procurement.warehouse_id: - domain += ['|', ('warehouse_id', '=', procurement.warehouse_id.id), ('warehouse_id', '=', False)] - warehouse_route_ids = [x.id for x in procurement.warehouse_id.route_ids] - product_route_ids = [x.id for x in procurement.product_id.route_ids + procurement.product_id.categ_id.total_route_ids] - procurement_route_ids = [x.id for x in procurement.route_ids] - res = pull_obj.search(cr, uid, domain + [('route_id', 'in', procurement_route_ids)], order='route_sequence, sequence', context=context) - if not res: - res = pull_obj.search(cr, uid, domain + [('route_id', 'in', product_route_ids)], order='route_sequence, sequence', context=context) - if not res: - res = warehouse_route_ids and pull_obj.search(cr, uid, domain + [('route_id', 'in', warehouse_route_ids)], order='route_sequence, sequence', context=context) or [] - if not res: - res = pull_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context) - return res - - def _find_suitable_rule(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - rule_id = super(procurement_order, self)._find_suitable_rule(cr, uid, ids, context=context) - if not rule_id: - #a rule defined on 'Stock' is suitable for a procurement in 'Stock\Bin A' - all_parent_location_ids = procurement._find_parent_locations() - rule_id = procurement._search_suitable_rule([('location_id', 'in', all_parent_location_ids)]) - rule_id = rule_id and rule_id[0] or False - return rule_id - - def _run_move_create(self, cr, uid, ids, context=None): - ''' Returns a dictionary of values that will be used to create a stock move from a procurement. - This function assumes that the given procurement has a rule (action == 'move') set on it. - - :param procurement: browse record - :rtype: dictionary - ''' - procurement = self.browse(cr, uid, ids[0], context=context) - newdate = (datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - relativedelta(days=procurement.rule_id.delay or 0)).strftime('%Y-%m-%d %H:%M:%S') - group_id = False - if procurement.rule_id.group_propagation_option == 'propagate': - group_id = procurement.group_id and procurement.group_id.id or False - elif procurement.rule_id.group_propagation_option == 'fixed': - group_id = procurement.rule_id.group_id and procurement.rule_id.group_id.id or False - #it is possible that we've already got some move done, so check for the done qty and create - #a new move with the correct qty - already_done_qty = 0 - for move in procurement.move_ids: - already_done_qty += move.product_uom_qty if move.state == 'done' else 0 - qty_left = max(procurement.product_qty - already_done_qty, 0) - vals = { - 'name': procurement.name, - 'company_id': procurement.rule_id.company_id.id or procurement.rule_id.location_src_id.company_id.id or procurement.rule_id.location_id.company_id.id or procurement.company_id.id, - 'product_id': procurement.product_id.id, - 'product_uom': procurement.product_uom.id, - 'product_uom_qty': qty_left, - 'partner_id': procurement.rule_id.partner_address_id.id or (procurement.group_id and procurement.group_id.partner_id.id) or False, - 'location_id': procurement.rule_id.location_src_id.id, - 'location_dest_id': procurement.location_id.id, - 'move_dest_id': procurement.move_dest_id and procurement.move_dest_id.id or False, - 'procurement_id': procurement.id, - 'rule_id': procurement.rule_id.id, - 'procure_method': procurement.rule_id.procure_method, - 'origin': procurement.origin, - 'picking_type_id': procurement.rule_id.picking_type_id.id, - 'group_id': group_id, - 'route_ids': [(4, x.id) for x in procurement.route_ids], - 'warehouse_id': procurement.rule_id.propagate_warehouse_id.id or procurement.rule_id.warehouse_id.id, - 'date': newdate, - 'date_expected': newdate, - 'propagate': procurement.rule_id.propagate, - 'priority': procurement.priority, - } - return vals - - def _run(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - if procurement.rule_id and procurement.rule_id.action == 'move': - if not procurement.rule_id.location_src_id: - self.message_post(cr, uid, [procurement.id], body=_('No source location defined!'), context=context) - return False - move_obj = self.pool.get('stock.move') - move_dict = procurement._run_move_create() - #create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) - move_obj.create(cr, SUPERUSER_ID, move_dict, context=context) - return True - return super(procurement_order, self)._run(cr, uid, ids, context=context) - - def run(self, cr, uid, ids, autocommit=False, context=None): - new_ids = [x.id for x in self.browse(cr, uid, ids, context=context) if x.state not in ('running', 'done', 'cancel')] - context = dict(context or {}, procurement_auto_defer=True) #When creating - res = super(procurement_order, self).run(cr, uid, new_ids, autocommit=autocommit, context=context) - - #after all the procurements are run, check if some created a draft stock move that needs to be confirmed - #(we do that in batch because it fasts the picking assignation and the picking state computation) - move_to_confirm_ids = [] - for procurement in self.browse(cr, uid, new_ids, context=context): - if procurement.state == "running" and procurement.rule_id and procurement.rule_id.action == "move": - move_to_confirm_ids += [m.id for m in procurement.move_ids if m.state == 'draft'] - if move_to_confirm_ids: - self.pool.get('stock.move').action_confirm(cr, uid, move_to_confirm_ids, context=context) - # If procurements created other procurements, run the created in batch - procurement_ids = self.search(cr, uid, [('move_dest_id.procurement_id', 'in', new_ids)], order='id', context=context) - if procurement_ids: - res = res and self.run(cr, uid, procurement_ids, autocommit=autocommit, context=context) - return res - - def _check(self, cr, uid, ids, context=None): - ''' Implement the procurement checking for rules of type 'move'. The procurement will be satisfied only if all related - moves are done/cancel and if the requested quantity is moved. - ''' - procurement = self.browse(cr, uid, ids[0], context=context) - if procurement.rule_id and procurement.rule_id.action == 'move': - uom_obj = self.pool.get('product.uom') - # In case Phantom BoM splits only into procurements - if not procurement.move_ids: - return True - cancel_test_list = [x.state == 'cancel' for x in procurement.move_ids] - done_cancel_test_list = [x.state in ('done', 'cancel') for x in procurement.move_ids] - at_least_one_cancel = any(cancel_test_list) - all_done_or_cancel = all(done_cancel_test_list) - all_cancel = all(cancel_test_list) - if not all_done_or_cancel: - return False - elif all_done_or_cancel and not all_cancel: - return True - elif all_cancel: - self.message_post(cr, uid, [procurement.id], body=_('All stock moves have been cancelled for this procurement.'), context=context) - self.write(cr, uid, [procurement.id], {'state': 'cancel'}, context=context) - return False - - return super(procurement_order, self)._check(cr, uid, procurement, context) - - def do_view_pickings(self, cr, uid, ids, context=None): - ''' - This function returns an action that display the pickings of the procurements belonging - to the same procurement group of given ids. - ''' - mod_obj = self.pool.get('ir.model.data') - act_obj = self.pool.get('ir.actions.act_window') - result = mod_obj.get_object_reference(cr, uid, 'stock', 'do_view_pickings') - id = result and result[1] or False - result = act_obj.read(cr, uid, [id], context=context)[0] - group_ids = set([proc.group_id.id for proc in self.browse(cr, uid, ids, context=context) if proc.group_id]) - result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]" - return result - - def run_scheduler(self, cr, uid, use_new_cursor=False, company_id=False, context=None): - ''' - Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules - and the availability of moves. This function is intended to be run for all the companies at the same time, so - we run functions as SUPERUSER to avoid intercompanies and access rights issues. - - @param self: The object pointer - @param cr: The current row, from the database cursor, - @param uid: The current user ID for security checks - @param ids: List of selected IDs - @param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement. - This is appropriate for batch jobs only. - @param context: A standard dictionary for contextual values - @return: Dictionary of values - ''' - super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, company_id=company_id, context=context) - if context is None: - context = {} - try: - if use_new_cursor: - cr = openerp.registry(cr.dbname).cursor() - - move_obj = self.pool.get('stock.move') - - #Minimum stock rules - self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=use_new_cursor, company_id=company_id, context=context) - - #Search all confirmed stock_moves and try to assign them - confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc', context=context) - for x in xrange(0, len(confirmed_ids), 100): - move_obj.action_assign(cr, uid, confirmed_ids[x:x + 100], context=context) - if use_new_cursor: - cr.commit() - - if use_new_cursor: - cr.commit() - finally: - if use_new_cursor: - try: - cr.close() - except Exception: - pass - return {} - - def _procurement_from_orderpoint_get_order(self, cr, uid, context=None): - return 'location_id' - - def _procurement_from_orderpoint_get_grouping_key(self, cr, uid, orderpoint_ids, context=None): - orderpoint = self.pool['stock.warehouse.orderpoint'].browse(cr, uid, orderpoint_ids[0], context=context) - return orderpoint.location_id.id - - def _procurement_from_orderpoint_get_groups(self, cr, uid, orderpoint_ids, context=None): - """ Make groups for a given orderpoint; by default schedule all operations in one without date """ - return [{'to_date': False, 'procurement_values': dict()}] - - def _procurement_from_orderpoint_post_process(self, cr, uid, orderpoint_ids, context=None): - return True - - def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id=False, context=None): - ''' - Create procurement based on Orderpoint - - :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement. - This is appropriate for batch jobs only. - ''' - if context is None: - context = {} - procurement_context = dict(context, procurement_autorun_defer=True) - if use_new_cursor: - cr = openerp.registry(cr.dbname).cursor() - orderpoint_obj = self.pool.get('stock.warehouse.orderpoint') - procurement_obj = self.pool.get('procurement.order') - product_obj = self.pool.get('product.product') - - orderpoint_ids = orderpoint_obj.search( - cr, uid, - company_id and [('company_id', '=', company_id)] or [], - order=self._procurement_from_orderpoint_get_order(cr, uid, context=context)) - procurement_ids = [] - while orderpoint_ids: - current_ids = orderpoint_ids[:1000] - orderpoint_ids = orderpoint_ids[1000:] - orderpoints = orderpoint_obj.browse(cr, uid, current_ids, context=context) - - # Calculate groups that can be executed together - location_data = defaultdict(lambda: dict(products=list(), orderpoints=list(), groups=list())) - - for orderpoint in orderpoints: - key = self._procurement_from_orderpoint_get_grouping_key(cr, uid, [orderpoint.id], context=context) - location_data[key]['products'] += orderpoint.product_id - location_data[key]['orderpoints'] += orderpoint - location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups(cr, uid, [orderpoint.id], context=context) - - for location_id, location_data in location_data.iteritems(): - location_orderpoints = location_data['orderpoints'] - product_context = dict(context, location=location_orderpoints[0].location_id.id) - substract_quantity = self.pool['stock.warehouse.orderpoint'].subtract_procurements_from_orderpoints( - cr, uid, - [op.id for op in location_orderpoints], - context=context) - - for group in location_data['groups']: - if group['to_date']: - product_context['to_date'] = group['to_date'].strftime(DEFAULT_SERVER_DATETIME_FORMAT) - product_quantity = product_obj._product_available(cr, uid, [prod.id for prod in location_data['products']], context=product_context) - for orderpoint in location_orderpoints: - try: - op_product_virtual = product_quantity[orderpoint.product_id.id]['virtual_available'] - if op_product_virtual is None: - continue - if float_compare(op_product_virtual, orderpoint.product_min_qty, precision_rounding=orderpoint.product_uom.rounding) <= 0: - qty = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - op_product_virtual - remainder = orderpoint.qty_multiple > 0 and qty % orderpoint.qty_multiple or 0.0 - - if float_compare(remainder, 0.0, precision_rounding=orderpoint.product_uom.rounding) > 0: - qty += orderpoint.qty_multiple - remainder - - if float_compare(qty, 0.0, precision_rounding=orderpoint.product_uom.rounding) < 0: - continue - - qty -= substract_quantity[orderpoint.id] - qty_rounded = float_round(qty, precision_rounding=orderpoint.product_uom.rounding) - if qty_rounded > 0: - new_procurement_id = procurement_obj.create( - cr, uid, - orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values']), - context=procurement_context) - procurement_ids.append(new_procurement_id) - self._procurement_from_orderpoint_post_process(cr, uid, [orderpoint.id], context=context) - if use_new_cursor: - cr.commit() - - except OperationalError: - if use_new_cursor: - orderpoint_ids += orderpoint.id - cr.rollback() - continue - else: - raise - - try: - procurement_ids.reverse() - self.run(cr, uid, procurement_ids, context=context) - procurement_ids = [] - if use_new_cursor: - cr.commit() - except OperationalError: - if use_new_cursor: - cr.rollback() - continue - else: - raise - - if use_new_cursor: - cr.commit() - - if use_new_cursor: - cr.commit() - cr.close() - return {} diff --git a/addons/stock/product.py b/addons/stock/product.py deleted file mode 100644 index 791e840a603..00000000000 --- a/addons/stock/product.py +++ /dev/null @@ -1,583 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp.tools.safe_eval import safe_eval as eval -import openerp.addons.decimal_precision as dp -from openerp.tools.float_utils import float_round -from openerp.exceptions import UserError - -class product_product(osv.osv): - _inherit = "product.product" - - def _stock_move_count(self, cr, uid, ids, field_name, arg, context=None): - res = dict([(id, {'reception_count': 0, 'delivery_count': 0}) for id in ids]) - move_pool=self.pool.get('stock.move') - moves = move_pool.read_group(cr, uid, [ - ('product_id', 'in', ids), - ('location_id.usage', '!=', 'internal'), - ('location_dest_id.usage', '=', 'internal'), - ('state','in',('confirmed','assigned','pending')) - ], ['product_id'], ['product_id']) - for move in moves: - product_id = move['product_id'][0] - res[product_id]['reception_count'] = move['product_id_count'] - moves = move_pool.read_group(cr, uid, [ - ('product_id', 'in', ids), - ('location_id.usage', '=', 'internal'), - ('location_dest_id.usage', '!=', 'internal'), - ('state','in',('confirmed','assigned','pending')) - ], ['product_id'], ['product_id']) - for move in moves: - product_id = move['product_id'][0] - res[product_id]['delivery_count'] = move['product_id_count'] - return res - - def view_header_get(self, cr, user, view_id, view_type, context=None): - if context is None: - context = {} - res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context) - if res: return res - if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'): - return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name - return res - - def _get_domain_locations(self, cr, uid, ids, context=None): - ''' - Parses the context and returns a list of location_ids based on it. - It will return all stock locations when no parameters are given - Possible parameters are shop, warehouse, location, force_company, compute_child - ''' - context = context or {} - - location_obj = self.pool.get('stock.location') - warehouse_obj = self.pool.get('stock.warehouse') - - location_ids = [] - if context.get('location', False): - if isinstance(context['location'], (int, long)): - location_ids = [context['location']] - elif isinstance(context['location'], basestring): - domain = [('complete_name','ilike',context['location'])] - if context.get('force_company', False): - domain += [('company_id', '=', context['force_company'])] - location_ids = location_obj.search(cr, uid, domain, context=context) - else: - location_ids = context['location'] - else: - if context.get('warehouse', False): - if isinstance(context['warehouse'], (int, long)): - wids = [context['warehouse']] - elif isinstance(context['warehouse'], basestring): - domain = [('name', 'ilike', context['warehouse'])] - if context.get('force_company', False): - domain += [('company_id', '=', context['force_company'])] - wids = warehouse_obj.search(cr, uid, domain, context=context) - else: - wids = context['warehouse'] - else: - wids = warehouse_obj.search(cr, uid, [], context=context) - - for w in warehouse_obj.browse(cr, uid, wids, context=context): - location_ids.append(w.view_location_id.id) - - operator = context.get('compute_child', True) and 'child_of' or 'in' - domain = context.get('force_company', False) and ['&', ('company_id', '=', context['force_company'])] or [] - locations = location_obj.browse(cr, uid, location_ids, context=context) - # TDE FIXME: should move the support of child_of + auto_join directly in expression - # The code has been modified because having one location with parent_left being - # 0 make the whole domain unusable - hierarchical_locations = [location for location in locations if location.parent_left != 0 and operator == "child_of"] - other_locations = [location for location in locations if location not in hierarchical_locations] - loc_domain = [] - dest_loc_domain = [] - for location in hierarchical_locations: - loc_domain = loc_domain and ['|'] + loc_domain or loc_domain - loc_domain += ['&', - ('location_id.parent_left', '>=', location.parent_left), - ('location_id.parent_left', '<', location.parent_right)] - dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain - dest_loc_domain += ['&', - ('location_dest_id.parent_left', '>=', location.parent_left), - ('location_dest_id.parent_left', '<', location.parent_right)] - if other_locations: - loc_domain = loc_domain and ['|'] + loc_domain or loc_domain - loc_domain = loc_domain + [('location_id', operator, [location.id for location in other_locations])] - dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain - dest_loc_domain = dest_loc_domain + [('location_dest_id', operator, [location.id for location in other_locations])] - return ( - domain + loc_domain, - domain + ['&'] + dest_loc_domain + ['!'] + loc_domain, - domain + ['&'] + loc_domain + ['!'] + dest_loc_domain - ) - - def _get_domain_dates(self, cr, uid, ids, context): - from_date = context.get('from_date', False) - to_date = context.get('to_date', False) - domain = [] - if from_date: - domain.append(('date', '>=', from_date)) - if to_date: - domain.append(('date', '<=', to_date)) - return domain - - def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None): - context = context or {} - field_names = field_names or [] - - domain_products = [('product_id', 'in', ids)] - domain_quant, domain_move_in, domain_move_out = [], [], [] - domain_quant_loc, domain_move_in_loc, domain_move_out_loc = self._get_domain_locations(cr, uid, ids, context=context) - domain_move_in += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_products - domain_move_out += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_products - domain_quant += domain_products - - if context.get('lot_id'): - domain_quant.append(('lot_id', '=', context['lot_id'])) - if context.get('owner_id'): - domain_quant.append(('owner_id', '=', context['owner_id'])) - owner_domain = ('restrict_partner_id', '=', context['owner_id']) - domain_move_in.append(owner_domain) - domain_move_out.append(owner_domain) - if context.get('package_id'): - domain_quant.append(('package_id', '=', context['package_id'])) - - domain_move_in += domain_move_in_loc - domain_move_out += domain_move_out_loc - moves_in = self.pool.get('stock.move').read_group(cr, uid, domain_move_in, ['product_id', 'product_qty'], ['product_id'], context=context) - moves_out = self.pool.get('stock.move').read_group(cr, uid, domain_move_out, ['product_id', 'product_qty'], ['product_id'], context=context) - - domain_quant += domain_quant_loc - quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context) - quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants)) - - moves_in = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_in)) - moves_out = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_out)) - res = {} - ctx = context.copy() - ctx.update({'prefetch_fields': False}) - for product in self.browse(cr, uid, ids, context=ctx): - id = product.id - qty_available = float_round(quants.get(id, 0.0), precision_rounding=product.uom_id.rounding) - incoming_qty = float_round(moves_in.get(id, 0.0), precision_rounding=product.uom_id.rounding) - outgoing_qty = float_round(moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding) - virtual_available = float_round(quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding) - res[id] = { - 'qty_available': qty_available, - 'incoming_qty': incoming_qty, - 'outgoing_qty': outgoing_qty, - 'virtual_available': virtual_available, - } - return res - - def _search_product_quantity(self, cr, uid, obj, name, domain, context): - res = [] - for field, operator, value in domain: - #to prevent sql injections - assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand' - assert operator in ('<', '>', '=', '!=', '<=', '>='), 'Invalid domain operator' - assert isinstance(value, (float, int)), 'Invalid domain right operand' - - if operator == '=': - operator = '==' - - ids = [] - if name == 'qty_available' and (value != 0.0 or operator not in ('==', '>=', '<=')): - res.append(('id', 'in', self._search_qty_available(cr, uid, operator, value, context))) - else: - product_ids = self.search(cr, uid, [], context=context) - if product_ids: - #TODO: Still optimization possible when searching virtual quantities - for element in self.browse(cr, uid, product_ids, context=context): - if eval(str(element[field]) + operator + str(value)): - ids.append(element.id) - res.append(('id', 'in', ids)) - return res - - def _search_qty_available(self, cr, uid, operator, value, context): - domain_quant = [] - if context.get('lot_id'): - domain_quant.append(('lot_id', '=', context['lot_id'])) - if context.get('owner_id'): - domain_quant.append(('owner_id', '=', context['owner_id'])) - if context.get('package_id'): - domain_quant.append(('package_id', '=', context['package_id'])) - domain_quant += self._get_domain_locations(cr, uid, [], context=context)[0] - quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context) - quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants)) - quants = dict((k, v) for k, v in quants.iteritems() if eval(str(v) + operator + str(value))) - return(list(quants)) - - def _compute_nbr_reordering_rules(self, cr, uid, ids, field_names=None, arg=None, context=None): - res = dict.fromkeys(ids, {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0}) - product_data = self.pool['stock.warehouse.orderpoint'].read_group(cr, uid, [('product_id', 'in', ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id'], context=context) - for data in product_data: - res[data['product_id'][0]]['nbr_reordering_rules'] = int(data['product_id_count']) - res[data['product_id'][0]]['reordering_min_qty'] = data['product_min_qty'] - res[data['product_id'][0]]['reordering_max_qty'] = data['product_max_qty'] - return res - - _columns = { - 'reception_count': fields.function(_stock_move_count, string="Receipt", type='integer', multi='pickings'), - 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'), - 'qty_available': fields.function(_product_available, multi='qty_available', - type='float', digits_compute=dp.get_precision('Product Unit of Measure'), - string='Quantity On Hand', - fnct_search=_search_product_quantity, - help="Current quantity of products.\n" - "In a context with a single Stock Location, this includes " - "goods stored at this Location, or any of its children.\n" - "In a context with a single Warehouse, this includes " - "goods stored in the Stock Location of this Warehouse, or any " - "of its children.\n" - "stored in the Stock Location of the Warehouse of this Shop, " - "or any of its children.\n" - "Otherwise, this includes goods stored in any Stock Location " - "with 'internal' type."), - 'virtual_available': fields.function(_product_available, multi='qty_available', - type='float', digits_compute=dp.get_precision('Product Unit of Measure'), - string='Forecast Quantity', - fnct_search=_search_product_quantity, - help="Forecast quantity (computed as Quantity On Hand " - "- Outgoing + Incoming)\n" - "In a context with a single Stock Location, this includes " - "goods stored in this location, or any of its children.\n" - "In a context with a single Warehouse, this includes " - "goods stored in the Stock Location of this Warehouse, or any " - "of its children.\n" - "Otherwise, this includes goods stored in any Stock Location " - "with 'internal' type."), - 'incoming_qty': fields.function(_product_available, multi='qty_available', - type='float', digits_compute=dp.get_precision('Product Unit of Measure'), - string='Incoming', - fnct_search=_search_product_quantity, - help="Quantity of products that are planned to arrive.\n" - "In a context with a single Stock Location, this includes " - "goods arriving to this Location, or any of its children.\n" - "In a context with a single Warehouse, this includes " - "goods arriving to the Stock Location of this Warehouse, or " - "any of its children.\n" - "Otherwise, this includes goods arriving to any Stock " - "Location with 'internal' type."), - 'outgoing_qty': fields.function(_product_available, multi='qty_available', - type='float', digits_compute=dp.get_precision('Product Unit of Measure'), - string='Outgoing', - fnct_search=_search_product_quantity, - help="Quantity of products that are planned to leave.\n" - "In a context with a single Stock Location, this includes " - "goods leaving this Location, or any of its children.\n" - "In a context with a single Warehouse, this includes " - "goods leaving the Stock Location of this Warehouse, or " - "any of its children.\n" - "Otherwise, this includes goods leaving any Stock " - "Location with 'internal' type."), - 'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules'), - 'nbr_reordering_rules': fields.function(_compute_nbr_reordering_rules, string='Reordering Rules', type='integer', multi=True), - 'reordering_min_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True), - 'reordering_max_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True), - } - - def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): - res = super(product_product, self).fields_view_get( - cr, uid, view_id=view_id, view_type=view_type, context=context, - toolbar=toolbar, submenu=submenu) - if context is None: - context = {} - if context.get('location') and isinstance(context['location'], int): - location_info = self.pool.get('stock.location').browse(cr, uid, context['location']) - fields=res.get('fields',{}) - if fields: - if location_info.usage == 'supplier': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future Receipts') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('Received Qty') - - if location_info.usage == 'internal': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Forecasted Quantity') - - if location_info.usage == 'customer': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future Deliveries') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('Delivered Qty') - - if location_info.usage == 'inventory': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future P&L') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('P&L Qty') - - if location_info.usage == 'procurement': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future Qty') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('Unplanned Qty') - - if location_info.usage == 'production': - if fields.get('virtual_available'): - res['fields']['virtual_available']['string'] = _('Future Productions') - if fields.get('qty_available'): - res['fields']['qty_available']['string'] = _('Produced Qty') - return res - - - def action_view_routes(self, cr, uid, ids, context=None): - template_obj = self.pool.get("product.template") - templ_ids = list(set([x.product_tmpl_id.id for x in self.browse(cr, uid, ids, context=context)])) - return template_obj.action_view_routes(cr, uid, templ_ids, context=context) - - def onchange_tracking(self, cr, uid, ids, tracking, context=None): - if not tracking or tracking == 'none': - return {} - unassigned_quants = self.pool['stock.quant'].search_count(cr, uid, [('product_id','in', ids), ('lot_id','=', False), ('location_id.usage','=', 'internal')], context=context) - if unassigned_quants: - return {'warning' : { - 'title': _('Warning!'), - 'message' : _("You have products in stock that have no lot number. You can assign serial numbers by doing an inventory. ") - }} - return {} - - -class product_template(osv.osv): - _name = 'product.template' - _inherit = 'product.template' - - def _product_available(self, cr, uid, ids, name, arg, context=None): - prod_available = {} - product_ids = self.browse(cr, uid, ids, context=context) - var_ids = [] - for product in product_ids: - var_ids += [p.id for p in product.product_variant_ids] - variant_available= self.pool['product.product']._product_available(cr, uid, var_ids, context=context) - - for product in product_ids: - qty_available = 0 - virtual_available = 0 - incoming_qty = 0 - outgoing_qty = 0 - for p in product.product_variant_ids: - qty_available += variant_available[p.id]["qty_available"] - virtual_available += variant_available[p.id]["virtual_available"] - incoming_qty += variant_available[p.id]["incoming_qty"] - outgoing_qty += variant_available[p.id]["outgoing_qty"] - prod_available[product.id] = { - "qty_available": qty_available, - "virtual_available": virtual_available, - "incoming_qty": incoming_qty, - "outgoing_qty": outgoing_qty, - } - return prod_available - - def _search_product_quantity(self, cr, uid, obj, name, domain, context): - prod = self.pool.get("product.product") - product_variant_ids = prod.search(cr, uid, domain, context=context) - return [('product_variant_ids', 'in', product_variant_ids)] - - def _compute_nbr_reordering_rules(self, cr, uid, ids, field_names=None, arg=None, context=None): - res = {k: {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0} for k in ids} - product_data = self.pool['stock.warehouse.orderpoint'].read_group(cr, uid, [('product_id.product_tmpl_id', 'in', ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id'], context=context) - for data in product_data: - p = self.pool['product.product'].browse(cr, uid, [data['product_id'][0]], context=context) - product_tmpl_id = p.product_tmpl_id.id - res[product_tmpl_id]['nbr_reordering_rules'] += int(data['product_id_count']) - res[product_tmpl_id]['reordering_min_qty'] = data['product_min_qty'] - res[product_tmpl_id]['reordering_max_qty'] = data['product_max_qty'] - return res - - def _get_product_template_type(self, cr, uid, context=None): - res = super(product_template, self)._get_product_template_type(cr, uid, context=context) - if 'product' not in [item[0] for item in res]: - res.append(('product', _('Stockable Product'))) - return res - - _columns = { - 'property_stock_procurement': fields.property( - type='many2one', - relation='stock.location', - string="Procurement Location", - domain=[('usage','like','procurement')], - help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."), - 'property_stock_production': fields.property( - type='many2one', - relation='stock.location', - string="Production Location", - domain=[('usage','like','production')], - help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."), - 'property_stock_inventory': fields.property( - type='many2one', - relation='stock.location', - string="Inventory Location", - domain=[('usage','like','inventory')], - help="This stock location will be used, instead of the default one, as the source location for stock moves generated when you do an inventory."), - 'sale_delay': fields.float('Customer Lead Time', help="The average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."), - 'tracking': fields.selection(selection=[('serial', 'By Unique Serial Number'), ('lot', 'By Lots'), ('none', 'No Tracking')], string="Tracking", required=True), - 'description_picking': fields.text('Description on Picking', translate=True), - # sum of product variant qty - # 'reception_count': fields.function(_product_available, multi='qty_available', - # fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'), - # 'delivery_count': fields.function(_product_available, multi='qty_available', - # fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'), - 'qty_available': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'), - fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'), - 'virtual_available': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'), - fnct_search=_search_product_quantity, type='float', string='Forecasted Quantity'), - 'incoming_qty': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'), - fnct_search=_search_product_quantity, type='float', string='Incoming'), - 'outgoing_qty': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'), - fnct_search=_search_product_quantity, type='float', string='Outgoing'), - 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'), - 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'), - 'route_ids': fields.many2many('stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes', domain="[('product_selectable', '=', True)]", - help="Depending on the modules installed, this will allow you to define the route of the product: whether it will be bought, manufactured, MTO/MTS,..."), - 'nbr_reordering_rules': fields.function(_compute_nbr_reordering_rules, string='Reordering Rules', type='integer', multi=True), - 'reordering_min_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True), - 'reordering_max_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True), - 'route_from_categ_ids': fields.related('categ_id', 'total_route_ids', type="many2many", relation="stock.location.route", string="Category Routes"), - } - - _defaults = { - 'sale_delay': 0, - 'tracking': 'none', - } - - def action_view_routes(self, cr, uid, ids, context=None): - route_obj = self.pool.get("stock.location.route") - act_obj = self.pool.get('ir.actions.act_window') - mod_obj = self.pool.get('ir.model.data') - product_route_ids = set() - for product in self.browse(cr, uid, ids, context=context): - product_route_ids |= set([r.id for r in product.route_ids]) - product_route_ids |= set([r.id for r in product.categ_id.total_route_ids]) - route_ids = route_obj.search(cr, uid, ['|', ('id', 'in', list(product_route_ids)), ('warehouse_selectable', '=', True)], context=context) - result = mod_obj.xmlid_to_res_id(cr, uid, 'stock.action_routes_form', raise_if_not_found=True) - result = act_obj.read(cr, uid, [result], context=context)[0] - result['domain'] = "[('id','in',[" + ','.join(map(str, route_ids)) + "])]" - return result - - def onchange_tracking(self, cr, uid, ids, tracking, context=None): - if not tracking: - return {} - product_product = self.pool['product.product'] - variant_ids = product_product.search(cr, uid, [('product_tmpl_id', 'in', ids)], context=context) - return product_product.onchange_tracking(cr, uid, variant_ids, tracking, context=context) - - def _get_products(self, cr, uid, ids, context=None): - products = [] - for prodtmpl in self.browse(cr, uid, ids, context=None): - products += [x.id for x in prodtmpl.product_variant_ids] - return products - - def _get_act_window_dict(self, cr, uid, name, context=None): - mod_obj = self.pool.get('ir.model.data') - act_obj = self.pool.get('ir.actions.act_window') - result = mod_obj.xmlid_to_res_id(cr, uid, name, raise_if_not_found=True) - result = act_obj.read(cr, uid, [result], context=context)[0] - return result - - def action_open_quants(self, cr, uid, ids, context=None): - products = self._get_products(cr, uid, ids, context=context) - result = self._get_act_window_dict(cr, uid, 'stock.product_open_quants', context=context) - result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]" - result['context'] = "{'search_default_locationgroup': 1, 'search_default_internal_loc': 1}" - return result - - def action_view_orderpoints(self, cr, uid, ids, context=None): - products = self._get_products(cr, uid, ids, context=context) - result = self._get_act_window_dict(cr, uid, 'stock.product_open_orderpoint', context=context) - if len(ids) == 1 and len(products) == 1: - result['context'] = "{'default_product_id': " + str(products[0]) + ", 'search_default_product_id': " + str(products[0]) + "}" - else: - result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]" - result['context'] = "{}" - return result - - def action_view_stock_moves(self, cr, uid, ids, context=None): - products = self._get_products(cr, uid, ids, context=context) - result = self._get_act_window_dict(cr, uid, 'stock.act_product_stock_move_open', context=context) - if products: - result['context'] = "{'default_product_id': %d}" % products[0] - result['domain'] = "[('product_id.product_tmpl_id','in',[" + ','.join(map(str,ids)) + "])]" - return result - - def write(self, cr, uid, ids, vals, context=None): - if 'uom_id' in vals: - new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_id'], context=context) - for product in self.browse(cr, uid, ids, context=context): - old_uom = product.uom_id - if old_uom != new_uom: - if self.pool.get('stock.move').search(cr, uid, [('product_id', 'in', [x.id for x in product.product_variant_ids]), ('state', '=', 'done')], limit=1, context=context): - raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product.")) - return super(product_template, self).write(cr, uid, ids, vals, context=context) - - -class product_removal_strategy(osv.osv): - _name = 'product.removal' - _description = 'Removal Strategy' - - _columns = { - 'name': fields.char('Name', required=True), - 'method': fields.char("Method", required=True, help="FIFO, LIFO..."), - } - - -class product_putaway_strategy(osv.osv): - _name = 'product.putaway' - _description = 'Put Away Strategy' - - def _get_putaway_options(self, cr, uid, context=None): - return [('fixed', 'Fixed Location')] - - _columns = { - 'name': fields.char('Name', required=True), - 'method': fields.selection(_get_putaway_options, "Method", required=True), - 'fixed_location_ids': fields.one2many('stock.fixed.putaway.strat', 'putaway_id', 'Fixed Locations Per Product Category', help="When the method is fixed, this location will be used to store the products", copy=True), - } - - _defaults = { - 'method': 'fixed', - } - - def putaway_apply(self, cr, uid, ids, product, context=None): - putaway_strat = self.browse(cr, uid, ids[0], context=context) - if putaway_strat.method == 'fixed': - for strat in putaway_strat.fixed_location_ids: - categ = product.categ_id - while categ: - if strat.category_id.id == categ.id: - return strat.fixed_location_id.id - categ = categ.parent_id - - -class fixed_putaway_strat(osv.osv): - _name = 'stock.fixed.putaway.strat' - _order = 'sequence' - _columns = { - 'putaway_id': fields.many2one('product.putaway', 'Put Away Method', required=True), - 'category_id': fields.many2one('product.category', 'Product Category', required=True), - 'fixed_location_id': fields.many2one('stock.location', 'Location', required=True), - 'sequence': fields.integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list."), - } - - -class product_category(osv.osv): - _inherit = 'product.category' - - def calculate_total_routes(self, cr, uid, ids, name, args, context=None): - res = {} - for categ in self.browse(cr, uid, ids, context=context): - categ2 = categ - routes = [x.id for x in categ.route_ids] - while categ2.parent_id: - categ2 = categ2.parent_id - routes += [x.id for x in categ2.route_ids] - res[categ.id] = routes - return res - - _columns = { - 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes', domain="[('product_categ_selectable', '=', True)]"), - 'removal_strategy_id': fields.many2one('product.removal', 'Force Removal Strategy', help="Set a specific removal strategy that will be used regardless of the source location for this product category"), - 'total_route_ids': fields.function(calculate_total_routes, relation='stock.location.route', type='many2many', string='Total routes', readonly=True), - } diff --git a/addons/stock/product_view.xml b/addons/stock/product_view.xml deleted file mode 100644 index 050cc3eb1df..00000000000 --- a/addons/stock/product_view.xml +++ /dev/null @@ -1,332 +0,0 @@ - - - - - - product.stock.tree.inherit - product.product - - - - virtual_available<0 - virtual_available>=0 - - - - - - - - - - product.template.stock.tree.inherit - product.template - - - - virtual_available<0 - virtual_available>=0 - - - - - - - - - - - Receipts - stock.move - ir.actions.act_window - form - tree,form - - - - -

- Click to register a receipt for this product. -

- Here you will find the history of all receipts related to - this product, as well as all future receipts you are waiting - from your suppliers. -

-
-
- - - Deliveries - stock.move - ir.actions.act_window - form - tree,form - - - - -

- Click to add a delivery order for this product. -

- Here you will find the history of all past deliveries related to - this product, as well as all the products you must deliver to - customers. -

-
-
- - - - - product.template.search.stock.form - product.template - - - - - - - - - - - - - - - - product.template.stock.property.form.inherit - product.template - - -
- -
- - - - - - - - - - - - - - - - -
- - - Product Template Kanban Stock - product.template - - - - - -
    -
  • On hand:
  • -
-
-
- - - - - product.search.stock.form - product.product - - - - - - - - - - - Product Kanban Stock - product.product - - - - - - - - - Receipts - - - Receipt - - - Deliveries - - - Delivery - - - - - - - product.product.view.form.easy.inherit.stock - product.product - - - -
-
-
-
-
- - - product.product.procurement - product.product - - - -
-
-
- - - - -
-
-
-
- - - product.template_procurement - product.template - - - -
-
-
- - - - -
- - -
-
-
-
-
diff --git a/addons/stock/res_company.py b/addons/stock/res_company.py deleted file mode 100644 index 37e20b09225..00000000000 --- a/addons/stock/res_company.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -from openerp.tools.translate import _ - -class res_company(osv.osv): - _inherit = "res.company" - _columns = { - 'propagation_minimum_delta': fields.integer('Minimum Delta for Propagation of a Date Change on moves linked together'), - 'internal_transit_location_id': fields.many2one('stock.location', 'Internal Transit Location', help="Technical field used for resupply routes between warehouses that belong to this company", on_delete="restrict"), - } - - def create_transit_location(self, cr, uid, company_id, company_name, context=None): - '''Create a transit location with company_id being the given company_id. This is needed - in case of resuply routes between warehouses belonging to the same company, because - we don't want to create accounting entries at that time. - ''' - data_obj = self.pool.get('ir.model.data') - try: - parent_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1] - except: - parent_loc = False - location_vals = { - 'name': _('%s: Transit Location') % company_name, - 'usage': 'transit', - 'company_id': company_id, - 'location_id': parent_loc, - } - location_id = self.pool.get('stock.location').create(cr, uid, location_vals, context=context) - self.write(cr, uid, [company_id], {'internal_transit_location_id': location_id}, context=context) - - def create(self, cr, uid, vals, context=None): - company_id = super(res_company, self).create(cr, uid, vals, context=context) - self.pool['stock.warehouse'].create(cr, uid, { - 'name': vals['name'], - 'code': vals['name'][:5], - 'company_id': company_id, - }, context=context) - self.create_transit_location(cr, uid, company_id, vals['name'], context=context) - return company_id - - _defaults = { - 'propagation_minimum_delta': 1, - } diff --git a/addons/stock/res_config.py b/addons/stock/res_config.py deleted file mode 100644 index 565165e2738..00000000000 --- a/addons/stock/res_config.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv - -class stock_config_settings(osv.osv_memory): - _name = 'stock.config.settings' - _inherit = 'res.config.settings' - - def set_group_stock_multi_locations(self, cr, uid, ids, context=None): - """ - If we are not in multiple locations, - we can deactivate the internal picking types of the warehouses. - That way, they won't appear in the dashboard. - """ - for obj in self.browse(cr, uid, ids, context=context): - wh_obj = self.pool['stock.warehouse'] - whs = wh_obj.search(cr, uid, [], context=context) - warehouses = wh_obj.browse(cr, uid, whs, context=context) - if obj.group_stock_multi_locations: - # Check inactive picking types and of warehouses make them active (by warehouse) - inttypes = [x.int_type_id.id for x in warehouses if not x.int_type_id.active] - if inttypes: - self.pool['stock.picking.type'].write(cr, uid, inttypes, {'active': True}, context=context) - else: - # Check active internal picking types of warehouses and make them inactive - inttypes = [x.int_type_id.id for x in warehouses if x.int_type_id.active and x.reception_steps == 'one_step' and x.delivery_steps == 'ship_only'] - if inttypes: - self.pool['stock.picking.type'].write(cr, uid, inttypes, {'active': False}, context=context) - return True - - def default_get(self, cr, uid, fields, context=None): - res = super(stock_config_settings, self).default_get(cr, uid, fields, context=context) - if 'warehouse_and_location_usage_level' in fields or not fields: - res['warehouse_and_location_usage_level'] = int(res.get('group_stock_multi_locations', False)) + int(res.get('group_stock_multi_warehouses', False)) - return res - - def onchange_warehouse_and_location_usage_level(self, cr, uid, ids, level, context=None): - return {'value': { - 'group_stock_multi_locations': level > 0, - 'group_stock_multi_warehouses': level > 1, - }} - - _columns = { - 'group_product_variant': fields.selection([ - (0, "No variants on products"), - (1, 'Products can have several attributes, defining variants (Example: size, color,...)') - ], "Product Variants", - help='Work with product variant allows you to define some variant of the same products, an ease the product management in the ecommerce for example', - implied_group='product.group_product_variant'), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'module_procurement_jit': fields.selection([ - (1, 'Reserve products immediately after the sale order confirmation'), - (0, 'Reserve products manually or based on automatic scheduler') - ], "Procurements", - help="""Allows you to automatically reserve the available - products when confirming a sale order. - This installs the module procurement_jit."""), - 'module_claim_from_delivery': fields.selection([ - (0, 'Do not manage claims'), - (1, 'Allow claims on deliveries') - ], "Claims", - help='Adds a Claim link to the delivery order.\n' - '-This installs the module claim_from_delivery.'), - 'module_product_expiry': fields.selection([ - (0, 'Do not use Expiration Date on serial numbers'), - (1, 'Define Expiration Date on serial numbers') - ], "Expiration Dates", - help="""Track different dates on products and serial numbers. - The following dates can be tracked: - - end of life - - best before date - - removal date - - alert date. - This installs the module product_expiry."""), - 'group_uom': fields.selection([ - (0, 'Products have only one unit of measure (easier)'), - (1, 'Some products may be sold/purchased in different units of measure (advanced)') - ], "Units of Measure", - implied_group='product.group_uom', - help="""Allows you to select and maintain different units of measure for products."""), - 'group_stock_packaging': fields.selection([ - (0, 'Do not manage packaging'), - (1, 'Manage available packaging options per products') - ], "Packaging Methods", - implied_group='product.group_stock_packaging', - help="""Allows you to create and manage your packaging dimensions and types you want to be maintained in your system."""), - 'group_stock_production_lot': fields.selection([ - (0, 'Do not track individual product items'), - (1, 'Track lots or serial numbers') - ], "Lots and Serial Numbers", - implied_group='stock.group_production_lot', - help="""This allows you to assign a lot (or serial number) to the pickings and moves. This can make it possible to know which production lot was sent to a certain client, ..."""), - 'group_stock_tracking_lot': fields.selection([ - (0, 'Do not manage packaging'), - (1, 'Record packages used on packing: pallets, boxes, ...') - ], "Packages", - implied_group='stock.group_tracking_lot', - help="""This allows to manipulate packages. You can put something in, take something from a package, but also move entire packages and put them even in another package. """), - 'group_warning': fields.selection([ - (0, 'All the partners can be used in pickings'), - (1, 'An informative or blocking warning can be set on a partner') - ], "Warning", implied_group='stock.group_warning'), - - 'group_stock_tracking_owner': fields.selection([ - (0, 'All products in your warehouse belong to your company'), - (1, 'Manage consignee stocks (advanced)') - ], "Product Owners", - implied_group='stock.group_tracking_owner', - help="""This way you can receive products attributed to a certain owner. """), - 'group_stock_adv_location': fields.selection([ - (0, 'No automatic routing of products'), - (1, 'Advanced routing of products using rules') - ], "Routes", - implied_group='stock.group_adv_location', - help="""This option supplements the warehouse application by effectively implementing Push and Pull inventory flows through Routes."""), - 'decimal_precision': fields.integer('Decimal precision on weight', help="As an example, a decimal precision of 2 will allow weights like: 9.99 kg, whereas a decimal precision of 4 will allow weights like: 0.0231 kg."), - 'propagation_minimum_delta': fields.related('company_id', 'propagation_minimum_delta', type='integer', string="Minimum days to trigger a propagation of date change in pushed/pull flows."), - 'module_stock_dropshipping': fields.selection([ - (0, 'Suppliers always deliver to your warehouse(s)'), - (1, "Allow suppliers to deliver directly to your customers") - ], "Dropshipping", - help='\nCreates the dropship route and add more complex tests\n' - '-This installs the module stock_dropshipping.'), - 'module_stock_picking_wave': fields.selection([ - (0, 'Manage pickings one at a time'), - (1, 'Manage picking in batch per worker') - ], "Picking Waves", - help='Install the picking wave module which will help you grouping your pickings and processing them in batch'), - 'module_stock_calendar': fields.selection([ - (0, 'Set lead times in calendar days (easy)'), - (1, "Adapt lead times using the suppliers' open days calendars (advanced)") - ], "Minimum Stock Rules", - help='This allows you to handle minimum stock rules differently by the possibility to take into account the purchase and delivery calendars \n-This installs the module stock_calendar.'), - 'module_stock_barcode': fields.boolean("Barcode scanner support"), - 'module_delivery_dhl': fields.boolean("DHL integration"), - 'module_delivery_fedex': fields.boolean("Fedex integration"), - 'module_delivery_temando': fields.boolean("Temando integration"), - 'module_delivery_ups': fields.boolean("UPS integration"), - 'module_delivery_usps': fields.boolean("USPS integration"), - # Warehouse and location usage_level : - 'warehouse_and_location_usage_level': fields.selection([ - (0, 'Manage only 1 Warehouse with only 1 stock location'), - (1, 'Manage only 1 Warehouse, composed by several stock locations'), - (2, 'Manage several Warehouses, each one composed by several stock locations') - ], "Warehouses and Locations usage level"), - 'group_stock_multi_locations': fields.boolean('Manage several stock locations', - implied_group='stock.group_stock_multi_locations'), - 'group_stock_multi_warehouses': fields.boolean('Manage several warehouses', - implied_group='stock.group_stock_multi_warehouses'), - } - - def onchange_adv_location(self, cr, uid, ids, group_stock_adv_location, context=None): - if group_stock_adv_location: - return {'value': {'warehouse_and_location_usage_level': 1}} - return {} - - def _default_company(self, cr, uid, context=None): - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - return user.company_id.id - - _defaults = { - 'company_id': _default_company, - } diff --git a/addons/stock/res_config_view.xml b/addons/stock/res_config_view.xml deleted file mode 100644 index 62e46f9f5f8..00000000000 --- a/addons/stock/res_config_view.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - stock settings - stock.config.settings - -
-
-
- - - - - - - - - - - - - - - - - - - - -
-
-
- - - Configure Warehouse - ir.actions.act_window - stock.config.settings - form - inline - - - - -
-
diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 24ec392a3a9..9db1ee8da97 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -5,7 +5,6 @@ from datetime import datetime from dateutil import relativedelta import json import time -import sets from openerp.osv import fields, osv from openerp.tools.float_utils import float_compare, float_round @@ -4936,19 +4935,6 @@ class stock_picking_type(osv.osv): 'use_create_lots': True, } -class barcode_rule(models.Model): - _inherit = 'barcode.rule' - - def _get_type_selection(self): - types = sets.Set(super(barcode_rule,self)._get_type_selection()) - types.update([ - ('weight', _('Weighted Product')), - ('location', _('Location')), - ('lot', _('Lot')), - ('package', _('Package')) - ]) - return list(types) - class StockPackOperation(models.Model): _inherit = 'stock.pack.operation' diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index d5db9efa796..ac26c41b86c 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -386,42 +386,6 @@ - - product.putaway.form - product.putaway - -
- - - - -
- - - - - - - - -
-
-
-
- - - product.removal.form - product.removal - -
- - - - -
-
-
- stock.location.path.select stock.location.path @@ -485,24 +449,6 @@ - - product.category.form - product.category - - - - - -
- The following routes will apply to the products in this category taking into account parent categories: - -
- -
-
-
-
- Locations stock.location @@ -1918,86 +1864,6 @@ src_model="stock.warehouse" groups="stock.group_stock_user"/> - - - - procurement.order.form.stock.inherit - procurement.order - - - - - - - - - -
-
- - - - -
-
- - procurement.order.tree.stock.inherit - procurement.order - - - - - - - - - procurement.rule.tree.stock.inherit - procurement.rule - - - - - - - - - - procurement.rule.form.stock.inherit - procurement.rule - - - - - - - - - - - - - - - - - - - - - - - - - {'search_default_route_false':1} - Procurement Rules - procurement.rule - - {'search_default_route_false':1} Push Rules @@ -2131,23 +1997,10 @@ - - - - - - - - - - {'default_product_id': active_id, 'search_default_product_id': active_id} - Reordering Rules - stock.warehouse.orderpoint - Stock On Hand @@ -2369,15 +2222,5 @@ tree,form [('group_id','=',active_id)] - - procurement.group.form.herited - procurement.group - - -
-
-
-
diff --git a/addons/stock/views/procurement_views.xml b/addons/stock/views/procurement_views.xml new file mode 100644 index 00000000000..241b0a296d8 --- /dev/null +++ b/addons/stock/views/procurement_views.xml @@ -0,0 +1,101 @@ + + + + procurement.group.view.form.inherit.picking + procurement.group + + +
+
+
+
+ + + + + procurement.order.form.stock.inherit + procurement.order + + + + + + + + + +
+
+ + + + +
+
+ + procurement.order.tree.stock.inherit + procurement.order + + + + + + + + + procurement.rule.tree.stock.inherit + procurement.rule + + + + + + + + + + procurement.rule.form.stock.inherit + procurement.rule + + + + + + + + + + + + + + + + + + + + + + + + + {'search_default_route_false':1} + Procurement Rules + procurement.rule + + + + + + + + + +
\ No newline at end of file diff --git a/addons/stock/views/product_strategy_views.xml b/addons/stock/views/product_strategy_views.xml new file mode 100644 index 00000000000..c93a1428bbd --- /dev/null +++ b/addons/stock/views/product_strategy_views.xml @@ -0,0 +1,38 @@ + + + + product.putaway.form + product.putaway + +
+ + + + +
+ + + + + + + + +
+
+
+
+ + + product.removal.form + product.removal + +
+ + + + +
+
+
+
\ No newline at end of file diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml new file mode 100644 index 00000000000..fc668c0d2e6 --- /dev/null +++ b/addons/stock/views/product_views.xml @@ -0,0 +1,375 @@ + + + + + + product.category.form + product.category + + + + + +
+ The following routes will apply to the products in this category taking into account parent categories: + +
+ +
+
+
+
+ + + {'default_product_id': active_id, 'search_default_product_id': active_id} + Reordering Rules + stock.warehouse.orderpoint + + + + product.stock.tree.inherit + product.product + + + + virtual_available<0 + virtual_available>=0 + + + + + + + + + + product.template.stock.tree.inherit + product.template + + + + virtual_available<0 + virtual_available>=0 + + + + + + + + + + + Receipts + stock.move + ir.actions.act_window + form + tree,form + + + + +

+ Click to register a receipt for this product. +

+ Here you will find the history of all receipts related to + this product, as well as all future receipts you are waiting + from your suppliers. +

+
+
+ + + Deliveries + stock.move + ir.actions.act_window + form + tree,form + + + + +

+ Click to add a delivery order for this product. +

+ Here you will find the history of all past deliveries related to + this product, as well as all the products you must deliver to + customers. +

+
+
+ + + + + product.template.search.stock.form + product.template + + + + + + + + + + + + + + + + product.template.stock.property.form.inherit + product.template + + +
+ +
+ + + + + + + + + + + + + + + + +
+ + + Product Template Kanban Stock + product.template + + + + + +
    +
  • On hand:
  • +
+
+
+ + + + + product.search.stock.form + product.product + + + + + + + + + + + Product Kanban Stock + product.product + + + + + + + + + Receipts + + + Receipt + + + Deliveries + + + Delivery + + + + + + + product.product.view.form.easy.inherit.stock + product.product + + + +
+
+
+
+
+ + + product.product.procurement + product.product + + + +
+
+
+ + + + +
+
+
+
+ + + product.template_procurement + product.template + + + +
+
+
+ + + + +
+ + +
+
+
+ + + Products + ir.actions.act_window + product.template + kanban,tree,form + form + {"search_default_products": 1, 'default_type': 'product'} + +

+ Click to define a new product. +

+
+
+ + + +
+
diff --git a/addons/stock/views/res_partner_views.xml b/addons/stock/views/res_partner_views.xml new file mode 100644 index 00000000000..efb615bc3ef --- /dev/null +++ b/addons/stock/views/res_partner_views.xml @@ -0,0 +1,41 @@ + + + + + + + res.partner.stock.property.form.inherit + res.partner + + + + + + + + + + + + + + + res.partner.stock.warning + res.partner + + + + + + + + + + + + + + diff --git a/addons/stock/views/stock_config_settings_views.xml b/addons/stock/views/stock_config_settings_views.xml new file mode 100644 index 00000000000..62e46f9f5f8 --- /dev/null +++ b/addons/stock/views/stock_config_settings_views.xml @@ -0,0 +1,101 @@ + + + + + stock settings + stock.config.settings + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Configure Warehouse + ir.actions.act_window + stock.config.settings + form + inline + + + + +
+
diff --git a/addons/stock/views/stock_menu_views.xml b/addons/stock/views/stock_menu_views.xml index 623fdbc1fc6..7484db4d3b5 100644 --- a/addons/stock/views/stock_menu_views.xml +++ b/addons/stock/views/stock_menu_views.xml @@ -11,9 +11,11 @@ + + @@ -23,27 +25,11 @@ + - - Products - ir.actions.act_window - product.template - kanban,tree,form - form - {"search_default_products": 1, 'default_type': 'product'} - -

- Click to define a new product. -

-
-
- - \ No newline at end of file diff --git a/addons/stock/web_planner.py b/addons/stock/web_planner.py deleted file mode 100644 index 9662aab8041..00000000000 --- a/addons/stock/web_planner.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -from openerp import models - - -class PlannerInventory(models.Model): - - _inherit = 'web.planner' - - def _get_planner_application(self): - planner = super(PlannerInventory, self)._get_planner_application() - planner.append(['planner_inventory', 'Inventory Planner']) - return planner - - def _prepare_planner_inventory_data(self): - # TDE note: void method kept (stable version), probably to remove in master - return { - } -- cgit v1.2.3