stock_warehouse.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from odoo import api, fields, models, _
  4. from odoo.exceptions import ValidationError, UserError
  5. from odoo.tools import split_every
  6. class StockWarehouse(models.Model):
  7. _inherit = 'stock.warehouse'
  8. manufacture_to_resupply = fields.Boolean(
  9. 'Manufacture to Resupply', default=True,
  10. help="When products are manufactured, they can be manufactured in this warehouse.")
  11. manufacture_pull_id = fields.Many2one(
  12. 'stock.rule', 'Manufacture Rule')
  13. manufacture_mto_pull_id = fields.Many2one(
  14. 'stock.rule', 'Manufacture MTO Rule')
  15. pbm_mto_pull_id = fields.Many2one(
  16. 'stock.rule', 'Picking Before Manufacturing MTO Rule')
  17. sam_rule_id = fields.Many2one(
  18. 'stock.rule', 'Stock After Manufacturing Rule')
  19. manu_type_id = fields.Many2one(
  20. 'stock.picking.type', 'Manufacturing Operation Type',
  21. domain="[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]", check_company=True)
  22. pbm_type_id = fields.Many2one('stock.picking.type', 'Picking Before Manufacturing Operation Type', check_company=True)
  23. sam_type_id = fields.Many2one('stock.picking.type', 'Stock After Manufacturing Operation Type', check_company=True)
  24. manufacture_steps = fields.Selection([
  25. ('mrp_one_step', 'Manufacture (1 step)'),
  26. ('pbm', 'Pick components and then manufacture (2 steps)'),
  27. ('pbm_sam', 'Pick components, manufacture and then store products (3 steps)')],
  28. 'Manufacture', default='mrp_one_step', required=True,
  29. help="Produce : Move the components to the production location\
  30. directly and start the manufacturing process.\nPick / Produce : Unload\
  31. the components from the Stock to Input location first, and then\
  32. transfer it to the Production location.")
  33. pbm_route_id = fields.Many2one('stock.route', 'Picking Before Manufacturing Route', ondelete='restrict')
  34. pbm_loc_id = fields.Many2one('stock.location', 'Picking before Manufacturing Location', check_company=True)
  35. sam_loc_id = fields.Many2one('stock.location', 'Stock after Manufacturing Location', check_company=True)
  36. def get_rules_dict(self):
  37. result = super(StockWarehouse, self).get_rules_dict()
  38. production_location_id = self._get_production_location()
  39. for warehouse in self:
  40. result[warehouse.id].update({
  41. 'mrp_one_step': [],
  42. 'pbm': [
  43. self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id, warehouse.pbm_type_id, 'pull'),
  44. self.Routing(warehouse.pbm_loc_id, production_location_id, warehouse.manu_type_id, 'pull'),
  45. ],
  46. 'pbm_sam': [
  47. self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id, warehouse.pbm_type_id, 'pull'),
  48. self.Routing(warehouse.pbm_loc_id, production_location_id, warehouse.manu_type_id, 'pull'),
  49. self.Routing(warehouse.sam_loc_id, warehouse.lot_stock_id, warehouse.sam_type_id, 'push'),
  50. ],
  51. })
  52. result[warehouse.id].update(warehouse._get_receive_rules_dict())
  53. return result
  54. @api.model
  55. def _get_production_location(self):
  56. location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.company_id.id)], limit=1)
  57. if not location:
  58. raise UserError(_('Can\'t find any production location.'))
  59. return location
  60. def _get_routes_values(self):
  61. routes = super(StockWarehouse, self)._get_routes_values()
  62. routes.update({
  63. 'pbm_route_id': {
  64. 'routing_key': self.manufacture_steps,
  65. 'depends': ['manufacture_steps', 'manufacture_to_resupply'],
  66. 'route_update_values': {
  67. 'name': self._format_routename(route_type=self.manufacture_steps),
  68. 'active': self.manufacture_steps != 'mrp_one_step',
  69. },
  70. 'route_create_values': {
  71. 'product_categ_selectable': True,
  72. 'warehouse_selectable': True,
  73. 'product_selectable': False,
  74. 'company_id': self.company_id.id,
  75. 'sequence': 10,
  76. },
  77. 'rules_values': {
  78. 'active': True,
  79. }
  80. }
  81. })
  82. routes.update(self._get_receive_routes_values('manufacture_to_resupply'))
  83. return routes
  84. def _get_route_name(self, route_type):
  85. names = {
  86. 'mrp_one_step': _('Manufacture (1 step)'),
  87. 'pbm': _('Pick components and then manufacture'),
  88. 'pbm_sam': _('Pick components, manufacture and then store products (3 steps)'),
  89. }
  90. if route_type in names:
  91. return names[route_type]
  92. else:
  93. return super(StockWarehouse, self)._get_route_name(route_type)
  94. def _get_global_route_rules_values(self):
  95. rules = super(StockWarehouse, self)._get_global_route_rules_values()
  96. location_src = self.manufacture_steps == 'mrp_one_step' and self.lot_stock_id or self.pbm_loc_id
  97. production_location = self._get_production_location()
  98. location_dest_id = self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id
  99. rules.update({
  100. 'manufacture_pull_id': {
  101. 'depends': ['manufacture_steps', 'manufacture_to_resupply'],
  102. 'create_values': {
  103. 'action': 'manufacture',
  104. 'procure_method': 'make_to_order',
  105. 'company_id': self.company_id.id,
  106. 'picking_type_id': self.manu_type_id.id,
  107. 'route_id': self._find_global_route('mrp.route_warehouse0_manufacture', _('Manufacture')).id
  108. },
  109. 'update_values': {
  110. 'active': self.manufacture_to_resupply,
  111. 'name': self._format_rulename(location_dest_id, False, 'Production'),
  112. 'location_dest_id': location_dest_id.id,
  113. 'propagate_cancel': self.manufacture_steps == 'pbm_sam'
  114. },
  115. },
  116. 'manufacture_mto_pull_id': {
  117. 'depends': ['manufacture_steps', 'manufacture_to_resupply'],
  118. 'create_values': {
  119. 'procure_method': 'mts_else_mto',
  120. 'company_id': self.company_id.id,
  121. 'action': 'pull',
  122. 'auto': 'manual',
  123. 'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id,
  124. 'location_dest_id': production_location.id,
  125. 'location_src_id': location_src.id,
  126. 'picking_type_id': self.manu_type_id.id
  127. },
  128. 'update_values': {
  129. 'name': self._format_rulename(location_src, production_location, 'MTO'),
  130. 'active': self.manufacture_to_resupply,
  131. },
  132. },
  133. 'pbm_mto_pull_id': {
  134. 'depends': ['manufacture_steps', 'manufacture_to_resupply'],
  135. 'create_values': {
  136. 'procure_method': 'make_to_order',
  137. 'company_id': self.company_id.id,
  138. 'action': 'pull',
  139. 'auto': 'manual',
  140. 'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id,
  141. 'name': self._format_rulename(self.lot_stock_id, self.pbm_loc_id, 'MTO'),
  142. 'location_dest_id': self.pbm_loc_id.id,
  143. 'location_src_id': self.lot_stock_id.id,
  144. 'picking_type_id': self.pbm_type_id.id
  145. },
  146. 'update_values': {
  147. 'active': self.manufacture_steps != 'mrp_one_step' and self.manufacture_to_resupply,
  148. }
  149. },
  150. # The purpose to move sam rule in the manufacture route instead of
  151. # pbm_route_id is to avoid conflict with receipt in multiple
  152. # step. For example if the product is manufacture and receipt in two
  153. # step it would conflict in WH/Stock since product could come from
  154. # WH/post-prod or WH/input. We do not have this conflict with
  155. # manufacture route since it is set on the product.
  156. 'sam_rule_id': {
  157. 'depends': ['manufacture_steps', 'manufacture_to_resupply'],
  158. 'create_values': {
  159. 'procure_method': 'make_to_order',
  160. 'company_id': self.company_id.id,
  161. 'action': 'pull',
  162. 'auto': 'manual',
  163. 'route_id': self._find_global_route('mrp.route_warehouse0_manufacture', _('Manufacture')).id,
  164. 'name': self._format_rulename(self.sam_loc_id, self.lot_stock_id, False),
  165. 'location_dest_id': self.lot_stock_id.id,
  166. 'location_src_id': self.sam_loc_id.id,
  167. 'picking_type_id': self.sam_type_id.id
  168. },
  169. 'update_values': {
  170. 'active': self.manufacture_steps == 'pbm_sam' and self.manufacture_to_resupply,
  171. }
  172. }
  173. })
  174. return rules
  175. def _get_locations_values(self, vals, code=False):
  176. values = super(StockWarehouse, self)._get_locations_values(vals, code=code)
  177. def_values = self.default_get(['company_id', 'manufacture_steps'])
  178. manufacture_steps = vals.get('manufacture_steps', def_values['manufacture_steps'])
  179. code = vals.get('code') or code or ''
  180. code = code.replace(' ', '').upper()
  181. company_id = vals.get('company_id', def_values['company_id'])
  182. values.update({
  183. 'pbm_loc_id': {
  184. 'name': _('Pre-Production'),
  185. 'active': manufacture_steps in ('pbm', 'pbm_sam'),
  186. 'usage': 'internal',
  187. 'barcode': self._valid_barcode(code + '-PREPRODUCTION', company_id)
  188. },
  189. 'sam_loc_id': {
  190. 'name': _('Post-Production'),
  191. 'active': manufacture_steps == 'pbm_sam',
  192. 'usage': 'internal',
  193. 'barcode': self._valid_barcode(code + '-POSTPRODUCTION', company_id)
  194. },
  195. })
  196. return values
  197. def _get_sequence_values(self, name=False, code=False):
  198. values = super(StockWarehouse, self)._get_sequence_values(name=name, code=code)
  199. values.update({
  200. 'pbm_type_id': {'name': self.name + ' ' + _('Sequence picking before manufacturing'), 'prefix': self.code + '/PC/', 'padding': 5, 'company_id': self.company_id.id},
  201. 'sam_type_id': {'name': self.name + ' ' + _('Sequence stock after manufacturing'), 'prefix': self.code + '/SFP/', 'padding': 5, 'company_id': self.company_id.id},
  202. 'manu_type_id': {'name': self.name + ' ' + _('Sequence production'), 'prefix': self.code + '/MO/', 'padding': 5, 'company_id': self.company_id.id},
  203. })
  204. return values
  205. def _get_picking_type_create_values(self, max_sequence):
  206. data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence)
  207. data.update({
  208. 'pbm_type_id': {
  209. 'name': _('Pick Components'),
  210. 'code': 'internal',
  211. 'use_create_lots': True,
  212. 'use_existing_lots': True,
  213. 'default_location_src_id': self.lot_stock_id.id,
  214. 'default_location_dest_id': self.pbm_loc_id.id,
  215. 'sequence': next_sequence + 1,
  216. 'sequence_code': 'PC',
  217. 'company_id': self.company_id.id,
  218. },
  219. 'sam_type_id': {
  220. 'name': _('Store Finished Product'),
  221. 'code': 'internal',
  222. 'use_create_lots': True,
  223. 'use_existing_lots': True,
  224. 'default_location_src_id': self.sam_loc_id.id,
  225. 'default_location_dest_id': self.lot_stock_id.id,
  226. 'sequence': next_sequence + 3,
  227. 'sequence_code': 'SFP',
  228. 'company_id': self.company_id.id,
  229. },
  230. 'manu_type_id': {
  231. 'name': _('Manufacturing'),
  232. 'code': 'mrp_operation',
  233. 'use_create_lots': True,
  234. 'use_existing_lots': True,
  235. 'sequence': next_sequence + 2,
  236. 'sequence_code': 'MO',
  237. 'company_id': self.company_id.id,
  238. },
  239. })
  240. return data, max_sequence + 4
  241. def _get_picking_type_update_values(self):
  242. data = super(StockWarehouse, self)._get_picking_type_update_values()
  243. data.update({
  244. 'pbm_type_id': {
  245. 'active': self.manufacture_to_resupply and self.manufacture_steps in ('pbm', 'pbm_sam') and self.active,
  246. 'barcode': self.code.replace(" ", "").upper() + "-PC",
  247. },
  248. 'sam_type_id': {
  249. 'active': self.manufacture_to_resupply and self.manufacture_steps == 'pbm_sam' and self.active,
  250. 'barcode': self.code.replace(" ", "").upper() + "-SFP",
  251. },
  252. 'manu_type_id': {
  253. 'active': self.manufacture_to_resupply and self.active,
  254. 'default_location_src_id': self.manufacture_steps in ('pbm', 'pbm_sam') and self.pbm_loc_id.id or self.lot_stock_id.id,
  255. 'default_location_dest_id': self.manufacture_steps == 'pbm_sam' and self.sam_loc_id.id or self.lot_stock_id.id,
  256. },
  257. })
  258. return data
  259. def _create_missing_locations(self, vals):
  260. super()._create_missing_locations(vals)
  261. for company_id in self.company_id:
  262. location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', company_id.id)], limit=1)
  263. if not location:
  264. company_id._create_production_location()
  265. def write(self, vals):
  266. if any(field in vals for field in ('manufacture_steps', 'manufacture_to_resupply')):
  267. for warehouse in self:
  268. warehouse._update_location_manufacture(vals.get('manufacture_steps', warehouse.manufacture_steps))
  269. return super(StockWarehouse, self).write(vals)
  270. def _get_all_routes(self):
  271. routes = super(StockWarehouse, self)._get_all_routes()
  272. routes |= self.filtered(lambda self: self.manufacture_to_resupply and self.manufacture_pull_id and self.manufacture_pull_id.route_id).mapped('manufacture_pull_id').mapped('route_id')
  273. return routes
  274. def _update_location_manufacture(self, new_manufacture_step):
  275. self.mapped('pbm_loc_id').write({'active': new_manufacture_step != 'mrp_one_step'})
  276. self.mapped('sam_loc_id').write({'active': new_manufacture_step == 'pbm_sam'})
  277. def _update_name_and_code(self, name=False, code=False):
  278. res = super(StockWarehouse, self)._update_name_and_code(name, code)
  279. # change the manufacture stock rule name
  280. for warehouse in self:
  281. if warehouse.manufacture_pull_id and name:
  282. warehouse.manufacture_pull_id.write({'name': warehouse.manufacture_pull_id.name.replace(warehouse.name, name, 1)})
  283. return res
  284. class Orderpoint(models.Model):
  285. _inherit = "stock.warehouse.orderpoint"
  286. @api.constrains('product_id')
  287. def check_product_is_not_kit(self):
  288. if self.env['mrp.bom'].search(['|', ('product_id', 'in', self.product_id.ids),
  289. '&', ('product_id', '=', False), ('product_tmpl_id', 'in', self.product_id.product_tmpl_id.ids),
  290. ('type', '=', 'phantom')], count=True):
  291. raise ValidationError(_("A product with a kit-type bill of materials can not have a reordering rule."))
  292. def _get_orderpoint_products(self):
  293. non_kit_ids = []
  294. for products in split_every(2000, super()._get_orderpoint_products().ids, self.env['product.product'].browse):
  295. kit_ids = set(k.id for k in self.env['mrp.bom']._bom_find(products, bom_type='phantom').keys())
  296. non_kit_ids.extend(id_ for id_ in products.ids if id_ not in kit_ids)
  297. products.invalidate_recordset()
  298. return self.env['product.product'].browse(non_kit_ids)