diff options
| author | Fabien Pinckaers <fp@openerp.com> | 2017-09-07 14:26:52 +0200 |
|---|---|---|
| committer | amoyaux <arm@odoo.com> | 2017-09-08 17:08:05 +0200 |
| commit | 0e4f3bb95991261c04c08e72b28430acfabdecd1 (patch) | |
| tree | 48bc0719049b132334219771d9ef6e01544c5d70 | |
| parent | ad2dc1e7cd31bb7847f33a7f20ac3300dbc81960 (diff) | |
[IMP] stock: remove procurement orders to fufill immediately
This removes the procurement.order model. To fufill their needs SO, PO, MO and
stock moves now call the _run method of the relevant procurement.group.
This mecanism is now only used for stockable product, tasks now uses their own
independent mecanism.
The _run method will check all the applicable rules and create directly the
needed model to fufill the need.
The modules stock, purchase, mrp, extends the _run method to implement their
specific strategy relevant for the rule type they define.
If an exception happens the message will be logged as a mail messsage on the
source model, for example, if a sales order cannot be fufilled the salesperson
will now see directly the reason.
OLD commit messages:
[WIP] procurement: removing procurement.order in stock, sale, purchase, sale_stock. WIP
fixup! [WIP] procurement: removing procurement.order in stock, sale, purchase, sale_stock. WIP
[IMP] Basic tests
[FIX] test not necessary anymore
[FIX] remove unnecessary print statement
[FIX] unnecessary test + why passing warehouse worked before?
[IMP] purchase: one move by purchase order line
[FIX] purchase: correct inventory tests and pass move_dest_ids among procurements
[FIX] because of bad cherry-pick merge
[IMP] make mrp pass by adding move_dest_ids there too
[IMP] tests of sale_mrp, no need for cancelpropagation then
[IMP] better to consistently use recordsets also for one2many
[FIX] purchase_requisition
[FIX] Exceptions should trigger errors, which should be caught in the tests
[FIX] sale_mrp: remove usage of procurement.order and use sale order name instead of sol
[FIX] stock_dropshipping: add sale_line_id on purchase_line_id
[FIX] Remove pdb
[IMP] add stock_dropshipping files
[IMP] stock: search carrier through sale line instead of procurement group
[IMP] add procrule test and preision needed when updating sol
[FIX] sale_order_dates + [IMP] procurement exceptions by scheduler
[FIX] No need to return task
[IMP] move file as name changes and add corrections
[FIX] Continue Run Schedulers wizard fix
[FIX] name issues of takss
[FIX] updating sale order line, but there is still a problem with the recompute
101 files changed, 1078 insertions, 1697 deletions
diff --git a/addons/delivery/models/stock_move.py b/addons/delivery/models/stock_move.py index 635fbf27494..79db247b879 100644 --- a/addons/delivery/models/stock_move.py +++ b/addons/delivery/models/stock_move.py @@ -23,5 +23,5 @@ class StockMove(models.Model): def _get_new_picking_values(self): vals = super(StockMove, self)._get_new_picking_values() - vals['carrier_id'] = self.group_id.sale_order_id.carrier_id.id + vals['carrier_id'] = self.sale_line_id.order_id.carrier_id.id return vals diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 18ee117f94f..f8a333a67f4 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -538,7 +538,7 @@ <function model="stock.inventory" name="action_done" eval="[[ref('stock_inventory_product_laptop_charger'), ref('stock_inventory_product_laptop_keypad')]]"/> <!-- Run Scheduler --> - <function model="procurement.order" name="run_scheduler"/> + <function model="procurement.group" name="run_scheduler"/> <!-- OEE --> diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index da28b39b610..796a3a69387 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -9,7 +9,6 @@ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError from odoo.tools import float_compare - class MrpProduction(models.Model): """ Manufacturing Orders """ _name = 'mrp.production' @@ -112,6 +111,7 @@ class MrpProduction(models.Model): copy=False, oldname='workcenter_lines', readonly=True) workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count') workorder_done_count = fields.Integer('# Done Work Orders', compute='_compute_workorder_done_count') + move_dest_ids = fields.One2many('stock.move', 'created_production_id') state = fields.Selection([ ('confirmed', 'Confirmed'), @@ -149,7 +149,6 @@ class MrpProduction(models.Model): procurement_group_id = fields.Many2one( 'procurement.group', 'Procurement Group', copy=False) - procurement_ids = fields.One2many('procurement.order', 'production_id', 'Related Procurements') 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') @@ -316,13 +315,12 @@ class MrpProduction(models.Model): 'product_uom_qty': self.product_qty, 'location_id': self.product_id.property_stock_production.id, 'location_dest_id': self.location_dest_id.id, - 'move_dest_ids': self.procurement_ids and [(4, p) for p in self.procurement_ids.mapped('move_dest_id').ids] or False, - 'procurement_id': self.procurement_ids and self.procurement_ids[0].id or False, 'company_id': self.company_id.id, 'production_id': self.id, 'origin': self.name, 'group_id': self.procurement_group_id.id, 'propagate': self.propagate, + 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids], }) move.action_confirm() return move @@ -493,7 +491,6 @@ class MrpProduction(models.Model): orders in exception """ if any(workorder.state == 'progress' for workorder in self.mapped('workorder_ids')): raise UserError(_('You can not cancel production order, a work order is still in progress.')) - ProcurementOrder = self.env['procurement.order'] for production in self: production.workorder_ids.filtered(lambda x: x.state != 'cancel').action_cancel() @@ -501,13 +498,6 @@ class MrpProduction(models.Model): raw_moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) (finish_moves | raw_moves).action_cancel() - procurements = ProcurementOrder.search([('move_dest_id', 'in', (finish_moves | raw_moves).ids)]) - if procurements: - procurements.cancel() - - # Put relatfinish_to_canceled procurements in exception -> I agree - ProcurementOrder.search([('production_id', 'in', self.ids)]).write({'state': 'exception'}) - self.write({'state': 'cancel'}) return True @@ -547,7 +537,6 @@ class MrpProduction(models.Model): moves_to_cancel = (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')) moves_to_cancel.action_cancel() self.write({'state': 'done', 'date_finished': fields.Datetime.now()}) - self.env["procurement.order"].search([('production_id', 'in', self.ids)]).check() return self.write({'state': 'done'}) @api.multi diff --git a/addons/mrp/models/procurement.py b/addons/mrp/models/procurement.py index ba11ffc9e81..ccf430be369 100644 --- a/addons/mrp/models/procurement.py +++ b/addons/mrp/models/procurement.py @@ -4,98 +4,74 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ +from odoo.exceptions import UserError class ProcurementRule(models.Model): _inherit = 'procurement.rule' + action = fields.Selection(selection_add=[('manufacture', 'Manufacture')]) - @api.model - def _get_action(self): - return [('manufacture', _('Manufacture'))] + super(ProcurementRule, self)._get_action() - - -class ProcurementOrder(models.Model): - _inherit = 'procurement.order' - - bom_id = fields.Many2one('mrp.bom', 'BoM', index=True, ondelete='cascade') - production_id = fields.Many2one('mrp.production', 'Manufacturing Order') - - @api.multi - def propagate_cancels(self): - cancel_man_orders = self.filtered(lambda procurement: procurement.rule_id.action == 'manufacture' and procurement.production_id).mapped('production_id') - if cancel_man_orders: - cancel_man_orders.action_cancel() - return super(ProcurementOrder, self).propagate_cancels() +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' @api.multi - def _run(self): - self.ensure_one() - if self.rule_id.action == 'manufacture': - # make a manufacturing order for the procurement - return self.make_mo()[self.id] - return super(ProcurementOrder, self)._run() + def _run(self, values, rule, doraise=True): + if rule.action == 'manufacture': + Production = self.env['mrp.production'] + ProductionSudo = Production.sudo().with_context(force_company=values['company_id'].id) + bom = self._get_matching_bom(values, rule) + if not bom: + msg = _('No Bill of Material found for product %s.') % (values['product_id'].display_name,) + if doraise: + raise UserError(msg) + else: + self.log_next_activity(values['product_id'], msg) + return False + + # create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) + production = ProductionSudo.create(self._prepare_mo_vals(values, rule, bom)) + origin_production = values.get('move_dest_ids') and values ['move_dest_ids'][0].raw_material_production_id or False + orderpoint = values.get('orderpoint_id') + if orderpoint: + production.message_post_with_view('mail.message_origin_link', + values={'self': production, 'origin': orderpoint.id}, + subtype_id=self.env.ref('mail.mt_note').id) + if origin_production: + production.message_post_with_view('mail.message_origin_link', + values={'self': production, 'origin': origin_production}, + subtype_id=self.env.ref('mail.mt_note').id) + return True + return super(ProcurementGroup, self)._run(values, rule, doraise) @api.multi - def _check(self): - return self.production_id.state == 'done' or super(ProcurementOrder, self)._check() - - @api.multi - def _get_matching_bom(self): - """ Finds the bill of material for the product from procurement order. """ - if self.bom_id: - return self.bom_id + def _get_matching_bom(self, values, rule): + if values.get('bom_id', False): + return values['bom_id'] return self.env['mrp.bom'].with_context( - company_id=self.company_id.id, force_company=self.company_id.id - )._bom_find(product=self.product_id, picking_type=self.rule_id.picking_type_id) # TDE FIXME: context bullshit + company_id=values['company_id'].id, force_company=values['company_id'].id + )._bom_find(product=values['product_id'], picking_type=rule.picking_type_id) # TDE FIXME: context bullshit - def _get_date_planned(self): - format_date_planned = fields.Datetime.from_string(self.date_planned) - date_planned = format_date_planned - relativedelta(days=self.product_id.produce_delay or 0.0) - date_planned = date_planned - relativedelta(days=self.company_id.manufacturing_lead) + def _get_date_planned(self, values, rule): + format_date_planned = fields.Datetime.from_string(values['date_planned']) + date_planned = format_date_planned - relativedelta(days=values['product_id'].produce_delay or 0.0) + date_planned = date_planned - relativedelta(days=values['company_id'].manufacturing_lead) return date_planned - def _prepare_mo_vals(self, bom): + def _prepare_mo_vals(self, values, rule, bom): return { - 'origin': self.origin, - 'product_id': self.product_id.id, - 'product_qty': self.product_qty, - 'product_uom_id': self.product_uom.id, - 'location_src_id': self.rule_id.location_src_id.id or self.location_id.id, - 'location_dest_id': self.location_id.id, + 'origin': values['origin'], + 'product_id': values['product_id'].id, + 'product_qty': values['product_qty'], + 'product_uom_id': values['product_uom'].id, + 'location_src_id': rule.location_src_id.id or values['location_id'].id, + 'location_dest_id': values['location_id'].id, 'bom_id': bom.id, - 'date_planned_start': fields.Datetime.to_string(self._get_date_planned()), - 'date_planned_finished': self.date_planned, - 'procurement_group_id': self.group_id.id, - 'propagate': self.rule_id.propagate, - 'picking_type_id': self.rule_id.picking_type_id.id or self.warehouse_id.manu_type_id.id, - 'company_id': self.company_id.id, - 'procurement_ids': [(6, 0, [self.id])], + 'date_planned_start': fields.Datetime.to_string(self._get_date_planned(values, rule)), + 'date_planned_finished': values['date_planned'], + 'procurement_group_id': values.get('group_id').id if values.get('group_id', False) else False, + 'propagate': rule.propagate, + 'picking_type_id': rule.picking_type_id.id or values['warehouse_id'].manu_type_id.id, + 'company_id': values['company_id'].id, + 'move_dest_ids': values.get('move_dest_ids') and [(4, x.id) for x in values['move_dest_ids']] or False, } - @api.multi - def make_mo(self): - """ Create production orders from procurements """ - res = {} - Production = self.env['mrp.production'] - for procurement in self: - ProductionSudo = Production.sudo().with_context(force_company=procurement.company_id.id) - bom = procurement._get_matching_bom() - if bom: - # create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) - production = ProductionSudo.create(procurement._prepare_mo_vals(bom)) - origin_production = procurement.move_dest_id.raw_material_production_id - orderpoint = procurement.orderpoint_id - if orderpoint: - production.message_post_with_view('mail.message_origin_link', - values={'self': production, 'origin': orderpoint}, - subtype_id=self.env.ref('mail.mt_note').id) - if origin_production: - production.message_post_with_view('mail.message_origin_link', - values={'self': production, 'origin': origin_production}, - subtype_id=self.env.ref('mail.mt_note').id) - - res[procurement.id] = production.id - else: - res[procurement.id] = False - procurement.message_post(body=_("No BoM exists for this product!")) - return res diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 40098f5f16e..a391f986a99 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -42,6 +42,7 @@ class StockMoveLine(models.Model): class StockMove(models.Model): _inherit = 'stock.move' + created_production_id = fields.Many2one('mrp.production', 'Created Production Order') production_id = fields.Many2one( 'mrp.production', 'Production Order for finished products') raw_material_production_id = fields.Many2one( @@ -178,7 +179,6 @@ class StockMove(models.Model): 'product_uom_qty': quantity, 'state': 'draft', # will be confirmed below 'name': self.name, - 'procurement_id': self.procurement_id.id, }) return self.env['stock.move'] diff --git a/addons/mrp/report/mrp_production_templates.xml b/addons/mrp/report/mrp_production_templates.xml index d6869e71b33..1dfead642db 100644 --- a/addons/mrp/report/mrp_production_templates.xml +++ b/addons/mrp/report/mrp_production_templates.xml @@ -38,14 +38,6 @@ <span t-esc="time.strftime('%Y-%m-%d')" t-options='{"widget": "date"}'/> </div> - <div class="col-xs-3" t-if="'sale_ref' in o"> - <strong>Partner Ref:</strong><br/> - <span t-esc="'sale_ref' in o and o.sale_ref"/> - </div> - <div class="col-xs-3" t-if="'sale_name' in o"> - <strong>SO Number:</strong><br/> - <span t-esc="'sale_name' in o and o.sale_name"/> - </div> </div> <div t-if="o.workorder_ids"> diff --git a/addons/mrp/security/ir.model.access.csv b/addons/mrp/security/ir.model.access.csv index ebdc3c6c170..9d5a89fca90 100644 --- a/addons/mrp/security/ir.model.access.csv +++ b/addons/mrp/security/ir.model.access.csv @@ -8,7 +8,6 @@ access_mrp_routing_workcenter,mrp.routing.workcenter,model_mrp_routing_workcente access_mrp_bom,mrp.bom,model_mrp_bom,group_mrp_user,1,0,0,0 access_mrp_bom_line,mrp.bom.line,model_mrp_bom_line,group_mrp_user,1,0,0,0 access_mrp_production,mrp.production user,model_mrp_production,mrp.group_mrp_user,1,1,1,1 -access_procurement,procurement.order,model_procurement_order,mrp.group_mrp_user,1,1,1,1 access_mrp_workcenter_manager,mrp.workcenter.manager,model_mrp_workcenter,mrp.group_mrp_manager,1,1,1,1 access_mrp_resource_manager,access_mrp_resource manager,resource.model_resource_resource,mrp.group_mrp_manager,1,1,1,1 access_mrp_routing_manager,mrp.routing.manager,model_mrp_routing,mrp.group_mrp_manager,1,1,1,1 @@ -19,7 +18,6 @@ access_stock_location_mrp_worker,stock.location mrp_worker,stock.model_stock_loc access_stock_move_mrp_worker,stock.move mrp_worker,stock.model_stock_move,mrp.group_mrp_user,1,1,1,0 access_stock_picking_mrp_worker,stock.picking mrp_worker,stock.model_stock_picking,mrp.group_mrp_user,1,1,1,1 access_stock_warehouse,stock.warehouse mrp_worker,stock.model_stock_warehouse,mrp.group_mrp_user,1,0,0,0 -access_procurement_user,procurement.order.user,model_procurement_order,base.group_user,1,1,1,1 access_mrp_production_stock_worker,mrp.production stock_worker,model_mrp_production,stock.group_stock_user,1,0,0,0 access_product_product_user,product.product user,product.model_product_product,mrp.group_mrp_user,1,0,0,0 access_product_template_user,product.template user,product.model_product_template,mrp.group_mrp_user,1,0,0,0 diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py index 98074e5e004..16c6dbe4a32 100644 --- a/addons/mrp/tests/test_procurement.py +++ b/addons/mrp/tests/test_procurement.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.mrp.tests.common import TestMrpCommon - +from odoo.exceptions import UserError class TestProcurement(TestMrpCommon): @@ -38,11 +38,11 @@ class TestProcurement(TestMrpCommon): self.assertEqual(production_product_6.state, 'confirmed', 'Production order should be for Confirmed state') # Check procurement for product 4 created or not. - procurement = self.env['procurement.order'].search([('group_id', '=', production_product_6.procurement_group_id.id), ('product_id', '=', self.product_4.id)]) - self.assertTrue(procurement, 'No procurement are created !') - self.assertEqual(procurement.state, 'running', 'Procurement order should be in state running') + # Check it created a purchase order - produce_product_4 = procurement.production_id + move_raw_product4 = production_product_6.move_raw_ids.filtered(lambda x: x.product_id == self.product_4) + produce_product_4 = self.env['mrp.production'].search([('product_id', '=', self.product_4.id), + ('move_dest_ids', '=', move_raw_product4[0].id)]) # produce product self.assertEqual(produce_product_4.availability, 'waiting', "Consume material not available") @@ -76,7 +76,6 @@ class TestProcurement(TestMrpCommon): # Check procurement and Production state for product 4. produce_product_4.button_mark_done() self.assertEqual(produce_product_4.state, 'done', 'Production order should be in state done') - self.assertEqual(procurement.state, 'done', 'Procurement order should be in state done') # Produce product 6 # ------------------ @@ -126,17 +125,12 @@ class TestProcurement(TestMrpCommon): mto_route.product_categ_selectable = True all_categ_id.write({'route_ids': [(6, 0, [mto_route.id])]}) - # create MO - production_product_4 = self.env['mrp.production'].create({ - 'name': 'MO/Test-00002', - 'product_id': self.product_4.id, - 'product_qty': 1, - 'bom_id': self.bom_1.id, - 'product_uom_id': self.product_4.uom_id.id, - }) - - # check that procurement are correctly created - procurement = self.env['procurement.order'].search( - [('group_id', '=', production_product_4.procurement_group_id.id), - ('product_id', 'in', self.bom_1.bom_line_ids.mapped('product_id.id'))]) - self.assertEqual(len(procurement), 2) + # create MO, but check it raises error as components are in make to order and not everyone has + with self.assertRaises(UserError): + production_product_4 = self.env['mrp.production'].create({ + 'name': 'MO/Test-00002', + 'product_id': self.product_4.id, + 'product_qty': 1, + 'bom_id': self.bom_1.id, + 'product_uom_id': self.product_4.uom_id.id, + })
\ No newline at end of file diff --git a/addons/mrp/views/procurement_views.xml b/addons/mrp/views/procurement_views.xml index fac569428db..9101fa31b83 100644 --- a/addons/mrp/views/procurement_views.xml +++ b/addons/mrp/views/procurement_views.xml @@ -5,7 +5,7 @@ <record id="view_procurement_rule_form_stock_inherit_mrp" model="ir.ui.view"> <field name="name">procurement.rule.form.stock.inherit.mrp</field> <field name="model">procurement.rule</field> - <field name="inherit_id" ref="stock.view_procurement_rule_form_stock_inherit"/> + <field name="inherit_id" ref="stock.view_procurement_rule_form"/> <field name="arch" type="xml"> <field name="location_src_id" position="attributes"> <attribute name="attrs">{'required': [('action', '=', 'move')], 'invisible': [('action', 'not in', ['move', 'manufacture'])]}</attribute> @@ -13,18 +13,5 @@ </field> </record> - <record id="view_procurement_form_inherit" model="ir.ui.view"> - <field name="name">procurement.order.form.inherit</field> - <field name="model">procurement.order</field> - <field name="inherit_id" ref="procurement.procurement_form_view"/> - <field name="arch" type="xml"> - <data> - <field name="origin" position="before"> - <field name="bom_id" domain="[('product_id','=',product_id)]"/> - <field name="production_id" attrs="{'invisible': [('production_id','=',False)]}"/> - </field> - </data> - </field> - </record> </data> -</odoo>
\ No newline at end of file +</odoo> diff --git a/addons/mrp/views/res_config_settings_views.xml b/addons/mrp/views/res_config_settings_views.xml index ab83a854335..4cef988f45d 100644 --- a/addons/mrp/views/res_config_settings_views.xml +++ b/addons/mrp/views/res_config_settings_views.xml @@ -89,7 +89,7 @@ <div class="o_setting_right_pane"> <label for="module_mrp_mps"/> <div class="text-muted"> - Plan procurements based on forecasts + Plan manufacturing or purchase orders based on forecasts </div> <div class="content-group" id="content_mrp_mps"/> </div> diff --git a/addons/procurement/__manifest__.py b/addons/procurement/__manifest__.py index b5cc7db3389..6c929aa19f9 100644 --- a/addons/procurement/__manifest__.py +++ b/addons/procurement/__manifest__.py @@ -37,6 +37,6 @@ Procurements in exception should be checked manually and can be re-run. ], 'demo': [], 'test': [], - 'installable': True, - 'auto_install': True, + 'installable': False, + 'auto_install': False, } diff --git a/addons/product/models/product.py b/addons/product/models/product.py index c0c117f990c..e19f4c456d8 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -542,12 +542,6 @@ class ProductProduct(models.Model): ('datetime', '<=', date or fields.Datetime.now())], limit=1) return history.cost or 0.0 - def _need_procurement(self): - # When sale/product is installed alone, there is no need to create procurements. Only - # sale_stock and sale_service need procurements - return False - - class ProductPackaging(models.Model): _name = "product.packaging" _description = "Packaging" diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 4a364682920..9ef10ae5317 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -13,7 +13,7 @@ from odoo.tools import pycompat class ProductTemplate(models.Model): _name = "product.template" - _inherit = ['mail.thread'] + _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Product Template" _order = "name" diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 94d5e873a0b..0ea88a978a2 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -103,6 +103,7 @@ </sheet> <div class="oe_chatter"> <field name="message_follower_ids" widget="mail_followers"/> + <field name="activity_ids" widget="mail_activity"/> <field name="message_ids" widget="mail_thread"/> </div> </form> diff --git a/addons/purchase/__manifest__.py b/addons/purchase/__manifest__.py index 1994afd8ada..628acd4392f 100644 --- a/addons/purchase/__manifest__.py +++ b/addons/purchase/__manifest__.py @@ -18,7 +18,6 @@ 'data/purchase_data.yml', 'report/purchase_reports.xml', 'views/purchase_views.xml', - 'views/procurement_views.xml', 'views/stock_views.xml', 'views/res_config_settings_views.xml', 'views/res_partner_views.xml', diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 3d10872c728..310715e05ed 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -104,7 +104,7 @@ class PurchaseOrder(models.Model): name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New') origin = fields.Char('Source Document', copy=False,\ help="Reference of the document that generated this purchase order " - "request (e.g. a sales order or an internal procurement request)") + "request (e.g. a sales order)") partner_ref = fields.Char('Vendor Reference', copy=False,\ help="Reference of the sales order or bid sent by the vendor. " "It's used to do the matching when you receive the " @@ -365,13 +365,6 @@ class PurchaseOrder(models.Model): for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'): pick.action_cancel() - # TDE FIXME: I don' think context key is necessary, as actions are not related / called from each other - if not self.env.context.get('cancel_procurement'): - procurements = order.order_line.mapped('procurement_ids') - procurements.filtered(lambda r: r.state not in ('cancel', 'exception') and r.rule_id.propagate).write({'state': 'cancel'}) - procurements.filtered(lambda r: r.state not in ('cancel', 'exception') and not r.rule_id.propagate).write({'state': 'exception'}) - moves = procurements.filtered(lambda r: r.rule_id.propagate).mapped('move_dest_id') - moves.filtered(lambda r: r.state != 'cancel').action_cancel() self.write({'state': 'cancel'}) @@ -579,21 +572,21 @@ class PurchaseOrderLine(models.Model): def create(self, values): line = super(PurchaseOrderLine, self).create(values) if line.order_id.state == 'purchase': - line._create_or_update_picking(values) + line._create_or_update_picking() msg = _("Extra line with %s ") % (line.product_id.display_name,) line.order_id.message_post(body=msg) return line @api.multi def write(self, values): - if 'product_qty' in values: - self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking(values) result = super(PurchaseOrderLine, self).write(values) # Update expected date of corresponding moves if 'date_planned' in values: self.env['stock.move'].search([ ('purchase_line_id', 'in', self.ids), ('state', '!=', 'done') ]).write({'date_expected': values['date_planned']}) + if 'product_qty' in values: + self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking() return result name = fields.Text(string='Description', required=True) @@ -628,13 +621,20 @@ class PurchaseOrderLine(models.Model): partner_id = fields.Many2one('res.partner', related='order_id.partner_id', string='Partner', readonly=True, store=True) currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True) date_order = fields.Datetime(related='order_id.date_order', string='Order Date', readonly=True) - procurement_ids = fields.One2many('procurement.order', 'purchase_line_id', string='Associated Procurements', copy=False) + + orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint') + move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves') @api.multi - def _create_or_update_picking(self, vals): + def _create_or_update_picking(self): for line in self: if line.product_id.type in ('product', 'consu'): - if float_compare(vals['product_qty'], line.qty_invoiced, line.product_uom.rounding) == -1: + # Prevent decreasing below received quantity + if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0: + raise UserError('You cannot decrease the ordered quantity below the received quantity.\n' + 'Create a return first.') + + if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1: # If the quantity is now below the invoiced quantity, create an activity on the vendor bill # inviting the user to create a refund. activity = self.env['mail.activity'].sudo().create({ @@ -644,42 +644,19 @@ class PurchaseOrderLine(models.Model): 'res_model_id': self.env.ref('account.model_account_invoice').id, }) activity._onchange_activity_type_id() - diff_purchase_uom_qty = vals['product_qty'] - line.product_qty - diff_qty = line.product_uom._compute_quantity(diff_purchase_uom_qty, line.product_id.uom_id) - if diff_purchase_uom_qty > 0 or not line.move_ids: - # If the user increased quantity of existing line or created a new line - pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit')) - picking = pickings and pickings[0] or False - if not picking: - res = line.order_id._prepare_picking() - picking = self.env['stock.picking'].create(res) - move_vals = line._get_move_template(picking) - if not line.move_ids: - move_vals['product_uom_qty'] = vals['product_qty'] - else: - move_vals['product_uom_qty'] = diff_purchase_uom_qty + + # If the user increased quantity of existing line or created a new line + pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit')) + picking = pickings and pickings[0] or False + if not picking: + res = line.order_id._prepare_picking() + picking = self.env['stock.picking'].create(res) + move_vals = line._prepare_stock_moves(picking) + for move_val in move_vals: self.env['stock.move']\ - .create(move_vals)\ + .create(move_val)\ .action_confirm()\ .action_assign() - else: - # Prevent decreasing below received quantity - if float_compare(vals['product_qty'], line.qty_received, line.product_uom.rounding) >= 0: - open_moves = line.move_ids.filtered(lambda m: m.state not in ('done', 'cancel')).sorted(lambda m: m.procurement_id.id) - # We sort by procurement id to decrease first the moves without procurement id, - # the idea is to get rid of the extra moves in priority - for move in open_moves: - if float_compare(move.product_qty, -diff_qty, line.product_id.uom_id.rounding) > 0: - qty_for_move = line.product_uom._compute_quantity(diff_purchase_uom_qty, move.product_uom) - move.product_uom_qty += qty_for_move - move.action_assign() - break - else: - diff_qty += move.product_qty - move.action_cancel() - else: - raise UserError('You cannot decrease the ordered quantity below the received quantity.\n' - 'Create a return first.') @api.multi def _get_stock_move_price_unit(self): @@ -696,8 +673,18 @@ class PurchaseOrderLine(models.Model): return price_unit @api.multi - def _get_move_template(self, picking): + def _prepare_stock_moves(self, picking): + """ Prepare the stock moves data for one order line. This function returns a list of + dictionary ready to be used in stock.move's create() + """ self.ensure_one() + res = [] + if self.product_id.type not in ['product', 'consu']: + return res + qty = 0.0 + price_unit = self._get_stock_move_price_unit() + for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"): + qty += move.product_qty template = { 'name': self.name or '', 'product_id': self.product_id.id, @@ -708,51 +695,18 @@ class PurchaseOrderLine(models.Model): 'location_dest_id': self.order_id._get_destination_location(), 'picking_id': picking.id, 'partner_id': self.order_id.dest_address_id.id, - 'move_dest_ids': False, + 'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids], 'state': 'draft', 'purchase_line_id': self.id, 'company_id': self.order_id.company_id.id, - 'price_unit': self._get_stock_move_price_unit(), + 'price_unit': price_unit, 'picking_type_id': self.order_id.picking_type_id.id, 'group_id': self.order_id.group_id.id, - 'procurement_id': False, 'origin': self.order_id.name, - 'route_ids': self.order_id.picking_type_id.warehouse_id and [ - (6, 0, [x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids])] or [], + 'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids])] or [], 'warehouse_id': self.order_id.picking_type_id.warehouse_id.id, } - return template - - @api.multi - def _prepare_stock_moves(self, picking): - """ Prepare the stock moves data for one order line. This function returns a list of - dictionary ready to be used in stock.move's create() - """ - self.ensure_one() - res = [] - if self.product_id.type not in ['product', 'consu']: - return res - qty = 0.0 - for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"): - qty += move.product_qty - template = self._get_move_template(picking) - # Fullfill all related procurements with this po line diff_quantity = self.product_qty - qty - for procurement in self.procurement_ids.filtered(lambda p: p.state != 'cancel'): - # If the procurement has some moves already, we should deduct their quantity - sum_existing_moves = sum(x.product_qty for x in procurement.move_ids if x.state != 'cancel') - existing_proc_qty = procurement.product_id.uom_id._compute_quantity(sum_existing_moves, procurement.product_uom) - procurement_qty = procurement.product_uom._compute_quantity(procurement.product_qty, self.product_uom) - existing_proc_qty - if float_compare(procurement_qty, 0.0, precision_rounding=procurement.product_uom.rounding) > 0 and float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0: - tmp = template.copy() - tmp.update({ - 'product_uom_qty': min(procurement_qty, diff_quantity), - 'move_dest_ids': procurement.move_dest_id and [(4, procurement.move_dest_id.id)] or [], # move destination is same as procurement destination - 'procurement_id': procurement.id, - 'propagate': procurement.rule_id.propagate, - }) - res.append(tmp) - diff_quantity -= min(procurement_qty, diff_quantity) if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0: template['product_uom_qty'] = diff_quantity res.append(template) @@ -772,9 +726,6 @@ class PurchaseOrderLine(models.Model): for line in self: if line.order_id.state in ['purchase', 'done']: raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,)) - for proc in line.procurement_ids: - proc.message_post(body=_('Purchase order line deleted.')) - line.procurement_ids.filtered(lambda r: r.state != 'cancel').write({'state': 'exception'}) return super(PurchaseOrderLine, self).unlink() @api.model @@ -894,111 +845,114 @@ class PurchaseOrderLine(models.Model): class ProcurementRule(models.Model): _inherit = 'procurement.rule' - @api.model - def _get_action(self): - return [('buy', _('Buy'))] + super(ProcurementRule, self)._get_action() - + action = fields.Selection(selection_add=[('buy', 'Buy')]) -class ProcurementOrder(models.Model): - _inherit = 'procurement.order' - purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Order Line') - purchase_id = fields.Many2one(related='purchase_line_id.order_id', string='Purchase Order') +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' @api.multi - def propagate_cancels(self): - result = super(ProcurementOrder, self).propagate_cancels() - for procurement in self: - if procurement.rule_id.action == 'buy' and procurement.purchase_line_id: - if procurement.purchase_line_id.order_id.state not in ('draft', 'cancel', 'sent', 'to validate'): - raise UserError( - _('Can not cancel a procurement related to a purchase order. Please cancel the purchase order first.')) - if procurement.purchase_line_id: - price_unit = 0.0 - product_qty = 0.0 - others_procs = procurement.purchase_line_id.procurement_ids.filtered(lambda r: r != procurement) - for other_proc in others_procs: - if other_proc.state not in ['cancel', 'draft']: - product_qty += other_proc.product_uom._compute_quantity(other_proc.product_qty, procurement.purchase_line_id.product_uom) - - precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - if not float_is_zero(product_qty, precision_digits=precision): - seller = procurement.product_id._select_seller( - partner_id=procurement.purchase_line_id.partner_id, - quantity=product_qty, - date=procurement.purchase_line_id.order_id.date_order and procurement.purchase_line_id.order_id.date_order[:10], - uom_id=procurement.purchase_line_id.product_uom) - - price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, procurement.purchase_line_id.product_id.supplier_taxes_id, procurement.purchase_line_id.taxes_id) if seller else 0.0 - if price_unit and seller and procurement.purchase_line_id.order_id.currency_id and seller.currency_id != procurement.purchase_line_id.order_id.currency_id: - price_unit = seller.currency_id.compute(price_unit, procurement.purchase_line_id.order_id.currency_id) - - if seller and seller.product_uom != procurement.purchase_line_id.product_uom: - price_unit = seller.product_uom._compute_price(price_unit, procurement.purchase_line_id.product_uom) - - procurement.purchase_line_id.product_qty = product_qty - procurement.purchase_line_id.price_unit = price_unit + def _run(self, values, rule, doraise=True): + if rule.action == 'buy': + cache = {} + suppliers = values['product_id'].seller_ids\ + .filtered(lambda r: (not r.company_id or r.company_id == values['company_id']) and (not r.product_id or r.product_id == values['product_id'])) + if not suppliers: + msg = _('No vendor associated to product %s. Unable to generate the purchase order.') % (values['product_id'].display_name,) + if doraise: + raise UserError(msg) else: - procurement.purchase_line_id.unlink() + self._log_next_activity(values['product_id'], msg) + return False + supplier = self._make_po_select_supplier(values, rule, suppliers) + partner = supplier.name - return result + domain = self._make_po_get_domain(values, rule, partner) - @api.multi - def _run(self): - if self.rule_id and self.rule_id.action == 'buy': - return self.make_po() - return super(ProcurementOrder, self)._run() + if domain in cache: + po = cache[domain] + else: + po = self.env['purchase.order'].search([dom for dom in domain]) + po = po[0] if po else False + cache[domain] = po + if not po: + vals = self._prepare_purchase_order(values, rule, partner) + po = self.env['purchase.order'].create(vals) + cache[domain] = po + elif not po.origin or values['origin'] not in po.origin.split(', '): + if po.origin: + if values['origin']: + po.write({'origin': po.origin + ', ' + values['origin']}) + else: + po.write({'origin': po.origin}) + else: + po.write({'origin': values['origin']}) - @api.multi - def _check(self): - if self.purchase_line_id: - if not self.move_ids: - return False - return all(move.state in ('done', 'cancel') for move in self.move_ids) and any(move.state == 'done' for move in self.move_ids) - return super(ProcurementOrder, self)._check() - - def _get_purchase_schedule_date(self): + # Create Line + po_line = False + for line in po.order_line: + if line.product_id == values['product_id'] and line.product_uom == values['product_id'].uom_po_id: + procurement_uom_po_qty = values['product_uom']._compute_quantity(values['product_qty'], values['product_id'].uom_po_id) + seller = values['product_id']._select_seller( + partner_id=partner, + quantity = line.product_qty + procurement_uom_po_qty, + date = po.date_order and po.date_order[:10], + uom_id=values['product_id'].uom_po_id) + + price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, line.product_id.supplier_taxes_id, line.taxes_id) if seller else 0.0 + if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id: + price_unit = seller.currency_id.compute(price_unit, po.currency_id) + + po_line = line.write({ + 'product_qty': line.product_qty + procurement_uom_po_qty, + 'price_unit': price_unit, + 'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])] + }) + break + if not po_line: + vals = self._prepare_purchase_order_line(values, rule, po, supplier) + self.env['purchase.order.line'].create(vals) + return True + return super(ProcurementGroup, self)._run(values, rule, doraise=True) + + def _get_purchase_schedule_date(self, values, rule): """Return the datetime value to use as Schedule Date (``date_planned``) for the Purchase Order Lines created to satisfy the given procurement. """ - procurement_date_planned = datetime.strptime(self.date_planned, DEFAULT_SERVER_DATETIME_FORMAT) - schedule_date = (procurement_date_planned - relativedelta(days=self.company_id.po_lead)) + procurement_date_planned = datetime.strptime(values['date_planned'], DEFAULT_SERVER_DATETIME_FORMAT) + schedule_date = (procurement_date_planned - relativedelta(days=values['company_id'].po_lead)) return schedule_date - def _get_purchase_order_date(self, partner, schedule_date): + def _get_purchase_order_date(self, values, rule, partner, schedule_date): """Return the datetime value to use as Order Date (``date_order``) for the Purchase Order created to satisfy the given procurement. """ - self.ensure_one() - - seller = self.product_id._select_seller( + seller = values['product_id']._select_seller( partner_id=partner, - quantity=self.product_qty, + quantity=values['product_qty'], date=fields.Date.to_string(schedule_date), - uom_id=self.product_uom) + uom_id=values['product_uom']) return schedule_date - relativedelta(days=int(seller.delay)) - @api.multi - def _prepare_purchase_order_line(self, po, supplier): - self.ensure_one() - - procurement_uom_po_qty = self.product_uom._compute_quantity(self.product_qty, self.product_id.uom_po_id) - seller = self.product_id._select_seller( + @api.model + def _prepare_purchase_order_line(self, values, rule, po, supplier): + procurement_uom_po_qty = values['product_uom']._compute_quantity(values['product_qty'], values['product_id'].uom_po_id) + seller = values['product_id']._select_seller( partner_id=supplier.name, quantity=procurement_uom_po_qty, date=po.date_order and po.date_order[:10], - uom_id=self.product_id.uom_po_id) + uom_id=values['product_id'].uom_po_id) - taxes = self.product_id.supplier_taxes_id + taxes = values['product_id'].supplier_taxes_id fpos = po.fiscal_position_id taxes_id = fpos.map_tax(taxes) if fpos else taxes if taxes_id: - taxes_id = taxes_id.filtered(lambda x: x.company_id.id == self.company_id.id) + taxes_id = taxes_id.filtered(lambda x: x.company_id.id == values['company_id'].id) - price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, self.product_id.supplier_taxes_id, taxes_id) if seller else 0.0 + price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, values['product_id'].supplier_taxes_id, taxes_id) if seller else 0.0 if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id: price_unit = seller.currency_id.compute(price_unit, po.currency_id) - product_lang = self.product_id.with_context({ + product_lang = values['product_id'].with_context({ 'lang': supplier.name.lang, 'partner_id': supplier.name.id, }) @@ -1011,136 +965,65 @@ class ProcurementOrder(models.Model): return { 'name': name, 'product_qty': procurement_uom_po_qty, - 'product_id': self.product_id.id, - 'product_uom': self.product_id.uom_po_id.id, + 'product_id': values['product_id'].id, + 'product_uom': values['product_id'].uom_po_id.id, 'price_unit': price_unit, 'date_planned': date_planned, + 'orderpoint_id': values.get('orderpoint_id', False) and values.get('orderpoint_id').id, 'taxes_id': [(6, 0, taxes_id.ids)], - 'procurement_ids': [(4, self.id)], 'order_id': po.id, + 'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])], } - @api.multi - def _prepare_purchase_order(self, partner): - self.ensure_one() - schedule_date = self._get_purchase_schedule_date() - purchase_date = self._get_purchase_order_date(partner, schedule_date) - fpos = self.env['account.fiscal.position'].with_context(company_id=self.company_id.id).get_fiscal_position(partner.id) + def _prepare_purchase_order(self, values, rule, partner): + schedule_date = self._get_purchase_schedule_date(values, rule) + purchase_date = self._get_purchase_order_date(values, rule, partner, schedule_date) + fpos = self.env['account.fiscal.position'].with_context(company_id=values['company_id'].id).get_fiscal_position(partner.id) - gpo = self.rule_id.group_propagation_option - group = (gpo == 'fixed' and self.rule_id.group_id.id) or \ - (gpo == 'propagate' and self.group_id.id) or False + gpo = rule.group_propagation_option + group = (gpo == 'fixed' and rule.group_id.id) or \ + (gpo == 'propagate' and values['group_id'].id) or False return { 'partner_id': partner.id, - 'picking_type_id': self.rule_id.picking_type_id.id, - 'company_id': self.company_id.id, + 'picking_type_id': rule.picking_type_id.id, + 'company_id': values['company_id'].id, 'currency_id': partner.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id, - 'dest_address_id': self.partner_dest_id.id, - 'origin': self.origin, + 'dest_address_id': values.get('partner_dest_id', False) and values['partner_dest_id'].id, + 'origin': values['origin'], 'payment_term_id': partner.property_supplier_payment_term_id.id, 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), 'fiscal_position_id': fpos, 'group_id': group } - def _make_po_select_supplier(self, suppliers): + def _make_po_select_supplier(self, values, rule, suppliers): """ Method intended to be overridden by customized modules to implement any logic in the selection of supplier. """ return suppliers[0] - def _make_po_get_domain(self, partner): - gpo = self.rule_id.group_propagation_option - group = (gpo == 'fixed' and self.rule_id.group_id) or \ - (gpo == 'propagate' and self.group_id) or False + def _make_po_get_domain(self, values, rule, partner): + domain = super(ProcurementGroup, self)._make_po_get_domain(values, partner) + gpo = rule.group_propagation_option + group = (gpo == 'fixed' and rule.group_id) or \ + (gpo == 'propagate' and values['group_id']) or False - domain = ( + domain += ( ('partner_id', '=', partner.id), ('state', '=', 'draft'), - ('picking_type_id', '=', self.rule_id.picking_type_id.id), - ('company_id', '=', self.company_id.id), - ('dest_address_id', '=', self.partner_dest_id.id)) + ('picking_type_id', '=', rule.picking_type_id.id), + ('company_id', '=', values['company_id'].id), + ) if group: domain += (('group_id', '=', group.id),) + if values.get('date_planned', False): + domain += (('date_planned', '=', values.get('date_planned')),) return domain - @api.multi - def make_po(self): - cache = {} - res = [] - for procurement in self: - suppliers = procurement.product_id.seller_ids\ - .filtered(lambda r: (not r.company_id or r.company_id == procurement.company_id) and (not r.product_id or r.product_id == procurement.product_id)) - if not suppliers: - procurement.message_post(body=_('No vendor associated to product %s. Please set one to fix this procurement.') % (procurement.product_id.name)) - continue - supplier = procurement._make_po_select_supplier(suppliers) - partner = supplier.name - - domain = procurement._make_po_get_domain(partner) - - if domain in cache: - po = cache[domain] - else: - po = self.env['purchase.order'].search([dom for dom in domain]) - po = po[0] if po else False - cache[domain] = po - if not po: - vals = procurement._prepare_purchase_order(partner) - po = self.env['purchase.order'].create(vals) - name = (procurement.group_id and (procurement.group_id.name + ":") or "") + (procurement.name != "/" and procurement.name or procurement.move_dest_id.raw_material_production_id and procurement.move_dest_id.raw_material_production_id.name or "") - message = _("This purchase order has been created from: <a href=# data-oe-model=procurement.order data-oe-id=%d>%s</a>") % (procurement.id, name) - po.message_post(body=message) - cache[domain] = po - elif not po.origin or procurement.origin not in po.origin.split(', '): - # Keep track of all procurements - if po.origin: - if procurement.origin: - po.write({'origin': po.origin + ', ' + procurement.origin}) - else: - po.write({'origin': po.origin}) - else: - po.write({'origin': procurement.origin}) - name = (self.group_id and (self.group_id.name + ":") or "") + (self.name != "/" and self.name or self.move_dest_id.raw_material_production_id and self.move_dest_id.raw_material_production_id.name or "") - message = _("This purchase order has been modified from: <a href=# data-oe-model=procurement.order data-oe-id=%d>%s</a>") % (procurement.id, name) - po.message_post(body=message) - if po: - res += [procurement.id] - - # Create Line - po_line = False - for line in po.order_line: - if line.product_id == procurement.product_id and line.product_uom == procurement.product_id.uom_po_id: - procurement_uom_po_qty = procurement.product_uom._compute_quantity(procurement.product_qty, procurement.product_id.uom_po_id) - seller = procurement.product_id._select_seller( - partner_id=partner, - quantity=line.product_qty + procurement_uom_po_qty, - date=po.date_order and po.date_order[:10], - uom_id=procurement.product_id.uom_po_id) - - price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, line.product_id.supplier_taxes_id, line.taxes_id) if seller else 0.0 - if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id: - price_unit = seller.currency_id.compute(price_unit, po.currency_id) - - po_line = line.write({ - 'product_qty': line.product_qty + procurement_uom_po_qty, - 'price_unit': price_unit, - 'procurement_ids': [(4, procurement.id)] - }) - break - if not po_line: - vals = procurement._prepare_purchase_order_line(po, supplier) - self.env['purchase.order.line'].create(vals) - return res - - @api.multi - def open_purchase_order(self): - action = self.env.ref('purchase.purchase_order_action_generic') - action_dict = action.read()[0] - action_dict['res_id'] = self.purchase_id.id - action_dict['target'] = 'current' - return action_dict + @api.model + def _get_exceptions_domain(self): + return super(ProcurementGroup, self)._get_exceptions_domain() + [('created_purchase_line_id', '=', False)] class ProductTemplate(models.Model): diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py index 7abc5bd898b..1f02f4cc8d2 100644 --- a/addons/purchase/models/stock.py +++ b/addons/purchase/models/stock.py @@ -28,6 +28,8 @@ class StockMove(models.Model): purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', index=True, readonly=True, copy=False) + created_purchase_line_id = fields.Many2one('purchase.order.line', + 'Created Purchase Order Line', ondelete='set null', readonly=True, copy=False) @api.multi def _get_price_unit(self): @@ -137,3 +139,13 @@ class ReturnPicking(models.TransientModel): vals = super(ReturnPicking, self)._prepare_move_default_values(return_line, new_picking) vals['purchase_line_id'] = return_line.move_id.purchase_line_id.id return vals + + +class Orderpoint(models.Model): + _inherit = "stock.warehouse.orderpoint" + + def _quantity_in_progress(self): + res = super(Orderpoint, self)._quantity_in_progress() + for poline in self.env['purchase.order.line'].search([('state','in',('draft','sent','to approve')),('orderpoint_id','in',self.ids)]): + res[poline.orderpoint_id.id] += poline.product_uom._compute_quantity(poline.product_qty, poline.orderpoint_id.product_uom, round=False) + return res
\ No newline at end of file diff --git a/addons/purchase/test/process/run_scheduler.yml b/addons/purchase/test/process/run_scheduler.yml index 6ea17eaed48..a94e0c856ea 100644 --- a/addons/purchase/test/process/run_scheduler.yml +++ b/addons/purchase/test/process/run_scheduler.yml @@ -13,28 +13,3 @@ - !python {model: product.product, id: scheduler_product}: | self.write({"route_ids": [(4, ref("purchase.route_warehouse0_buy"))]}) -- - I create a procurement order. -- - !record {model: procurement.order, id: procurement_order_testcase0}: - location_id: stock.stock_location_stock - name: Test scheduler for RFQ - product_id: scheduler_product - product_qty: 15.0 -- - I run the scheduler. -- - !python {model: procurement.order, id: False}: | - self.run_scheduler() -- - I check Generated RFQ. -- - !python {model: procurement.order, id: procurement_order_testcase0}: | - assert self.purchase_line_id, 'RFQ should be generated!' -- - I delete the line from the purchase order and check that the move and the procurement are cancelled -- - !python {model: procurement.order, id: procurement_order_testcase0}: | - self.purchase_line_id.unlink() - self.refresh() - assert self.state == 'exception', 'Procurement should be in exception' diff --git a/addons/purchase/tests/common.py b/addons/purchase/tests/common.py index 39e23acc622..fee4813f344 100644 --- a/addons/purchase/tests/common.py +++ b/addons/purchase/tests/common.py @@ -7,16 +7,20 @@ from odoo.addons.stock.tests.common2 import TestStockCommon class TestPurchase(TestStockCommon): - def _create_make_procurement(self, product, product_qty): - MakeProcurement = self.env['make.procurement'] + def _create_make_procurement(self, product, product_qty, date_planned=False): + ProcurementGroup = self.env['procurement.group'] order_values = { - 'product_id': product.id, - 'qty': product_qty, - 'uom_id': self.uom_unit.id, - 'warehouse_id': self.warehouse_1.id, - 'date_planned': fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)) # 10 days added to current date of procurement to get future schedule date and order date of purchase order. + 'name': product.name, + 'product_id': product, + 'product_qty': product_qty, + 'product_uom': self.uom_unit, + 'warehouse_id': self.warehouse_1, + 'date_planned': date_planned or fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)), # 10 days added to current date of procurement to get future schedule date and order date of purchase order. + 'location_id': self.warehouse_1.lot_stock_id, + 'origin': '/', + 'group_id': self.env['procurement.group'], } - return MakeProcurement.create(order_values) + return ProcurementGroup.run(order_values) @classmethod def setUpClass(cls): diff --git a/addons/purchase/tests/test_purchase_lead_time.py b/addons/purchase/tests/test_purchase_lead_time.py index 658ad6b6258..df5486513ea 100644 --- a/addons/purchase/tests/test_purchase_lead_time.py +++ b/addons/purchase/tests/test_purchase_lead_time.py @@ -19,29 +19,29 @@ class TestPurchaseLeadTime(TestPurchase): company.write({'po_lead': 3.00}) # Make procurement request from product_1's form view, create procurement and check it's state - procurement_product_1 = self._create_make_procurement(self.product_1, 15.00) - procurement_act_dict = procurement_product_1.make_procurement() - procurement = self.env['procurement.order'].browse(procurement_act_dict.get("res_id")) - self.assertEqual(procurement.state, 'running', 'Procurement should be in running state.') + date_planned = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)) + self._create_make_procurement(self.product_1, 15.00, date_planned=date_planned) + purchase = self.env['purchase.order.line'].search([('product_id', '=', self.product_1.id)], limit=1).order_id + # Confirm purchase order - procurement.purchase_id.button_confirm() + purchase.button_confirm() # Check order date of purchase order - order_date = fields.Datetime.from_string(procurement.date_planned) - timedelta(days=company.po_lead) - timedelta(days=self.product_1.seller_ids.delay) + order_date = fields.Datetime.from_string(date_planned) - timedelta(days=company.po_lead) - timedelta(days=self.product_1.seller_ids.delay) po_order_date = fields.Datetime.to_string(order_date) - self.assertEqual(procurement.purchase_id.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Purchase Lead Time - Delivery Lead Time.') + self.assertEqual(purchase.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Purchase Lead Time - Delivery Lead Time.') # Check scheduled date of purchase order schedule_date = order_date + timedelta(days=self.product_1.seller_ids.delay) po_schedule_date = fields.Datetime.to_string(schedule_date) - self.assertEqual(procurement.purchase_id.date_planned, po_schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time.') + self.assertEqual(purchase.date_planned, po_schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time.') # check the picking created or not - self.assertTrue(procurement.purchase_id.picking_ids, "Picking should be created.") + self.assertTrue(purchase.picking_ids, "Picking should be created.") # Check scheduled date of In Type shipment - self.assertEqual(procurement.purchase_id.picking_ids.scheduled_date, po_schedule_date, 'Schedule date of In type shipment should be equal to: schedule date of purchase order.') + self.assertEqual(purchase.picking_ids.scheduled_date, po_schedule_date, 'Schedule date of In type shipment should be equal to: schedule date of purchase order.') def test_01_product_level_delay(self): """ To check schedule dates of multiple purchase order line of the same purchase order, @@ -49,29 +49,27 @@ class TestPurchaseLeadTime(TestPurchase): and different Delivery Lead Time.""" # Make procurement request from product_1's form view, create procurement and check it's state - procurement_product_1 = self._create_make_procurement(self.product_1, 10.00) - procurement_act_dict = procurement_product_1.make_procurement() - procurement_1 = self.env['procurement.order'].browse(procurement_act_dict.get("res_id")) - self.assertEqual(procurement_1.state, 'running', 'Procurement of product_1 should be in running state.') + date_planned1 = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)) + self._create_make_procurement(self.product_1, 10.00, date_planned=date_planned1) + purchase1 = self.env['purchase.order.line'].search([('product_id', '=', self.product_1.id)], limit=1).order_id # Make procurement request from product_2's form view, create procurement and check it's state - procurement_product_2 = self._create_make_procurement(self.product_2, 5.00) - procurement_act_dict_2 = procurement_product_2.make_procurement() - procurement_2 = self.env['procurement.order'].browse(procurement_act_dict_2.get("res_id")) - self.assertEqual(procurement_2.state, 'running', 'Procurement of product_2 should be in running state.') + date_planned2 = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)) + self._create_make_procurement(self.product_2, 5.00, date_planned=date_planned2) + purchase2 = self.env['purchase.order.line'].search([('product_id', '=', self.product_2.id)], limit=1).order_id # Check purchase order is same or not - self.assertEqual(procurement_1.purchase_id, procurement_2.purchase_id, 'Purchase orders should be same for the two different product with same vendor.') + self.assertEqual(purchase1, purchase2, 'Purchase orders should be same for the two different product with same vendor.') # Confirm purchase order - procurement_2.purchase_id.button_confirm() + purchase1.button_confirm() # Check order date of purchase order - order_line_pro_1 = procurement_2.purchase_id.order_line.filtered(lambda r: r.product_id == self.product_1) - order_line_pro_2 = procurement_2.purchase_id.order_line.filtered(lambda r: r.product_id == self.product_2) - order_date = fields.Datetime.from_string(procurement_1.date_planned) - timedelta(days=self.product_1.seller_ids.delay) + order_line_pro_1 = purchase2.order_line.filtered(lambda r: r.product_id == self.product_1) + order_line_pro_2 = purchase2.order_line.filtered(lambda r: r.product_id == self.product_2) + order_date = fields.Datetime.from_string(date_planned1) - timedelta(days=self.product_1.seller_ids.delay) po_order_date = fields.Datetime.to_string(order_date) - self.assertEqual(procurement_2.purchase_id.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time.') + self.assertEqual(purchase2.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time.') # Check scheduled date of purchase order line for product_1 schedule_date_1 = order_date + timedelta(days=self.product_1.seller_ids.delay) @@ -85,13 +83,13 @@ class TestPurchaseLeadTime(TestPurchase): # Check scheduled date of purchase order po_schedule_date = min(schedule_date_line_1, schedule_date_line_2) - self.assertEqual(procurement_2.purchase_id.date_planned, po_schedule_date, 'Schedule date of purchase order should be minimum of schedule dates of purchase order lines.') + self.assertEqual(purchase2.date_planned, po_schedule_date, 'Schedule date of purchase order should be minimum of schedule dates of purchase order lines.') - # Check the picking crated or not - self.assertTrue(procurement_2.purchase_id.picking_ids, "Picking should be created.") + # Check the picking created or not + self.assertTrue(purchase2.picking_ids, "Picking should be created.") # Check scheduled date of In Type shipment - self.assertEqual(procurement_2.purchase_id.picking_ids.scheduled_date, po_schedule_date, 'Schedule date of In type shipment should be same as schedule date of purchase order.') + self.assertEqual(purchase2.picking_ids.scheduled_date, po_schedule_date, 'Schedule date of In type shipment should be same as schedule date of purchase order.') def test_02_product_route_level_delays(self): """ In order to check dates, set product's Delivery Lead Time @@ -104,45 +102,51 @@ class TestPurchaseLeadTime(TestPurchase): for push_rule in self.warehouse_1.reception_route_id.push_ids: push_rule.write({'delay': 2}) + date_planned = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)) # Create procurement order of product_1 - procurement = self.env['procurement.order'].create({ - 'product_id': self.product_1.id, + self.env['procurement.group'].run({ + 'product_id': self.product_1, 'product_qty': 5.000, 'name': 'Test scheduler for RFQ', - 'product_uom': self.uom_unit.id, - 'warehouse_id': self.warehouse_1.id, - 'location_id': self.warehouse_1.lot_stock_id.id, - 'date_planned': fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)), # 10 days added to current date of procurement to get future schedule date and order date of purchase order. - 'rule_id': self.warehouse_1.buy_pull_id.id + 'product_uom': self.uom_unit, + 'warehouse_id': self.warehouse_1, + 'location_id': self.warehouse_1.lot_stock_id, + 'date_planned': date_planned, # 10 days added to current date of procurement to get future schedule date and order date of purchase order. + 'rule_id': self.warehouse_1.buy_pull_id, + 'origin': '/', + 'group_id': False, #self.env['procurement.group'], + 'route_ids': [], }) # Confirm purchase order - procurement.purchase_id.button_confirm() + + purchase = self.env['purchase.order.line'].search([('product_id', '=', self.product_1.id)], limit=1).order_id + purchase.button_confirm() # Check order date of purchase order - order_date = fields.Datetime.from_string(procurement.date_planned) - timedelta(days=self.product_1.seller_ids.delay) + order_date = fields.Datetime.from_string(date_planned) - timedelta(days=self.product_1.seller_ids.delay) po_order_date = fields.Datetime.to_string(order_date) - self.assertEqual(procurement.purchase_id.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time.') + self.assertEqual(purchase.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time.') # Check scheduled date of purchase order schedule_date = order_date + timedelta(days=self.product_1.seller_ids.delay) po_schedule_date = fields.Datetime.to_string(schedule_date) - self.assertEqual(procurement.date_planned, po_schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time.') + self.assertEqual(date_planned, po_schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time.') # Check the picking crated or not - self.assertTrue(procurement.purchase_id.picking_ids, "Picking should be created.") + self.assertTrue(purchase.picking_ids, "Picking should be created.") # Check scheduled date of In Type shipment - incomming_shipment = procurement.purchase_id.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse_1.in_type_id and r.location_dest_id == self.warehouse_1.wh_input_stock_loc_id) - self.assertEqual(incomming_shipment.scheduled_date, po_schedule_date, 'Schedule date of In type shipment should be same as schedule date of purchase order.') + incoming_shipment = purchase.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse_1.in_type_id and r.location_dest_id == self.warehouse_1.wh_input_stock_loc_id) + self.assertEqual(incoming_shipment.scheduled_date, po_schedule_date, 'Schedule date of In type shipment should be same as schedule date of purchase order.') # Check scheduled date of Internal Type shipment - incomming_shipment1 = self.env['stock.picking'].search([('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_input_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id)]) - incomming_shipment1_date = schedule_date + timedelta(days=incomming_shipment1.move_lines[0].push_rule_id.delay) - incomming_shipment1_schedule_date = fields.Datetime.to_string(incomming_shipment1_date) - self.assertEqual(incomming_shipment1.scheduled_date, incomming_shipment1_schedule_date, 'Schedule date of Internal Type shipment for input stock location should be equal to: schedule date of purchase order + push rule delay.') - - incomming_shipment2 = self.env['stock.picking'].search([('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.lot_stock_id.id)]) - incomming_shipment2_date = incomming_shipment1_date + timedelta(days=incomming_shipment2.move_lines[0].push_rule_id.delay) - incomming_shipment2_schedule_date = fields.Datetime.to_string(incomming_shipment2_date) - self.assertEqual(incomming_shipment2.scheduled_date, incomming_shipment2_schedule_date, 'Schedule date of Internal Type shipment for quality control stock location should be equal to: schedule date of Internal type shipment for input stock location + push rule delay..') + incoming_shipment1 = self.env['stock.picking'].search([('move_lines.product_id', 'in', (self.product_1.id, self.product_2.id)), ('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_input_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id)]) + incoming_shipment1_date = schedule_date + timedelta(days=incoming_shipment1.move_lines[0].push_rule_id.delay) + incoming_shipment1_schedule_date = fields.Datetime.to_string(incoming_shipment1_date) + self.assertEqual(incoming_shipment1.scheduled_date, incoming_shipment1_schedule_date, 'Schedule date of Internal Type shipment for input stock location should be equal to: schedule date of purchase order + push rule delay.') + + incoming_shipment2 = self.env['stock.picking'].search([('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.lot_stock_id.id)]) + incoming_shipment2_date = incoming_shipment1_date + timedelta(days=incoming_shipment2.move_lines[0].push_rule_id.delay) + incoming_shipment2_schedule_date = fields.Datetime.to_string(incoming_shipment2_date) + self.assertEqual(incoming_shipment2.scheduled_date, incoming_shipment2_schedule_date, 'Schedule date of Internal Type shipment for quality control stock location should be equal to: schedule date of Internal type shipment for input stock location + push rule delay..') diff --git a/addons/purchase/tests/test_purchase_order.py b/addons/purchase/tests/test_purchase_order.py index e0fe29b585f..edab672a214 100644 --- a/addons/purchase/tests/test_purchase_order.py +++ b/addons/purchase/tests/test_purchase_order.py @@ -126,7 +126,7 @@ class TestPurchaseOrder(AccountingTestCase): # Check quantity received received_qty = sum(pol.qty_received for pol in self.po.order_line) - self.assertEqual(received_qty, 10.0, 'Purchase: Received quantity should be 10.0 instead of %s after validating incomming shipment' % received_qty) + self.assertEqual(received_qty, 10.0, 'Purchase: Received quantity should be 10.0 instead of %s after validating incoming shipment' % received_qty) # Create return picking StockReturnPicking = self.env['stock.return.picking'] @@ -156,4 +156,4 @@ class TestPurchaseOrder(AccountingTestCase): self.invoice.invoice_line_ids[0].quantity = 2.0 self.invoice.invoice_line_ids[1].quantity = 2.0 self.invoice.invoice_validate() - self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [3.0, 3.0], 'Purchase: Billed quantity should be 3.0') + self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [3.0, 3.0], 'Purchase: Billed quantity should be 3.0')
\ No newline at end of file diff --git a/addons/purchase/views/procurement_views.xml b/addons/purchase/views/procurement_views.xml deleted file mode 100644 index d7ad173ac92..00000000000 --- a/addons/purchase/views/procurement_views.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <data> - <record id="procurement_order_view_form_inherit_purchase" model="ir.ui.view"> - <field name="name">procurement.order.form.inherit.button</field> - <field name="model">procurement.order</field> - <field name="inherit_id" ref="procurement.procurement_form_view"/> - <field name="arch" type="xml"> - <xpath expr="//button[@name='do_view_procurements']" position="before"> - <button name="open_purchase_order" string="Purchases" type="object" - class="oe_stat_button" icon="fa-shopping-cart" - context="{'active_id':purchase_id}" - attrs="{'invisible':[('purchase_id', '=', False)]}"> - </button> - </xpath> - </field> - </record> - </data> -</odoo>
\ No newline at end of file diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index 271969ad0b8..045cdb97ab7 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -535,19 +535,6 @@ </field> </record> - <!-- Procurements --> - - <record id="view_procurement_form_inherit" model="ir.ui.view"> - <field name="name">procurement.order.form.inherit</field> - <field name="model">procurement.order</field> - <field name="inherit_id" ref="procurement.procurement_form_view"/> - <field name="arch" type="xml"> - <xpath expr="//field[@name='origin']" position="after"> - <field name="purchase_id"/> - </xpath> - </field> - </record> - <!-- Product Suppliers--> <record id="view_product_supplier_inherit" model="ir.ui.view"> diff --git a/addons/purchase/views/stock_views.xml b/addons/purchase/views/stock_views.xml index d7982554e7e..1981678e7b8 100644 --- a/addons/purchase/views/stock_views.xml +++ b/addons/purchase/views/stock_views.xml @@ -21,5 +21,20 @@ </xpath> </field> </record> + + <!-- Purchase button on orderpoints --> + + <record id="view_warehouse_orderpoint_purchase_form" model="ir.ui.view"> + <field name="name">stock.warehouse.orderpoint.purchase.form</field> + <field name="model">stock.warehouse.orderpoint</field> + <field name="inherit_id" ref="stock.view_warehouse_orderpoint_form"/> + <field name="arch" type="xml"> + <div name="button_box" position="inside"> + <button name="%(purchase.purchase_order_action_generic)d" type="action" + string="Purchases" class="oe_stat_button" icon="fa-refresh" + domain="[('orderpoint_id','=',active_id)]"/> + </div> + </field> + </record> </odoo> diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index 7ad5fc29bed..c0c440954a8 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -58,7 +58,6 @@ class PurchaseRequisition(models.Model): company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env['res.company']._company_default_get('purchase.requisition')) purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]}) line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True) - procurement_id = fields.Many2one('procurement.order', string='Procurement', ondelete='set null', copy=False) warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse') state = fields.Selection([('draft', 'Draft'), ('in_progress', 'Confirmed'), ('open', 'Bid Selection'), ('done', 'Done'), @@ -121,6 +120,7 @@ class PurchaseRequisitionLine(models.Model): company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default= lambda self: self.env['res.company']._company_default_get('purchase.requisition.line')) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') schedule_date = fields.Date(string='Scheduled Date') + move_dest_id = fields.Many2one('stock.move', 'Downstream Move') @api.multi @api.depends('requisition_id.purchase_ids.state') @@ -157,8 +157,8 @@ class PurchaseRequisitionLine(models.Model): 'price_unit': price_unit, 'taxes_id': [(6, 0, taxes_ids)], 'date_planned': requisition.schedule_date or fields.Date.today(), - 'procurement_ids': [(6, 0, [requisition.procurement_id.id])] if requisition.procurement_id else False, 'account_analytic_id': self.account_analytic_id.id, + 'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or [] } @@ -242,17 +242,12 @@ class PurchaseOrder(models.Model): def button_confirm(self): res = super(PurchaseOrder, self).button_confirm() for po in self: + if not po.requisition_id: + continue if po.requisition_id.type_id.exclusive == 'exclusive': others_po = po.requisition_id.mapped('purchase_ids').filtered(lambda r: r.id != po.id) others_po.button_cancel() po.requisition_id.action_done() - - for element in po.order_line: - if element.product_id == po.requisition_id.procurement_id.product_id: - element.move_ids.write({ - 'procurement_id': po.requisition_id.procurement_id.id, - 'move_dest_id': po.requisition_id.procurement_id.move_dest_id.id, - }) return res @api.model @@ -301,41 +296,24 @@ class ProductTemplate(models.Model): string='Procurement', default='rfq') -class ProcurementOrder(models.Model): - _inherit = 'procurement.order' - - requisition_id = fields.Many2one('purchase.requisition', string='Latest Requisition') +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' @api.multi - def make_po(self): - Requisition = self.env['purchase.requisition'] - procurements = self.env['procurement.order'] - Warehouse = self.env['stock.warehouse'] - res = [] - for procurement in self: - if procurement.product_id.purchase_requisition == 'tenders': - warehouse_id = Warehouse.search([('company_id', '=', procurement.company_id.id)], limit=1).id - requisition_id = Requisition.create({ - 'origin': procurement.origin, - 'date_end': procurement.date_planned, - 'warehouse_id': warehouse_id, - 'company_id': procurement.company_id.id, - 'procurement_id': procurement.id, - 'picking_type_id': procurement.rule_id.picking_type_id.id, - 'line_ids': [(0, 0, { - 'product_id': procurement.product_id.id, - 'product_uom_id': procurement.product_uom.id, - 'product_qty': procurement.product_qty - })], - }) - procurement.message_post(body=_("Purchase Requisition created")) - requisition_id.message_post_with_view('mail.message_origin_link', - values={'self': requisition_id, 'origin': procurement}, - subtype_id=self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')) - procurement.requisition_id = requisition_id - procurements += procurement - res += [procurement.id] - set_others = self - procurements - if set_others: - res += super(ProcurementOrder, set_others).make_po() - return res + def _run_buy(self, product_id, product_qty, product_uom, location_id, name, origin, values, log_activites=False): + if product_id.purchase_requisition != 'tenders': + return super(ProcurementGroup, self)._run_buy(product_id, product_qty, product_uom, location_id, name, origin, values, log_activites) + self.env['purchase.requisition'].create({ + 'origin': origin, + 'date_end': values['date_planned'], + 'warehouse_id': values.get('warehouse_id') and values['warehouse_id'].id or False, + 'company_id': values['company_id'].id, + 'picking_type_id': self.picking_type_id.id, + 'line_ids': [(0, 0, { + 'product_id': product_id.id, + 'product_uom_id': product_uom.id, + 'product_qty': product_qty, + 'move_dest_id': values.get('move_dest_ids') and values['move_dest_ids'][0].id or False, + })], + }) + return True diff --git a/addons/purchase_requisition/tests/test_purchase_requisition.py b/addons/purchase_requisition/tests/test_purchase_requisition.py index 67d32b7b3c6..9ba09ca390d 100644 --- a/addons/purchase_requisition/tests/test_purchase_requisition.py +++ b/addons/purchase_requisition/tests/test_purchase_requisition.py @@ -3,6 +3,7 @@ from odoo.tests import common from odoo.tools import float_compare +from odoo import fields class TestPurchaseRequisition(common.TransactionCase): @@ -43,22 +44,26 @@ class TestPurchaseRequisition(common.TransactionCase): self.requisition1.sudo(self.res_users_purchase_requisition_user.id).copy() def test_02_purchase_requisition(self): - # Create the procurement order and run that procurement. - procurement_product_hdd3 = self.env['make.procurement'].create({'product_id': self.product_13_id, 'qty': 15, 'uom_id': self.ref('product.product_uom_unit'), 'warehouse_id': self.ref('stock.warehouse0')}) - procurement_product_hdd3.make_procurement() - # Run the scheduler. - ProcurementOrder = self.env['procurement.order'] - ProcurementOrder.run_scheduler() + date_planned = fields.Datetime.now() + warehouse = self.env['stock.warehouse'].browse(self.ref('stock.warehouse0')) + product = self.env['product.product'].browse(self.product_13_id) + product.write({'route_ids': [(4, self.ref('purchase.route_warehouse0_buy'))]}) + self.env['procurement.group'].run({'product_id': product, + 'product_qty': 14, + 'product_uom': self.env['product.uom'].browse(self.ref('product.product_uom_unit')), + 'warehouse_id': warehouse, + 'location_id': warehouse.lot_stock_id, + 'name': '/', + 'origin': '/', + 'date_planned': date_planned, + }) + # Check requisition details which created after run procurement. - procurements = ProcurementOrder.search([('requisition_id', '!=', False)]) - for procurement in procurements: - requisition = procurement.requisition_id - self.assertEqual(requisition.date_end, procurement.date_planned, "End date is not correspond.") - self.assertEqual(len(requisition.line_ids), 1, "Requisition Lines should be one.") - line = requisition.line_ids[0] - self.assertEqual(line.product_id.id, procurement.product_id.id, "Product is not correspond.") - self.assertEqual(line.product_uom_id.id, procurement.product_uom.id, "UOM is not correspond.") - self.assertEqual(float_compare(line.product_qty, procurement.product_qty, precision_digits=2), 0, "Quantity is not correspond.") + line = self.env['purchase.requisition.line'].search([('product_id', '=', self.product_13_id), ('product_qty', '=', 14.0)]) + requisition = line[0].requisition_id + self.assertEqual(requisition.date_end, date_planned, "End date does not correspond.") + self.assertEqual(len(requisition.line_ids), 1, "Requisition Lines should be one.") + self.assertEqual(line.product_uom_id.id, self.ref('product.product_uom_unit'), "UOM is not correspond.") # Give access rights of Purchase Requisition User to open requisition # Set tender state to choose tendering line. diff --git a/addons/purchase_requisition/views/purchase_requisition_views.xml b/addons/purchase_requisition/views/purchase_requisition_views.xml index 830a825d5d4..3c21493b04d 100644 --- a/addons/purchase_requisition/views/purchase_requisition_views.xml +++ b/addons/purchase_requisition/views/purchase_requisition_views.xml @@ -86,9 +86,6 @@ <field name="requisition_id" domain="[('state', 'in', ('in_progress', 'open')), ('vendor_id', 'in', (partner_id, False))]"/> </field> - <xpath expr="//field[@name='order_line']/tree" position="inside"> - <field name="procurement_ids" invisible="1"/> - </xpath> </field> </record> @@ -336,16 +333,6 @@ </field> </record> - <record model="ir.ui.view" id="procurement_normal_form_view_inherit"> - <field name="name">procurement.form.inherit</field> - <field name="model">procurement.order</field> - <field name="inherit_id" ref="procurement.procurement_form_view"/> - <field name="arch" type="xml"> - <xpath expr="//field[@name='origin']" position="after"> - <field name="requisition_id"/> - </xpath> - </field> - </record> <act_window domain="[('requisition_id', '=', active_id)]" context="{'default_requisition_id': active_id}" diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index c70b8ba17ed..5860dabd53c 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -9,7 +9,7 @@ 'description': """ This module contains all the common features of Sales Management and eCommerce. """, - 'depends': ['sales_team', 'account', 'procurement', 'portal'], + 'depends': ['sales_team', 'account', 'portal'], 'data': [ 'data/ir_sequence_data.xml', 'data/sale_data.xml', @@ -39,4 +39,4 @@ This module contains all the common features of Sales Management and eCommerce. 'css': ['static/src/css/sale.css'], 'installable': True, 'auto_install': False, -}
\ No newline at end of file +} diff --git a/addons/sale/models/__init__.py b/addons/sale/models/__init__.py index ec077e25adc..e5c5e1ecb51 100644 --- a/addons/sale/models/__init__.py +++ b/addons/sale/models/__init__.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import account_invoice -from . import procurement_order from . import product_pricelist from . import product_product from . import product_template @@ -12,4 +11,4 @@ from . import sale from . import sale_analytic from . import res_config_settings from . import sale_layout -from . import sales_team
\ No newline at end of file +from . import sales_team diff --git a/addons/sale/models/procurement_order.py b/addons/sale/models/procurement_order.py deleted file mode 100644 index a8ad9c09696..00000000000 --- a/addons/sale/models/procurement_order.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models - - -class ProcurementOrder(models.Model): - _inherit = 'procurement.order' - sale_line_id = fields.Many2one('sale.order.line', string='Sales Order Line') diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 475e0b034e8..13d2effa460 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -154,15 +154,14 @@ class SaleOrder(models.Model): note = fields.Text('Terms and conditions', default=_default_note) - amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always') - amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all', track_visibility='always') + amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='onchange') + amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all') amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all', track_visibility='always') payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term') fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position') company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('sale.order')) team_id = fields.Many2one('crm.team', 'Sales Channel', change_default=True, default=_get_default_team, oldname='section_id') - procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') @@ -428,9 +427,7 @@ class SaleOrder(models.Model): orders = self.filtered(lambda s: s.state in ['cancel', 'sent']) orders.write({ 'state': 'draft', - 'procurement_group_id': False, }) - orders.mapped('order_line').mapped('procurement_ids').write({'sale_line_id': False}) @api.multi def action_cancel(self): @@ -490,19 +487,16 @@ class SaleOrder(models.Model): def action_unlock(self): self.write({'state': 'sale'}) - def _prepare_procurement_group(self): - return {'name': self.name} - @api.multi def action_confirm(self): for order in self.filtered(lambda order: order.partner_id not in order.message_partner_ids): order.message_subscribe([order.partner_id.id]) - for order in self: - order.state = 'sale' - order.confirmation_date = fields.Datetime.now() - if self.env.context.get('send_email'): - self.force_quotation_send() - order.order_line._action_procurement_create() + self.write({ + 'state': 'sale', + 'confirmation_date': fields.Datetime.now() + }) + if self.env.context.get('send_email'): + self.force_quotation_send() if self.env['ir.config_parameter'].get_param('sale.auto_done_setting'): self.action_done() return True @@ -747,52 +741,6 @@ class SaleOrderLine(models.Model): taxes = line.product_id.taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id) line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_shipping_id) if fpos else taxes - @api.multi - def _prepare_order_line_procurement(self, group_id=False): - self.ensure_one() - return { - 'name': self.name, - 'origin': self.order_id.name, - 'date_planned': datetime.strptime(self.order_id.confirmation_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=self.customer_lead), - 'product_id': self.product_id.id, - 'product_qty': self.product_uom_qty, - 'product_uom': self.product_uom.id, - 'company_id': self.order_id.company_id.id, - 'group_id': group_id, - 'sale_line_id': self.id - } - - @api.multi - def _action_procurement_create(self): - """ - Create procurements based on quantity ordered. If the quantity is increased, new - procurements are created. If the quantity is decreased, no automated action is taken. - """ - precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - new_procs = self.env['procurement.order'] # Empty recordset - for line in self: - if line.state != 'sale' or not line.product_id._need_procurement(): - continue - qty = 0.0 - for proc in line.procurement_ids: - qty += proc.product_qty - if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: - continue - - if not line.order_id.procurement_group_id: - vals = line.order_id._prepare_procurement_group() - line.order_id.procurement_group_id = self.env["procurement.group"].create(vals) - - vals = line._prepare_order_line_procurement(group_id=line.order_id.procurement_group_id.id) - vals['product_qty'] = line.product_uom_qty - qty - new_proc = self.env["procurement.order"].with_context(procurement_autorun_defer=True).create(vals) - new_proc.message_post_with_view('mail.message_origin_link', - values={'self': new_proc, 'origin': line.order_id}, - subtype_id=self.env.ref('mail.mt_note').id) - new_procs += new_proc - new_procs.run() - return new_procs - @api.model def _get_purchase_price(self, pricelist, product, product_uom, date): return {} @@ -815,7 +763,6 @@ class SaleOrderLine(models.Model): values.update(self._prepare_add_missing_fields(values)) line = super(SaleOrderLine, self).create(values) if line.state == 'sale': - line._action_procurement_create() msg = _("Extra line with %s ") % (line.product_id.display_name,) line.order_id.message_post(body=msg) @@ -850,8 +797,6 @@ class SaleOrderLine(models.Model): self.filtered( lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) != 0)._update_line_quantity(values) result = super(SaleOrderLine, self).write(values) - if lines: - lines._action_procurement_create() return result order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False) @@ -913,7 +858,6 @@ class SaleOrderLine(models.Model): customer_lead = fields.Float( 'Delivery Lead Time', required=True, default=0.0, help="Number of days between the order confirmation and the shipping of the products to the customer", oldname="delay") - procurement_ids = fields.One2many('procurement.order', 'sale_line_id', string='Procurements') amt_to_invoice = fields.Monetary(string='Amount To Invoice', compute='_compute_invoice_amount', store=True) amt_invoiced = fields.Monetary(string='Amount Invoiced', compute='_compute_invoice_amount', store=True) diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 7d6396716a5..24e97c177a2 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -252,13 +252,12 @@ <group> <field name="product_id" context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}" - attrs="{'readonly': ['|', ('qty_invoiced', '>', 0), ('procurement_ids', '!=', [])]}" + attrs="{'readonly': [('qty_invoiced', '>', 0)]}" /> <field name="layout_category_id" groups="sale.group_sale_layout"/> <field name="invoice_status" invisible="1"/> <field name="qty_to_invoice" invisible="1"/> <field name="qty_delivered_updateable" invisible="1"/> - <field name="procurement_ids" invisible="1"/> <field name="price_subtotal" invisible="1"/> <label for="product_uom_qty" string="Ordered Quantity"/> <div> @@ -306,7 +305,7 @@ <tree string="Sales Order Lines" editable="bottom" decoration-info="invoice_status=='to invoice'"> <field name="sequence" widget="handle"/> <field name="product_id" - attrs="{'readonly': ['|', ('qty_invoiced', '>', 0), ('procurement_ids', '!=', [])]}" + attrs="{'readonly': [('qty_invoiced', '>', 0)]}" context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}" /> <field name="layout_category_id" groups="sale.group_sale_layout"/> @@ -333,7 +332,6 @@ <field name="price_subtotal" widget="monetary" groups="sale.group_show_price_subtotal"/> <field name="price_total" widget="monetary" groups="sale.group_show_price_total"/> <field name="qty_delivered_updateable" invisible="1"/> - <field name="procurement_ids" invisible="1"/> <field name="state" invisible="1"/> <field name="invoice_status" invisible="1"/> <field name="customer_lead" invisible="1"/> diff --git a/addons/sale_mrp/__init__.py b/addons/sale_mrp/__init__.py index eb7b66b4c4b..dc5e6b693d1 100644 --- a/addons/sale_mrp/__init__.py +++ b/addons/sale_mrp/__init__.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import sale_mrp from . import models diff --git a/addons/sale_mrp/__manifest__.py b/addons/sale_mrp/__manifest__.py index 9982c1cf6ba..0a68dd48fef 100644 --- a/addons/sale_mrp/__manifest__.py +++ b/addons/sale_mrp/__manifest__.py @@ -20,7 +20,6 @@ from sales order. It adds sales name and sales Reference on production order. ], 'demo': [], 'test':[ - 'test/cancellation_propagated.yml', 'test/sale_mrp.yml', ], 'installable': True, diff --git a/addons/sale_mrp/models/__init__.py b/addons/sale_mrp/models/__init__.py index c9f7c35fe54..75e0d652578 100644 --- a/addons/sale_mrp/models/__init__.py +++ b/addons/sale_mrp/models/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import procurement
\ No newline at end of file +from . import sale_mrp diff --git a/addons/sale_mrp/models/procurement.py b/addons/sale_mrp/models/procurement.py deleted file mode 100644 index 0606f9e14fe..00000000000 --- a/addons/sale_mrp/models/procurement.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models - - -class ProcurementOrder(models.Model): - _inherit = 'procurement.order' - - @api.multi - def make_mo(self): - """ override method to set link in production created from sale order.""" - res = super(ProcurementOrder, self).make_mo() - for procurement_id in res: - production_id = res[procurement_id] - if production_id: - production = self.env['mrp.production'].browse(production_id) - move = production._get_parent_move(production.move_finished_ids[0]) - sale_order = move.procurement_id.sale_line_id.order_id - if sale_order: - production.message_post_with_view('mail.message_origin_link', - values={'self': production, 'origin': sale_order}, - subtype_id=self.env.ref('mail.mt_note').id) - return res diff --git a/addons/sale_mrp/sale_mrp.py b/addons/sale_mrp/models/sale_mrp.py index edc9a72a383..20ddf2c3fc1 100644 --- a/addons/sale_mrp/sale_mrp.py +++ b/addons/sale_mrp/models/sale_mrp.py @@ -5,25 +5,6 @@ from odoo import api, fields, models from odoo.tools import float_compare -class MrpProduction(models.Model): - _inherit = 'mrp.production' - - sale_name = fields.Char(compute='_compute_sale_name_sale_ref', string='Sale Name', help='Indicate the name of sales order.') - sale_ref = fields.Char(compute='_compute_sale_name_sale_ref', string='Sale Reference', help='Indicate the Customer Reference from sales order.') - - def _get_parent_move(self, move): - if move.move_dest_ids: - return self._get_parent_move(move.move_dest_ids[0]) - return move - - @api.multi - def _compute_sale_name_sale_ref(self): - for production in self: - move = production._get_parent_move(production.move_finished_ids[0]) - production.sale_name = move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.name or False - production.sale_ref = move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.client_order_ref or False - - class SaleOrderLine(models.Model): _inherit = 'sale.order.line' @@ -35,7 +16,7 @@ class SaleOrderLine(models.Model): # have changed, we don't compute the quantities but verify the move state. bom = self.env['mrp.bom']._bom_find(product=self.product_id) if bom and bom.type == 'phantom': - bom_delivered = all([move.state == 'done' for move in self.procurement_ids.mapped('move_ids')]) + bom_delivered = all([move.state == 'done' for move in self.move_ids]) if bom_delivered: return self.product_uom_qty else: @@ -81,9 +62,7 @@ class AccountInvoiceLine(models.Model): qty_done = sum([x.uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in s_line.invoice_lines if x.invoice_id.state in ('open', 'paid')]) quantity = self.uom_id._compute_quantity(self.quantity, self.product_id.uom_id) # Put moves in fixed order by date executed - moves = self.env['stock.move'] - for procurement in s_line.procurement_ids: - moves |= procurement.move_ids + moves = s_line.move_ids moves.sorted(lambda x: x.date) # Go through all the moves and do nothing until you get to qty_done # Beyond qty_done we need to calculate the average of the price_unit diff --git a/addons/sale_mrp/test/cancellation_propagated.yml b/addons/sale_mrp/test/cancellation_propagated.yml deleted file mode 100644 index 500f1efd0c7..00000000000 --- a/addons/sale_mrp/test/cancellation_propagated.yml +++ /dev/null @@ -1,92 +0,0 @@ -- - I first create a warehouse with pick-pack-ship and reception in 2 steps -- - !record {model: stock.warehouse, id: wh_pps}: - name: WareHouse PickPackShip - code: whpps - reception_steps: 'two_steps' - delivery_steps: 'pick_pack_ship' - manufacture_to_resupply: True -- - Next I create a new product in this warehouse -- - !record {model: product.product, id: product_manu}: - name: "My MTO Product" - type: product - uom_id: product.product_uom_unit - uom_po_id: product.product_uom_unit -- - I create a bom for this product -- - !record {model: mrp.bom, id: mrp_bom_test1}: - company_id: base.main_company - product_tmpl_id: product_manu_product_template - product_id: product_manu - product_uom_id: product.product_uom_unit - product_qty: 1.0 - type: normal - bom_line_ids: - - product_id: product.consu_delivery_01 - product_uom_id: product.product_uom_unit - product_qty: 1.0 -- - And set routes on product to be MTO and manufacture -- - !python {model: product.product, id: product_manu}: | - route_warehouse0_manufacture_id = self.env['stock.warehouse'].browse(ref('stock.warehouse0')).manufacture_pull_id.route_id.id - route_warehouse0_mto_id = self.env['stock.warehouse'].browse(ref('stock.warehouse0')).mto_pull_id.route_id.id - self.write({'route_ids': [(6, 0, [route_warehouse0_mto_id, route_warehouse0_manufacture_id])]}) -- - I create a sales order -- - !record {model: sale.order, id: sale_order_product_manu}: - partner_id: base.res_partner_3 - note: Create Sales order - warehouse_id: wh_pps - pricelist_id: product.list0 - order_line: - - product_id: product_manu - name: "product_manu" - product_uom_qty: 5.00 - product_uom: product.product_uom_unit - -- - Confirm sales order -- - !python {model: sale.order, id: sale_order_product_manu}: | - self.action_confirm() -- - I run scheduler. -- - !python {model: procurement.order, id: False}: | - self.run_scheduler() -- - Check the propagation when we cancel the main procurement - * Retrieve related procurements and check that there are all running - * Check that the purchase order has been well created - * Cancel the main procurement - * Check that all procurements related and the purchase order has been well cancelled -- - !python {model: procurement.order, id: False}: | - # Retrieve related procu - so = self.env['sale.order'].browse(ref('sale_order_product_manu')) - procus = self.search([('group_id.name', '=', so.name)]) - assert len(procus.ids)>0, 'No procurements are found for sale order "%s" (with id : %d)' %(so.name, so.id) - - # Check that all procurements are running - for procu in procus: - assert procu.state == u'running', 'Procurement with id %d should be "running" but is with a state : %s!' %(procu.id, procu.state) - - # Check that one production order exist - prodor_ids = [proc.production_id for proc in procus if proc.production_id] - assert len(prodor_ids) > 0, 'No production order found !' - - # Cancel the main procurement - main_procu = self.search([('origin', '=', so.name)]) - assert len(main_procu.ids) == 1, 'Main procurement not identified !' - main_procu.cancel() - assert main_procu[0].state == u'cancel', 'Main procurement should be cancelled !' - - # Check that the production order is running - for prodor in self.env['mrp.production'].browse([prodor.id for prodor in prodor_ids]): - assert prodor.state == u'confirmed', 'Production order %d should be running since the pick does not propagate but is in state : %s!' %(prodor.id, prodor.state) diff --git a/addons/sale_mrp/test/sale_mrp.yml b/addons/sale_mrp/test/sale_mrp.yml index 6a155be42ed..945e4d1414a 100644 --- a/addons/sale_mrp/test/sale_mrp.yml +++ b/addons/sale_mrp/test/sale_mrp.yml @@ -79,34 +79,9 @@ - !python {model: sale.order, id: sale_order_so0}: | self.action_confirm() -- - I verify that a procurement has been generated for sale order -- - !python {model: procurement.order, id: False}: | - so = self.env['sale.order'].browse(ref("sale_order_so0")) - procs = self.search([('origin','=',so.name)]) - assert procs, 'No Procurements!' -- - Then I click on the "Run Procurement" button -- - !python {model: procurement.order, id: False}: | - so = self.env['sale.order'].browse(ref("sale_order_so0")) - procs = self.search([('origin','like',so.name)]) - procs.run() -- - I verify that a procurement state is "running" -- - !python {model: procurement.order, id: False}: | - so = self.env['sale.order'].browse(ref("sale_order_so0")) - procs = self.search([('origin','like',so.name)]) - # Check that all procurement are running - for procu in procs: - assert procu.state == u'running', 'Procurement with id %d should be with a state "running" but is with a state : %s!' %(procu.id,procu.state) -- +- I verify that a manufacturing order has been generated, and that its name and reference are correct - !python {model: sale.order, id: sale_order_so0}: | mo = self.env['mrp.production'].search([('origin','like',self.name)], limit=1) assert mo, 'Manufacturing order has not been generated' - assert mo.sale_name == self.name, 'Wrong Name for the Manufacturing Order. Expected %s, Got %s' % (self.name, mo.sale_name) - assert mo.sale_ref == self.client_order_ref, 'Wrong Sale Reference for the Manufacturing Order' diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 68bba7bdf94..5ab02b673ce 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -20,7 +20,6 @@ class TestSaleMrpFlow(common.TransactionCase): self.ProductUom = self.env['product.uom'] self.MrpProduction = self.env['mrp.production'] self.Product = self.env['product.product'] - self.ProcurementOrder = self.env['procurement.order'] self.Inventory = self.env['stock.inventory'] self.InventoryLine = self.env['stock.inventory.line'] self.ProductProduce = self.env['mrp.product.produce'] @@ -30,7 +29,6 @@ class TestSaleMrpFlow(common.TransactionCase): self.categ_kgm = self.env.ref('product.product_uom_categ_kgm') self.stock_location = self.env.ref('stock.stock_location_stock') self.warehouse = self.env.ref('stock.warehouse0') - self.procurement_jit = self.env.ref('base.module_procurement_jit') def test_00_sale_mrp_flow(self): """ Test sale to mrp flow with diffrent unit of measure.""" @@ -163,14 +161,10 @@ class TestSaleMrpFlow(common.TransactionCase): # Check manufacturing order for product A. # <><><><><><><><><><><><><><><><><><><><> - # Run procurement. - # ---------------- - self.ProcurementOrder.run_scheduler() - - mnf_product_a = self.ProcurementOrder.search([('product_id', '=', product_a.id), ('group_id', '=', order.procurement_group_id.id), ('production_id', '!=', False)]).production_id - # Check quantity, unit of measure and state of manufacturing order. # ----------------------------------------------------------------- + self.env['procurement.group'].run_scheduler() + mnf_product_a = self.env['mrp.production'].search([('product_id', '=', product_a.id)]) self.assertTrue(mnf_product_a, 'Manufacturing order not created.') self.assertEqual(mnf_product_a.product_qty, 10, 'Wrong product quantity in manufacturing order.') @@ -226,11 +220,8 @@ class TestSaleMrpFlow(common.TransactionCase): # Manufacturing order for product D (20 unit). # <><><><><><><><><><><><><><><><><><><><><><> - procurement_d = self.ProcurementOrder.search([('product_id', '=', product_d.id), ('group_id', '=', order.procurement_group_id.id)]) - # Check total consume line with product c and uom kg. - self.assertEqual(len(procurement_d), 1, 'Procurement order not generated.') - self.assertTrue(procurement_d.production_id, 'Production order not generated from procurement.') - mnf_product_d = procurement_d.production_id + # FP Todo: find a better way to look for the production order + mnf_product_d = self.MrpProduction.search([('product_id', '=', product_d.id), ('move_dest_ids.group_id', '=', order.procurement_group_id.id)], order='id desc', limit=1) # Check state of production order D. self.assertEqual(mnf_product_d.state, 'confirmed', 'Manufacturing order should be confirmed.') diff --git a/addons/sale_mrp/tests/test_sale_mrp_lead_time.py b/addons/sale_mrp/tests/test_sale_mrp_lead_time.py index 5cdaf6d51e9..8455226238f 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_lead_time.py +++ b/addons/sale_mrp/tests/test_sale_mrp_lead_time.py @@ -63,16 +63,10 @@ class TestSaleMrpLeadTime(TestStockCommon): # Confirm sale order order.action_confirm() - # Run scheduler - self.env['procurement.order'].run_scheduler() - # Check manufacturing order created or not - manufacturing_order = self.env['procurement.order'].search([('product_id', '=', self.product_1.id), ('group_id', '=', order.procurement_group_id.id), ('production_id', '!=', False)]).production_id + manufacturing_order = self.env['mrp.production'].search([('product_id', '=', self.product_1.id), ('move_dest_ids', 'in', order.picking_ids[0].move_lines.ids)]) self.assertTrue(manufacturing_order, 'Manufacturing order should be created.') - # Check the picking crated or not - self.assertTrue(order.picking_ids, "Picking should be created.") - # Check schedule date of picking out_date = fields.Datetime.from_string(order.date_order) + timedelta(days=self.product_1.sale_delay) - timedelta(days=company.security_lead) min_date = fields.Datetime.from_string(order.picking_ids[0].scheduled_date) @@ -112,10 +106,10 @@ class TestSaleMrpLeadTime(TestStockCommon): order.action_confirm() # Run scheduler - self.env['procurement.order'].run_scheduler() + self.env['procurement.group'].run_scheduler() # Check manufacturing order created or not - manufacturing_order = self.env['procurement.order'].search([('product_id', '=', self.product_1.id), ('group_id', '=', order.procurement_group_id.id), ('production_id', '!=', False)]).production_id + manufacturing_order = self.env['mrp.production'].search([('product_id', '=', self.product_1.id)]) self.assertTrue(manufacturing_order, 'Manufacturing order should be created.') # Check the picking crated or not diff --git a/addons/sale_order_dates/tests/test_requested_date.py b/addons/sale_order_dates/tests/test_requested_date.py index f6191176f1d..7ed58f42f27 100644 --- a/addons/sale_order_dates/tests/test_requested_date.py +++ b/addons/sale_order_dates/tests/test_requested_date.py @@ -19,8 +19,4 @@ class TestSaleOrderDates(common.TransactionCase): requested_date = fields.Datetime.from_string(new_order.requested_date) right_date = fields.Datetime.to_string(requested_date - security_delay) for line in new_order.order_line: - self.assertNotEqual(len(line.procurement_ids), 0, "No Procurement was created") - procurement = line.procurement_ids[0] - self.assertEqual(procurement.date_planned, right_date, "The planned date for the Procurement Order is wrong") - self.assertNotEqual(len(procurement.move_ids), 0, "No Move was created") - self.assertEqual(procurement.move_ids[0].date_expected, right_date, "The expected date for the Stock Move is wrong") + self.assertEqual(line.move_ids[0].date_expected, right_date, "The expected date for the Stock Move is wrong") diff --git a/addons/sale_stock/models/__init__.py b/addons/sale_stock/models/__init__.py index bdec99be319..aa85402192a 100644 --- a/addons/sale_stock/models/__init__.py +++ b/addons/sale_stock/models/__init__.py @@ -2,8 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import account_invoice -from . import procurement -from . import product from . import res_company from . import sale_order from . import res_config_settings diff --git a/addons/sale_stock/models/account_invoice.py b/addons/sale_stock/models/account_invoice.py index 96f35cf5424..c503d35d21f 100644 --- a/addons/sale_stock/models/account_invoice.py +++ b/addons/sale_stock/models/account_invoice.py @@ -26,8 +26,7 @@ class AccountInvoiceLine(models.Model): quantity = self.uom_id._compute_quantity(self.quantity, self.product_id.uom_id) # Put moves in fixed order by date executed moves = self.env['stock.move'] - for procurement in s_line.procurement_ids: - moves |= procurement.move_ids + moves |=_line.move_ids moves.sorted(lambda x: x.date) # Go through all the moves and do nothing until you get to qty_done # Beyond qty_done we need to calculate the average of the price_unit diff --git a/addons/sale_stock/models/procurement.py b/addons/sale_stock/models/procurement.py deleted file mode 100644 index 5710bff5af6..00000000000 --- a/addons/sale_stock/models/procurement.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models, fields - - -class ProcurementOrder(models.Model): - _inherit = "procurement.order" - - def _run_move_create(self): - vals = super(ProcurementOrder, self)._run_move_create() - if self.sale_line_id: - vals.update({'sequence': self.sale_line_id.sequence}) - return vals - -class ProcurementGroup(models.Model): - _inherit = 'procurement.group' - sale_order_id = fields.Many2one('sale.order', string='Sale Order') diff --git a/addons/sale_stock/models/product.py b/addons/sale_stock/models/product.py deleted file mode 100644 index a3c3b678cbf..00000000000 --- a/addons/sale_stock/models/product.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models - - -class ProductProduct(models.Model): - _inherit = 'product.product' - - @api.multi - def _need_procurement(self): - for product in self: - if product.type not in ['service', 'digital']: - return True - return super(ProductProduct, self)._need_procurement() diff --git a/addons/sale_stock/models/res_config_settings.py b/addons/sale_stock/models/res_config_settings.py index bd15f1e904d..a6e006caec2 100644 --- a/addons/sale_stock/models/res_config_settings.py +++ b/addons/sale_stock/models/res_config_settings.py @@ -15,7 +15,7 @@ class ResConfigSettings(models.TransientModel): use_security_lead = fields.Boolean( string="Security Lead Time for Sales", oldname='default_new_security_lead', - help="Margin of error for dates promised to customers. Products will be scheduled for procurement and delivery that many days earlier than the actual promised date, to cope with unexpected delays in the supply chain.") + help="Margin of error for dates promised to customers. Products will be scheduled for delivery that many days earlier than the actual promised date, to cope with unexpected delays in the supply chain.") default_picking_policy = fields.Selection([ ('direct', 'Ship products as soon as available, with back orders'), ('one', 'Ship all products at once') diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 16c122cb9af..701ff26261f 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -29,14 +29,20 @@ class SaleOrder(models.Model): 'stock.warehouse', string='Warehouse', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, default=_default_warehouse_id) - picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this sale') + picking_ids = fields.One2many('stock.picking', 'sale_id', string='Pickings') delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') + procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) @api.multi - @api.depends('procurement_group_id') + def action_confirm(self): + result = super(SaleOrder, self).action_confirm() + for order in self: + order.order_line._action_procurement_create() + return result + + @api.depends('picking_ids') def _compute_picking_ids(self): for order in self: - order.picking_ids = self.env['stock.picking'].search([('group_id', '=', order.procurement_group_id.id)]) if order.procurement_group_id else [] order.delivery_count = len(order.picking_ids) @api.onchange('warehouse_id') @@ -52,18 +58,12 @@ class SaleOrder(models.Model): view, if there is only one delivery order to show. ''' action = self.env.ref('stock.action_picking_tree_all').read()[0] - - pickings = self.mapped('picking_ids') - if len(pickings) > 1: - action['domain'] = [('id', 'in', pickings.ids)] - elif pickings: - action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] - action['res_id'] = pickings.id + action['domain'] = [('id', 'in', self.picking_ids.ids)] return action @api.multi def action_cancel(self): - self.mapped('order_line').mapped('procurement_ids').cancel() + self.picking_ids.action_cancel() return super(SaleOrder, self).action_cancel() @api.multi @@ -72,11 +72,6 @@ class SaleOrder(models.Model): invoice_vals['incoterms_id'] = self.incoterm.id or False return invoice_vals - def _prepare_procurement_group(self): - res = super(SaleOrder, self)._prepare_procurement_group() - res.update({'move_type': self.picking_policy, 'partner_id': self.partner_shipping_id.id, 'sale_order_id': self.id}) - return res - @api.model def _get_customer_lead(self, product_tmpl_id): super(SaleOrder, self)._get_customer_lead(product_tmpl_id) @@ -88,6 +83,27 @@ class SaleOrderLine(models.Model): product_packaging = fields.Many2one('product.packaging', string='Package', default=False) route_id = fields.Many2one('stock.location.route', string='Route', domain=[('sale_selectable', '=', True)], ondelete='restrict') + move_ids = fields.One2many('stock.move', 'sale_line_id', string='Stock Moves') + + @api.model + def create(self, values): + line = super(SaleOrderLine, self).create(values) + if line.state == 'sale': + line._action_procurement_create() + return line + + @api.multi + def write(self, values): + lines = False + if 'product_uom_qty' in values: + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + lines = self.filtered( + lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) + res = super(SaleOrderLine, self).write(values) + if lines: + lines._action_procurement_create() + return res + @api.depends('order_id.state') def _compute_invoice_status(self): @@ -101,8 +117,8 @@ class SaleOrderLine(models.Model): and line.invoice_status == 'no'\ and line.product_id.type in ['consu', 'product']\ and line.product_id.invoice_policy == 'delivery'\ - and line.procurement_ids.mapped('move_ids')\ - and all(move.state in ['done', 'cancel'] for move in line.procurement_ids.mapped('move_ids')): + and line.move_ids \ + and all(move.state in ['done', 'cancel'] for move in line.move_ids): line.invoice_status = 'invoiced' @api.multi @@ -171,27 +187,59 @@ class SaleOrderLine(models.Model): @api.multi def _prepare_order_line_procurement(self, group_id=False): - vals = super(SaleOrderLine, self)._prepare_order_line_procurement(group_id=group_id) + self.ensure_one() date_planned = datetime.strptime(self.order_id.confirmation_date, DEFAULT_SERVER_DATETIME_FORMAT)\ + timedelta(days=self.customer_lead or 0.0) - timedelta(days=self.order_id.company_id.security_lead) - vals.update({ - 'date_planned': date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - 'location_id': self.order_id.partner_shipping_id.property_stock_customer.id, - 'route_ids': self.route_id and [(4, self.route_id.id)] or [], - 'warehouse_id': self.order_id.warehouse_id and self.order_id.warehouse_id.id or False, - 'partner_dest_id': self.order_id.partner_shipping_id.id, + return { + 'name': self.name, + 'origin': self.order_id.name, + 'product_id': self.product_id, + 'product_qty': self.product_uom_qty, + 'product_uom': self.product_uom, + 'company_id': self.order_id.company_id, + 'group_id': group_id, 'sale_line_id': self.id, - }) - return vals + 'date_planned': date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'location_id': self.order_id.partner_shipping_id.property_stock_customer, + 'route_ids': self.route_id, + 'warehouse_id': self.order_id.warehouse_id or False, + 'partner_dest_id': self.order_id.partner_shipping_id + } @api.multi - def _get_delivered_qty(self): - """Computes the delivered quantity on sales order lines, based on done stock moves related to its procurements + def _action_procurement_create(self): + """ + Create procurements based on quantity ordered. If the quantity is increased, new + procurements are created. If the quantity is decreased, no automated action is taken. """ + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + for line in self: + if line.state != 'sale' or not line.product_id.type in ('consu','product'): + continue + qty = 0.0 + for move in line.move_ids: + qty += move.product_qty + if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: + continue + + if not line.order_id.procurement_group_id: + line.order_id.procurement_group_id = self.env["procurement.group"].create({ + 'name': line.order_id.name, 'move_type': line.order_id.picking_policy, + 'sale_id': line.order_id.id + }) + vals = line._prepare_order_line_procurement(group_id=line.order_id.procurement_group_id) + vals['product_qty'] = line.product_uom_qty - qty + + new_proc = self.env["procurement.group"].run(vals) + return True + + + @api.multi + def _get_delivered_qty(self): self.ensure_one() super(SaleOrderLine, self)._get_delivered_qty() qty = 0.0 - for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped): + for move in self.move_ids.filtered(lambda r: r.state == 'done' and not r.scrapped): if move.location_dest_id.usage == "customer": if not move.origin_returned_move_id: qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom) @@ -215,6 +263,7 @@ class SaleOrderLine(models.Model): } return {} + def _check_routing(self): """ Verify the route of the product based on the warehouse return True if the product availibility in stock does not need to be verified, diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 045a8f0498c..b2ee51e4d5a 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -6,34 +6,29 @@ from odoo import api, fields, models class StockLocationRoute(models.Model): _inherit = "stock.location.route" - sale_selectable = fields.Boolean("Selectable on Sales Order Line") class StockMove(models.Model): _inherit = "stock.move" + sale_line_id = fields.Many2one('sale.order.line', 'Sale Line') @api.multi def action_done(self): result = super(StockMove, self).action_done() - - # Update delivered quantities on sales order lines - sale_order_lines = self.filtered(lambda move: move.procurement_id.sale_line_id and move.product_id.expense_policy == 'no').mapped('procurement_id.sale_line_id') - for line in sale_order_lines: + for line in self.mapped('sale_line_id'): line.qty_delivered = line._get_delivered_qty() return result - @api.multi - def assign_picking(self): - result = super(StockMove, self).assign_picking() - for move in self: - if move.picking_id and move.picking_id.group_id: - picking = move.picking_id - order = self.env['sale.order'].sudo().search([('procurement_group_id', '=', picking.group_id.id)]) - picking.message_post_with_view( - 'mail.message_origin_link', - values={'self': picking, 'origin': order}, - subtype_id=self.env.ref('mail.mt_note').id) +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + sale_id = fields.Many2one('sale.order', 'Sale Order') + + def _get_stock_move_values(self, values, rule, group_id): + result = super(ProcurementGroup, self)._get_stock_move_values(values, rule, group_id) + if values.get('sale_line_id', False): + result['sale_line_id'] = values['sale_line_id'] return result @api.multi @@ -51,25 +46,17 @@ class StockMove(models.Model): class StockPicking(models.Model): _inherit = 'stock.picking' - sale_id = fields.Many2one('sale.order', "Sales Order", compute='_compute_sale_id', store=True) - @api.one - @api.depends('move_lines.procurement_id.sale_line_id.order_id') - def _compute_sale_id(self): - for move in self.move_lines: - if move.procurement_id.sale_line_id: - self.sale_id = move.procurement_id.sale_line_id.order_id - return + sale_id = fields.Many2one(related="group_id.sale_id", string="Sales Order", store=True) @api.multi def _create_backorder(self, backorder_moves=[]): res = super(StockPicking, self)._create_backorder(backorder_moves) for picking in self.filtered(lambda pick: pick.picking_type_id.code == 'outgoing'): backorder = picking.search([('backorder_id', '=', picking.id)]) - if backorder.group_id: # origin from a sale - order = self.env['sale.order'].search([('procurement_group_id', '=', backorder.group_id.id)]) + if backorder.sale_id: backorder.message_post_with_view( 'mail.message_origin_link', - values={'self': backorder, 'origin': order}, + values={'self': backorder, 'origin': backorder.sale_id}, subtype_id=self.env.ref('mail.mt_note').id) - return res + return res
\ No newline at end of file diff --git a/addons/sale_stock/security/ir.model.access.csv b/addons/sale_stock/security/ir.model.access.csv index 17cc727ec83..2bbea1c5935 100644 --- a/addons/sale_stock/security/ir.model.access.csv +++ b/addons/sale_stock/security/ir.model.access.csv @@ -2,8 +2,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_stock_picking_salesman,stock_picking salesman,stock.model_stock_picking,sales_team.group_sale_salesman,1,1,1,0 access_stock_move_salesman,stock_move salesman,stock.model_stock_move,sales_team.group_sale_salesman,1,1,1,0 access_stock_move_manager,stock_move manager,stock.model_stock_move,sales_team.group_sale_manager,1,1,1,1 -access_procurement_salesman,procurement.order salesman,procurement.model_procurement_order,sales_team.group_sale_salesman,1,1,1,0 -access_procurement_manager,procurement.order manager,procurement.model_procurement_order,sales_team.group_sale_manager,1,1,1,1 access_sale_order_stock_worker,sale.order stock worker,model_sale_order,stock.group_stock_user,1,1,0,0 access_sale_order_line_stock_worker,sale.order.line stock worker,model_sale_order_line,stock.group_stock_user,1,1,0,0 access_stock_picking_sales,stock.picking.sales,stock.model_stock_picking,sales_team.group_sale_manager,1,1,1,1 diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py index 3dd6d5103ca..2cc67016c8d 100644 --- a/addons/sale_stock/tests/test_sale_stock.py +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -267,6 +267,5 @@ class TestSaleStock(TestSale): (1, self.so.order_line[1].id, {'product_uom_qty': 2}), ] }) - # a single picking should be created for the new delivery self.assertEquals(len(self.so.picking_ids), 2) diff --git a/addons/sale_stock/views/stock_config_settings_views.xml b/addons/sale_stock/views/stock_config_settings_views.xml new file mode 100644 index 00000000000..3b07c6542b9 --- /dev/null +++ b/addons/sale_stock/views/stock_config_settings_views.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <record id="stock_config_settings_view_form_inherit_sale_stock" model="ir.ui.view"> + <field name="name">stock.config.settings.inherit.sale.stock</field> + <field name="model">stock.config.settings</field> + <field name="inherit_id" ref="stock.view_stock_config_settings"/> + <field name="arch" type="xml"> + <div id="warning_info" position="after"> + <div class="col-xs-12 col-md-6 o_setting_box" title="Reserving products manually in delivery orders or by running the scheduler is advised to better manage priorities in case of long customer lead times or/and frequent stock-outs. By default, the scheduler runs automatically every 24 hours."> + <div class="o_setting_right_pane"> + <label for="module_procurement_jit"/> + <div class="text-muted"> + When to reserve sold products + </div> + <div class="content-group"> + <div class="mt16"> + <field name="module_procurement_jit" class="o_light_label" widget="radio"/> + </div> + </div> + </div> + </div> + <div class="col-xs-12 col-md-6 o_setting_box"> + <div class="o_setting_right_pane"> + <label for="default_picking_policy"/> + <div class="text-muted"> + When to start shipping + </div> + <div class="content-group"> + <div class="mt16"> + <field name="default_picking_policy" class="o_light_label" widget="selection"/> + </div> + </div> + </div> + </div> + </div> + <div id="sale_security_lead" position="replace"> + <div class="col-xs-12 col-md-6 o_setting_box" title="Margin of error for dates promised to customers. Products will be scheduled for procurement and delivery that many days earlier than the actual promised date, to cope with unexpected delays in the supply chain."> + <div class="o_setting_left_pane"> + <field name="use_security_lead"/> + </div> + <div class="o_setting_right_pane"> + <label for="use_security_lead"/> + <span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/> + <div class="text-muted"> + Schedule deliveries earlier to avoid delays + </div> + <div class="content-group"> + <div class="mt16" attrs="{'invisible': [('use_security_lead','=',False)]}"> + <span>Move forward expected delivery dates by <field name="security_lead" class="oe_inline"/> days</span> + </div> + </div> + </div> + </div> + </div> + </field> + </record> + +</odoo>
\ No newline at end of file diff --git a/addons/sale_timesheet/__manifest__.py b/addons/sale_timesheet/__manifest__.py index 9343ca71f7f..8bc94cdffcf 100644 --- a/addons/sale_timesheet/__manifest__.py +++ b/addons/sale_timesheet/__manifest__.py @@ -22,7 +22,6 @@ have real delivered quantities in sales orders. 'views/product_views.xml', 'views/hr_views.xml', 'views/project_task_views.xml', - 'views/procurement_views.xml', 'views/hr_timesheet_views.xml', 'views/res_config_settings_views.xml', 'views/hr_timesheet_templates.xml', @@ -31,4 +30,4 @@ have real delivered quantities in sales orders. 'data/sale_service_demo.xml', ], 'auto_install': True, -}
\ No newline at end of file +} diff --git a/addons/sale_timesheet/models/__init__.py b/addons/sale_timesheet/models/__init__.py index 17cb3f027f9..cd36b996ae6 100644 --- a/addons/sale_timesheet/models/__init__.py +++ b/addons/sale_timesheet/models/__init__.py @@ -4,7 +4,6 @@ from . import account from . import account_invoice from . import hr -from . import procurement from . import product from . import project from . import res_company diff --git a/addons/sale_timesheet/models/procurement.py b/addons/sale_timesheet/models/procurement.py deleted file mode 100644 index d26ded705b1..00000000000 --- a/addons/sale_timesheet/models/procurement.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models, _ - - -class ProcurementOrder(models.Model): - _inherit = 'procurement.order' - - task_id = fields.Many2one('project.task', 'Task', copy=False) - - def _is_procurement_task(self): - return self.product_id.type == 'service' and self.product_id.track_service == 'task' - - @api.multi - def _assign(self): - self.ensure_one() - res = super(ProcurementOrder, self)._assign() - if not res: - # if there isn't any specific procurement.rule defined for the product, we may want to create a task - return self._is_procurement_task() - return res - - @api.multi - def _run(self): - self.ensure_one() - if self._is_procurement_task() and not self.task_id: - # If the SO was confirmed, cancelled, set to draft then confirmed, avoid creating a new - # task. - if self.sale_line_id: - existing_task = self.env['project.task'].search( - [('sale_line_id', '=', self.sale_line_id.id)] - ) - if existing_task: - return existing_task - # create a task for the procurement - return self._create_service_task() - return super(ProcurementOrder, self)._run() - - def _convert_qty_company_hours(self): - company_time_uom_id = self.env.user.company_id.project_time_mode_id - if self.product_uom.id != company_time_uom_id.id and self.product_uom.category_id.id == company_time_uom_id.category_id.id: - planned_hours = self.product_uom._compute_quantity(self.product_qty, company_time_uom_id) - else: - planned_hours = self.product_qty - return planned_hours - - def _get_project(self): - Project = self.env['project.project'] - project = self.product_id.with_context(force_company=self.company_id.id).project_id - if not project and self.sale_line_id: - # find the project corresponding to the analytic account of the sales order - account = self.sale_line_id.order_id.project_id - if not account: - self.sale_line_id.order_id._create_analytic_account() - account = self.sale_line_id.order_id.project_id - project = Project.search([('analytic_account_id', '=', account.id)], limit=1) - if not project: - project_id = account.project_create({'name': account.name}) - project = Project.browse(project_id) - return project - - def _prepare_service_task_values(self): - project = self._get_project() - planned_hours = self._convert_qty_company_hours() - return { - 'name': '%s:%s' % (self.origin or '', self.product_id.name), - 'date_deadline': self.date_planned, - 'planned_hours': planned_hours, - 'remaining_hours': planned_hours, - 'partner_id': self.sale_line_id.order_id.partner_id.id or self.partner_dest_id.id, - 'user_id': self.env.uid, - 'procurement_id': self.id, - 'description': self.name + '<br/>', - 'project_id': project.id, - 'company_id': self.company_id.id, - } - - def _create_service_task(self): - task_values = self._prepare_service_task_values() - task = self.env['project.task'].create(task_values) - self.write({'task_id': task.id}) - - msg_body = _("Task Created (%s): <a href=# data-oe-model=project.task data-oe-id=%d>%s</a>") % (self.product_id.name, task.id, task.name) - self.message_post(body=msg_body) - if self.sale_line_id.order_id: - self.sale_line_id.order_id.message_post(body=msg_body) - task_msg = _("This task has been created from: <a href=# data-oe-model=sale.order data-oe-id=%d>%s</a> (%s)") % (self.sale_line_id.order_id.id, self.sale_line_id.order_id.name, self.product_id.name) - task.message_post(body=task_msg) - - return task diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py index a863bef3805..b3035565c9b 100644 --- a/addons/sale_timesheet/models/product.py +++ b/addons/sale_timesheet/models/product.py @@ -21,13 +21,3 @@ class ProductTemplate(models.Model): else: self.track_service = 'manual' - -class ProductProduct(models.Model): - _inherit = 'product.product' - - @api.multi - def _need_procurement(self): - for product in self: - if product.type == 'service' and product.track_service == 'task': - return True - return super(ProductProduct, self)._need_procurement() diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index d8a15aec580..214cab4cf39 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -54,8 +54,7 @@ class Project(models.Model): class ProjectTask(models.Model): _inherit = "project.task" - procurement_id = fields.Many2one('procurement.order', 'Assign to Order', ondelete='set null', help="Procurement of the sale order line on which the timesheets should be assigned") - sale_line_id = fields.Many2one('sale.order.line', 'Sales Order Line', related='procurement_id.sale_line_id', store=True) + sale_line_id = fields.Many2one('sale.order.line', 'Sales Order Line') @api.multi def unlink(self): @@ -76,5 +75,4 @@ class ProjectTask(models.Model): @api.onchange('parent_id') def onchange_parent_id(self): - self.procurement_id = self.parent_id.procurement_id.id self.sale_line_id = self.parent_id.sale_line_id.id diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 39180e3ce05..d8a79438898 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -66,6 +66,7 @@ class SaleOrder(models.Model): order._create_analytic_account(prefix=line.product_id.default_code or None) order.project_id.project_create({'name': order.project_id.name}) break + order.order_line.filtered(lambda line: line._is_task())._create_task() return result @api.multi @@ -131,11 +132,14 @@ class SaleOrder(models.Model): class SaleOrderLine(models.Model): _inherit = "sale.order.line" + task_id = fields.Many2one('project.task', 'Task') + @api.model def create(self, values): line = super(SaleOrderLine, self).create(values) if line.state == 'sale' and not line.order_id.project_id and line.product_id.track_service in ['timesheet', 'task']: line.order_id._create_analytic_account() + line._create_task() return line @api.multi @@ -146,3 +150,51 @@ class SaleOrderLine(models.Model): expense_type_id = expense_type_id and expense_type_id.id domain = [('so_line', 'in', self.ids), '|', ('amount', '<=', 0.0), ('project_id', '!=', False)] return super(SaleOrderLine, self)._compute_analytic(domain=domain) + + def _convert_qty_company_hours(self): + company_time_uom_id = self.env.user.company_id.project_time_mode_id + if self.product_uom.id != company_time_uom_id.id and self.product_uom.category_id.id == company_time_uom_id.category_id.id: + planned_hours = self.product_uom._compute_quantity(self.product_uom_qty, company_time_uom_id) + else: + planned_hours = self.product_uom_qty + return planned_hours + + def _get_project(self): + Project = self.env['project.project'] + project = self.product_id.with_context(force_company=self.company_id.id).project_id + if not project: + # find the project corresponding to the analytic account of the sales order + account = self.order_id.project_id + if not account: + self.order_id._create_analytic_account() + account = self.order_id.project_id + project = Project.search([('analytic_account_id', '=', account.id)], limit=1) + if not project: + project_id = account.project_create({'name': account.name, 'use_tasks': True}) + project = Project.browse(project_id) + return project + + def _prepare_service_task_values(self): + self.ensure_one() + project = self._get_project() + planned_hours = self._convert_qty_company_hours() + return { + 'name': '%s:%s' % (self.order_id.name or '', self.product_id.name), + 'planned_hours': planned_hours, + 'remaining_hours': planned_hours, + 'partner_id': self.order_id.partner_id.id, + 'description': self.name + '<br/>', + 'project_id': project.id, + 'sale_line_id': self.id, + 'company_id': self.company_id.id, + } + + def _create_task(self): + for line in self: + task_values = line._prepare_service_task_values() + task = self.env['project.task'].create(task_values) + self.write({'task_id': task.id}) + + def _is_task(self): + self.ensure_one() + return self.product_id.type == 'service' and self.product_id.track_service == 'task' diff --git a/addons/sale_timesheet/security/ir.model.access.csv b/addons/sale_timesheet/security/ir.model.access.csv deleted file mode 100644 index 8bc4d4ba1b5..00000000000 --- a/addons/sale_timesheet/security/ir.model.access.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_procurement_project_user,procurement.order project_user,model_procurement_order,project.group_project_user,1,1,1,1
diff --git a/addons/sale_timesheet/views/procurement_views.xml b/addons/sale_timesheet/views/procurement_views.xml deleted file mode 100644 index 393ef3c8fba..00000000000 --- a/addons/sale_timesheet/views/procurement_views.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <record id="view_procurement_task_form" model="ir.ui.view"> - <field name="name">procurement.procurement.form.view.inherit</field> - <field name="model">procurement.order</field> - <field name="inherit_id" ref="procurement.procurement_form_view"/> - <field name="arch" type="xml"> - <field name="origin" position="after"> - <field name="task_id"/> - </field> - </field> - </record> -</odoo>
\ No newline at end of file diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 56a88c7a28d..27f26ad60a6 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -27,7 +27,6 @@ string="Sales Order"/> </xpath> <field name="company_id" position="before"> - <field name="procurement_id" groups="base.group_no_one"/> <field name="sale_line_id" string="Order Line" invisible="1"/> </field> </field> diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py index 5a27f9ff20c..f5fde7a8ff9 100644 --- a/addons/stock/__manifest__.py +++ b/addons/stock/__manifest__.py @@ -7,7 +7,7 @@ 'summary': 'Inventory, Logistics, Warehousing', 'description': "", 'website': 'https://www.odoo.com/page/warehouse', - 'depends': ['product', 'procurement', 'barcodes', 'web_planner'], + 'depends': ['product', 'barcodes', 'web_planner'], 'category': 'Warehouse', 'sequence': 13, 'demo': [ @@ -26,6 +26,7 @@ 'security/ir.model.access.csv', 'views/stock_menu_views.xml', 'data/stock_traceability_report_data.xml', + 'data/procurement_data.xml', 'report/report_stock_forecast.xml', 'report/stock_report_views.xml', @@ -38,8 +39,7 @@ 'wizard/stock_change_product_qty_views.xml', 'wizard/stock_picking_return_views.xml', - 'wizard/make_procurement_views.xml', - 'wizard/procurement_orderpoint_compute_views.xml', + 'wizard/stock_scheduler_compute_views.xml', 'wizard/stock_immediate_transfer_views.xml', 'wizard/stock_backorder_confirmation_views.xml', @@ -52,7 +52,6 @@ 'views/stock_inventory_views.xml', 'views/stock_quant_views.xml', 'views/stock_location_views.xml', - 'views/procurement_views.xml', 'views/stock_warehouse_views.xml', 'views/stock_move_line_views.xml', 'views/stock_move_views.xml', @@ -60,6 +59,7 @@ 'views/res_config_settings_views.xml', 'views/report_stock_traceability.xml', 'views/stock_template.xml', + 'views/procurement_views.xml', 'data/default_barcode_patterns.xml', 'data/stock_data.xml', @@ -72,10 +72,10 @@ 'static/src/xml/stock_traceability_report_backend.xml', ], 'test': [ - 'test/procrule.yml', 'test/stock_users.yml', 'test/packing.yml', 'test/packingneg.yml', + 'test/procrule.yml', 'test/wiseoperator.yml', ], 'installable': True, diff --git a/addons/stock/data/procurement_data.xml b/addons/stock/data/procurement_data.xml new file mode 100644 index 00000000000..0c541c5a38e --- /dev/null +++ b/addons/stock/data/procurement_data.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data noupdate="1"> + + <record id="sequence_proc_group" model="ir.sequence"> + <field name="name">Procurement Group</field> + <field name="code">procurement.group</field> + <field name="prefix">PG/</field> + <field name="padding">6</field> + <field name="number_next">1</field> + <field name="number_increment">1</field> + </record> + + </data> +</odoo> diff --git a/addons/stock/data/stock_data.xml b/addons/stock/data/stock_data.xml index 5b74427da75..81c493ecb06 100644 --- a/addons/stock/data/stock_data.xml +++ b/addons/stock/data/stock_data.xml @@ -111,11 +111,6 @@ <field name="fields_id" search="[('model','=','res.partner'),('name','=','property_stock_customer')]"/> <field eval="'stock.location,'+str(stock_location_customers)" name="value"/> </record> - <record forcecreate="True" id="property_stock_procurement" model="ir.property"> - <field name="name">property_stock_procurement</field> - <field name="fields_id" search="[('model','=','product.template'),('name','=','property_stock_procurement')]"/> - <field eval="'stock.location,'+str(location_procurement)" name="value"/> - </record> <record forcecreate="True" id="property_stock_inventory" model="ir.property"> <field name="name">property_stock_inventory</field> <field name="fields_id" search="[('model','=','product.template'),('name','=','property_stock_inventory')]"/> diff --git a/addons/stock/data/stock_demo.xml b/addons/stock/data/stock_demo.xml index 8d32f68dd99..e84dddfd106 100644 --- a/addons/stock/data/stock_demo.xml +++ b/addons/stock/data/stock_demo.xml @@ -132,7 +132,7 @@ </record> <function model="stock.inventory" name="action_done"> - <function eval="[[('id', '=', ref('stock_inventory_0'))]]" model="stock.inventory" name="search"/> + <function eval="[[('state','=','draft'),('id', '=', ref('stock_inventory_0'))]]" model="stock.inventory" name="search"/> </function> diff --git a/addons/stock/data/stock_sequence_data.xml b/addons/stock/data/stock_sequence_data.xml index 1d2f2f7cf53..5980da8de61 100644 --- a/addons/stock/data/stock_sequence_data.xml +++ b/addons/stock/data/stock_sequence_data.xml @@ -47,5 +47,24 @@ <field name="padding">7</field> <field name="company_id" eval="False"/> </record> + + <!-- Scheduler --> + + <record forcecreate="True" id="ir_cron_scheduler_action" model="ir.cron"> + <field name="name">Procurement: run MRP scheduler</field> + <field name="model_id" ref="model_procurement_group"/> + <field name="state">code</field> + <field name="code">model.run_scheduler(True)</field> + <field eval="False" name="active"/> + <field name="user_id" ref="base.user_root"/> + <field name="interval_number">1</field> + <field name="interval_type">days</field> + <field name="numbercall">-1</field> + <field eval="False" name="doall"/> + </record> + + + + </data> </odoo> diff --git a/addons/stock/models/__init__.py b/addons/stock/models/__init__.py index bd6f1102672..dac5e0568e8 100644 --- a/addons/stock/models/__init__.py +++ b/addons/stock/models/__init__.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import barcode -from . import product from . import procurement from . import product_strategy from . import res_company @@ -20,3 +19,4 @@ from . import stock_traceability from . import stock_warehouse from . import stock_scrap from . import web_planner +from . import product diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index ad5b81d9bdf..09b5b49d89e 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -11,20 +11,34 @@ from odoo import api, fields, models, registry, _ from odoo.osv import expression from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare, float_round -import logging +from odoo.exceptions import UserError +import logging _logger = logging.getLogger(__name__) -class ProcurementGroup(models.Model): - _inherit = 'procurement.group' - - partner_id = fields.Many2one('res.partner', 'Partner') - class ProcurementRule(models.Model): - """ Pull rules """ - _inherit = 'procurement.rule' - + """ A rule describe what a procurement should do; produce, buy, move, ... """ + _name = 'procurement.rule' + _description = "Procurement Rule" + _order = "sequence, name" + + name = fields.Char( + 'Name', required=True, translate=True, + help="This field will fill the packing origin and the name of its moves") + active = fields.Boolean( + 'Active', default=True, + help="If unchecked, it will allow you to hide the rule without removing it.") + group_propagation_option = fields.Selection([ + ('none', 'Leave Empty'), + ('propagate', 'Propagate'), + ('fixed', 'Fixed')], string="Propagation of Procurement Group", default='propagate') + group_id = fields.Many2one('procurement.group', 'Fixed Procurement Group') + action = fields.Selection( + selection=[('move', 'Move From Another Location')], string='Action', + required=True) + sequence = fields.Integer('Sequence', default=20) + company_id = fields.Many2one('res.company', 'Company') 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', required=True, ondelete='cascade') @@ -48,188 +62,190 @@ class ProcurementRule(models.Model): '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)") - @api.model - def _get_action(self): - result = super(ProcurementRule, self)._get_action() - return result + [('move', _('Move From Another Location'))] - - -class ProcurementOrder(models.Model): - _inherit = "procurement.order" - - 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') - - @api.onchange('warehouse_id') - def onchange_warehouse_id(self): - if self.warehouse_id: - self.location_id = self.warehouse_id.lot_stock_id.id - - @api.multi - def propagate_cancels(self): - # set the context for the propagation of the procurement cancellation - # TDE FIXME: was in cancel, moved here for consistency - cancel_moves = self.filtered(lambda order: order.rule_id.action == 'move').mapped('move_ids').filtered(lambda m: m.state not in ('cancel', 'done')) - if cancel_moves: - cancel_moves.action_cancel() - return self.search([('move_dest_id', 'in', cancel_moves.filtered(lambda move: move.propagate).ids)]) +class ProcurementGroup(models.Model): + """ + The procurement group class is used to group products together + when computing procurements. (tasks, physical products, ...) + + The goal is that when you have one sales order of several products + and the products are pulled from the same or several location(s), to keep + having the moves grouped into pickings that represent the sales order. + + Used in: sales order (to group delivery order lines like the so), pull/push + rules (to pack like the delivery order), on orderpoints (e.g. for wave picking + all the similar products together). + + Grouping is made only if the source and the destination is the same. + Suppose you have 4 lines on a picking from Output where 2 lines will need + to come from Input (crossdock) and 2 lines coming from Stock -> Output As + the four will have the same group ids from the SO, the move from input will + have a stock.picking with 2 grouped lines and the move from stock will have + 2 grouped lines also. + + The name is usually the name of the original document (sales order) or a + sequence computed if created manually. + """ + _name = 'procurement.group' + _description = 'Procurement Requisition' + _order = "id desc" - @api.multi - def cancel(self): - propagated_procurements = self.filtered(lambda order: order.state != 'done').propagate_cancels() - if propagated_procurements: - propagated_procurements.cancel() - return super(ProcurementOrder, self).cancel() + partner_id = fields.Many2one('res.partner', 'Partner') + name = fields.Char( + 'Reference', + default=lambda self: self.env['ir.sequence'].next_by_code('procurement.group') or '', + required=True) + move_type = fields.Selection([ + ('direct', 'Partial'), + ('one', 'All at once')], string='Delivery Type', default='direct', + required=True) + + def _make_po_get_domain(self, values, partner): + return () + + def _log_next_activity(self, product_id, note): + existing_activity = self.env['mail.activity'].search([('res_id', '=', product_id.product_tmpl_id.id), ('res_model_id', '=', self.env.ref('product.model_product_template').id), + ('note', '=', note)]) + if not existing_activity: + self.env['mail.activity'].create({ + 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, + 'note': note, + 'user_id': product_id.responsible_id.id, + 'res_id': product_id.product_tmpl_id.id, + 'res_model_id': self.env.ref('product.model_product_template').id, + }) - @api.multi - def do_view_pickings(self): - """ Return an action to display the pickings belonging to the same - procurement group of given ids. """ - action = self.env.ref('stock.do_view_pickings').read()[0] - action['domain'] = [('group_id', 'in', self.mapped('group_id').ids)] - return action + @api.model + def run(self, values, doraise=True): + for key in ('product_id', 'product_uom', 'product_qty', 'location_id', 'name', 'origin'): + assert key in values + values.setdefault('company_id', self.env['res.company']._company_default_get('procurement.group')) + values.setdefault('priority', '1') + values.setdefault('date_planned', fields.Datetime.now()) + rule = self._get_rule(values) - @api.multi - @api.returns('procurement.rule', lambda value: value.id if value else False) - def _find_suitable_rule(self): - rule = super(ProcurementOrder, self)._find_suitable_rule() if not rule: - # a rule defined on 'Stock' is suitable for a procurement in 'Stock\Bin A' - all_parent_location_ids = self._find_parent_locations() - rule = self._search_suitable_rule([('location_id', 'in', all_parent_location_ids.ids)]) - return rule - - def _find_parent_locations(self): - parent_locations = self.env['stock.location'] - location = self.location_id - while location: - parent_locations |= location - location = location.location_id - return parent_locations + if doraise: + raise UserError(_('No procurement rule found.')) + else: + msg = _('No procurement rule found. Update the inventory tab of the product form.') + if values.get('orderpoint_id', False): + msg = _('No procurement rule found for orderpoint %s. Update the inventory tab of the product form.') % (values['orderpoint_id'].name,) + self._log_next_activity(values['product_id'], msg) + return False + self._run(values, rule, doraise) + return True - def _search_suitable_rule(self, domain): - """ First find a rule among the ones defined on the procurement order + @api.model + def _search_rule(self, values, domain): + """ First find a rule among the ones defined on the procurement group; then try on the routes defined for the product; finally fallback on the default behavior """ - if self.warehouse_id: - domain = expression.AND([['|', ('warehouse_id', '=', self.warehouse_id.id), ('warehouse_id', '=', False)], domain]) + if values.get('warehouse_id', False): + domain = expression.AND([['|', ('warehouse_id', '=', values['warehouse_id'].id), ('warehouse_id', '=', False)], domain]) Pull = self.env['procurement.rule'] res = self.env['procurement.rule'] - if self.route_ids: - res = Pull.search(expression.AND([[('route_id', 'in', self.route_ids.ids)], domain]), order='route_sequence, sequence', limit=1) + if values.get('route_ids', False): + res = Pull.search(expression.AND([[('route_id', 'in', values['route_ids'].ids)], domain]), order='route_sequence, sequence', limit=1) if not res: - product_routes = self.product_id.route_ids | self.product_id.categ_id.total_route_ids + product_routes = values['product_id'].route_ids | values['product_id'].categ_id.total_route_ids if product_routes: res = Pull.search(expression.AND([[('route_id', 'in', product_routes.ids)], domain]), order='route_sequence, sequence', limit=1) if not res: - warehouse_routes = self.warehouse_id.route_ids + warehouse_routes = values['warehouse_id'].route_ids if warehouse_routes: res = Pull.search(expression.AND([[('route_id', 'in', warehouse_routes.ids)], domain]), order='route_sequence, sequence', limit=1) return res - def _get_stock_move_values(self): + @api.model + def _get_rule(self, values): + result = False + location = values['location_id'] + while (not result) and location: + result = self._search_rule(values, [('location_id', '=', location.id)]) + location = location.location_id + return result + + def _get_stock_move_values(self, values, rule, group_id): ''' 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 ''' - group_id = False - if self.rule_id.group_propagation_option == 'propagate': - group_id = self.group_id.id - elif self.rule_id.group_propagation_option == 'fixed': - group_id = self.rule_id.group_id.id - date_expected = (datetime.strptime(self.date_planned, DEFAULT_SERVER_DATETIME_FORMAT) - relativedelta(days=self.rule_id.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + date_expected = (datetime.strptime(values['date_planned'], DEFAULT_SERVER_DATETIME_FORMAT) - relativedelta(days=rule.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) # 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 - qty_done = sum(self.move_ids.filtered(lambda move: move.state == 'done').mapped('product_uom_qty')) - qty_left = max(self.product_qty - qty_done, 0) + qty_left = values['product_qty'] return { - 'name': self.name[:2000], - 'company_id': self.rule_id.company_id.id or self.rule_id.location_src_id.company_id.id or self.rule_id.location_id.company_id.id or self.company_id.id, - 'product_id': self.product_id.id, - 'product_uom': self.product_uom.id, + 'name': values['name'][:2000], + 'company_id': rule.company_id.id or rule.location_src_id.company_id.id or rule.location_id.company_id.id or values['company_id'].id, + 'product_id': values['product_id'].id, + 'product_uom': values['product_uom'].id, 'product_uom_qty': qty_left, - 'partner_id': self.rule_id.partner_address_id.id or (self.group_id and self.group_id.partner_id.id) or False, - 'location_id': self.rule_id.location_src_id.id, - 'location_dest_id': self.location_id.id, - 'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or False, - 'procurement_id': self.id, - 'rule_id': self.rule_id.id, - 'procure_method': self.rule_id.procure_method, - 'origin': self.origin, - 'picking_type_id': self.rule_id.picking_type_id.id, + 'partner_id': rule.partner_address_id.id or (values['group_id'] and values['group_id'].partner_id.id) or False, + 'location_id': rule.location_src_id.id, + 'location_dest_id': values['location_id'].id, + 'move_dest_ids': values.get('move_dest_ids', False) and [(4, x.id) for x in values['move_dest_ids']] or [], + 'rule_id': rule.id, + 'procure_method': rule.procure_method, + 'origin': values['origin'], + 'picking_type_id': rule.picking_type_id.id, 'group_id': group_id, - 'route_ids': [(4, route.id) for route in self.route_ids], - 'warehouse_id': self.rule_id.propagate_warehouse_id.id or self.rule_id.warehouse_id.id, + 'route_ids': [(4, route.id) for route in values.get('route_ids', [])], + 'warehouse_id': rule.propagate_warehouse_id.id or rule.warehouse_id.id, 'date': date_expected, 'date_expected': date_expected, - 'propagate': self.rule_id.propagate, - 'priority': self.priority, + 'propagate': rule.propagate, + 'priority': values.get('priority', "1"), } + def _merge_domain(self, values, rule, group_id): + return [ + ('group_id', '=', group_id), #extra logic? + ('location_id', '=', rule.location_src_id.id), + ('location_dest_id', '=', values['location_id'].id), + ('picking_type_id', '=', rule.picking_type_id.id), + ('picking_id.printed', '=', False), + ('picking_id.state', 'in', ['draft', 'confirmed', 'waiting', 'partially_available', 'assigned']), + ('picking_id.backorder_id', '=', False), + ('product_id', '=', values['product_id'].id)] + @api.multi - def _run(self): - if self.rule_id.action == 'move': - if not self.rule_id.location_src_id: - self.message_post(body=_('No source location defined!')) - return False + def _run(self, values, rule, doraise=True): + if rule.action == 'move': + if not rule.location_src_id: + msg = _('No source location defined on procurement rule: %s!') % (rule.name, ) + if doraise: + raise UserError(msg) + else: + self._log_next_activity(values['product_id'], msg) + return False + # 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) - self.env['stock.move'].sudo().create(self._get_stock_move_values()) + # Search if picking with move for it exists already: + group_id = False + if rule.group_propagation_option == 'propagate': + group_id = values.get('group_id', False) and values['group_id'].id + elif rule.group_propagation_option == 'fixed': + group_id = rule.group_id.id + + data = self._get_stock_move_values(values, rule, group_id) + # Since action_confirm launch following procurement_group we should activate it. + move = self.env['stock.move'].sudo().create(data) + move.assign_picking() + move.picking_id.action_confirm() return True - return super(ProcurementOrder, self)._run() + return False - @api.multi - def run(self, autocommit=False): - # TDE CLEANME: unused context key procurement_auto_defer remove - new_self = self.filtered(lambda order: order.state not in ['running', 'done', 'cancel']) - res = super(ProcurementOrder, new_self).run(autocommit=autocommit) - - # 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) - new_self.filtered(lambda order: order.state == 'running' and order.rule_id.action == 'move').mapped('move_ids').filtered(lambda move: move.state == 'draft').action_confirm() - - # TDE FIXME: action_confirm in stock_move already call run() ... necessary ?? - # If procurements created other procurements, run the created in batch - new_procurements = self.search([('move_dest_id.procurement_id', 'in', new_self.ids)], order='id') - if new_procurements: - res = new_procurements.run(autocommit=autocommit) - return res - - @api.multi - def _check(self): - """ Checking rules of type 'move': satisfied only if all related moves - are done/cancel and if the requested quantity is moved. """ - if self.rule_id.action == 'move': - # In case Phantom BoM splits only into procurements - if not self.move_ids: - return True - move_all_done_or_cancel = all(move.state in ['done', 'cancel'] for move in self.move_ids) - move_all_cancel = all(move.state == 'cancel' for move in self.move_ids) - if not move_all_done_or_cancel: - return False - elif move_all_done_or_cancel and not move_all_cancel: - return True - else: - self.message_post(body=_('All stock moves have been cancelled for this procurement.')) - # TDE FIXME: strange that a check method actually modified the procurement... - self.write({'state': 'cancel'}) - return False - return super(ProcurementOrder, self)._check() + @api.model + def _get_exceptions_domain(self): + return [('procure_method', '=', 'make_to_order'), ('move_orig_ids', '=', False)] @api.model def run_scheduler(self, use_new_cursor=False, company_id=False): - ''' Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules + """ 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. ''' - super(ProcurementOrder, self).run_scheduler(use_new_cursor=use_new_cursor, company_id=company_id) + we run functions as SUPERUSER to avoid intercompanies and access rights issues. """ try: if use_new_cursor: cr = registry(self._cr.dbname).cursor() @@ -241,10 +257,13 @@ class ProcurementOrder(models.Model): # Search all confirmed stock_moves and try to assign them confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc') for moves_chunk in split_every(100, confirmed_moves.ids): - # TDE CLEANME: muf muf self.env['stock.move'].browse(moves_chunk).action_assign() if use_new_cursor: self._cr.commit() + + exception_moves = self.env['stock.move'].search(self._get_exceptions_domain()) + for move in exception_moves: + self.env['procurement.group'].run(move._prepare_procurement_from_move(), doraise=False) if use_new_cursor: self._cr.commit() finally: @@ -295,9 +314,6 @@ class ProcurementOrder(models.Model): cr = registry(self._cr.dbname).cursor() self = self.with_env(self.env(cr=cr)) OrderPoint = self.env['stock.warehouse.orderpoint'] - Procurement = self.env['procurement.order'] - ProcurementAutorundefer = Procurement.with_context(procurement_autorun_defer=True) - procurement_list = [] orderpoints = OrderPoint.browse(orderpoints_noprefetch[:1000]) orderpoints_noprefetch = orderpoints_noprefetch[1000:] @@ -313,7 +329,7 @@ class ProcurementOrder(models.Model): for location_id, location_data in location_data.items(): location_orderpoints = location_data['orderpoints'] product_context = dict(self._context, location=location_orderpoints[0].location_id.id) - substract_quantity = location_orderpoints.subtract_procurements_from_orderpoints() + substract_quantity = location_orderpoints._quantity_in_progress() for group in location_data['groups']: if group.get('from_date'): @@ -339,12 +355,8 @@ class ProcurementOrder(models.Model): qty -= substract_quantity[orderpoint.id] qty_rounded = float_round(qty, precision_rounding=orderpoint.product_uom.rounding) if qty_rounded > 0: - new_procurement = ProcurementAutorundefer.create( - orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values'])) - procurement_list.append(new_procurement) - new_procurement.message_post_with_view('mail.message_origin_link', - values={'self': new_procurement, 'origin': orderpoint}, - subtype_id=self.env.ref('mail.mt_note').id) + self.env['procurement.group'].run( + orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values']), doraise=False) self._procurement_from_orderpoint_post_process([orderpoint.id]) if use_new_cursor: cr.commit() @@ -358,12 +370,6 @@ class ProcurementOrder(models.Model): raise try: - # TDE CLEANME: use record set ? - procurement_list.reverse() - procurements = self.env['procurement.order'] - for p in procurement_list: - procurements += p - procurements.run() if use_new_cursor: cr.commit() except OperationalError: @@ -378,3 +384,4 @@ class ProcurementOrder(models.Model): cr.close() return {} + diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 3f64fe60ecc..6d546f49b7a 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -70,8 +70,7 @@ class Product(models.Model): "any of its children.\n" "Otherwise, this includes goods leaving any Stock " "Location with 'internal' type.") - # TDE CLEANME: unused except in one test - orderpoint_ids = fields.One2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules') + nbr_reordering_rules = fields.Integer('Reordering Rules', compute='_compute_nbr_reordering_rules') reordering_min_qty = fields.Float(compute='_compute_nbr_reordering_rules') reordering_max_qty = fields.Float(compute='_compute_nbr_reordering_rules') @@ -370,11 +369,8 @@ class Product(models.Model): class ProductTemplate(models.Model): _inherit = 'product.template' + responsible_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.uid, required=True) type = fields.Selection(selection_add=[('product', 'Stockable Product')]) - property_stock_procurement = fields.Many2one( - 'stock.location', "Procurement Location", - company_dependent=True, 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.Many2one( 'stock.location', "Production Location", company_dependent=True, domain=[('usage', 'like', 'production')], diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 55e63bb4f9f..7e46e17093d 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -176,12 +176,10 @@ class Inventory(models.Model): def action_check(self): """ Checks the inventory and computes the stock move to do """ # tde todo: clean after _generate_moves - for inventory in self: + for inventory in self.filtered(lambda x: x.state not in ('done','cancel')): # first remove the existing stock moves linked to this inventory inventory.mapped('move_ids').unlink() - for line in inventory.line_ids: - # compare the checked quantities on inventory lines to the theorical one - stock_move = line._generate_moves() + inventory.line_ids._generate_moves() @api.multi def action_cancel_draft(self): @@ -447,8 +445,8 @@ class InventoryLine(models.Model): continue diff = line.theoretical_qty - line.product_qty if diff < 0: # found more than expected - vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id, False) + vals = line._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id, False) else: - vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id, True) + vals = line._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id, True) moves |= self.env['stock.move'].create(vals) return moves diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index 8b7e70f80f7..020384b5992 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -189,7 +189,7 @@ class PushedFlow(models.Model): name = fields.Char('Operation Name', required=True) company_id = fields.Many2one( 'res.company', 'Company', - default=lambda self: self.env['res.company']._company_default_get('procurement.order'), index=True) + default=lambda self: self.env['res.company']._company_default_get('stock.location.path'), index=True) route_id = fields.Many2one('stock.location.route', 'Route', required=True, ondelete='cascade') location_from_id = fields.Many2one( 'stock.location', 'Source Location', index=True, ondelete='cascade', required=True, @@ -243,7 +243,6 @@ class PushedFlow(models.Model): 'propagate': self.propagate, 'push_rule_id': self.id, 'warehouse_id': self.warehouse_id.id, - 'procurement_id': False, } return new_move_vals diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 3ee63610245..7e938d2e7bd 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -8,11 +8,11 @@ from operator import itemgetter from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp -from odoo.addons.procurement.models import procurement from odoo.exceptions import UserError from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.tools.float_utils import float_compare, float_round, float_is_zero +PROCUREMENT_PRIORITIES = [('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')] class StockMove(models.Model): _name = "stock.move" @@ -26,7 +26,7 @@ class StockMove(models.Model): name = fields.Char('Description', index=True, required=True) sequence = fields.Integer('Sequence', default=10) - priority = fields.Selection(procurement.PROCUREMENT_PRIORITIES, 'Priority', default='1') + priority = fields.Selection(PROCUREMENT_PRIORITIES, 'Priority', default='1') create_date = fields.Datetime('Creation Date', index=True, readonly=True) date = fields.Datetime( 'Date', default=fields.Datetime.now, index=True, required=True, @@ -122,7 +122,6 @@ class StockMove(models.Model): "this second option should be chosen.") scrapped = fields.Boolean('Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True) scrap_ids = fields.One2many('stock.scrap', 'move_id') - procurement_id = fields.Many2one('procurement.order', 'Procurement') group_id = fields.Many2one('procurement.group', 'Procurement Group', default=_default_group_id) rule_id = fields.Many2one('procurement.rule', 'Procurement Rule', ondelete='restrict', help='The procurement rule that created this stock move') push_rule_id = fields.Many2one('stock.location.path', 'Push Rule', ondelete='restrict', help='The push rule that created this stock move') @@ -146,7 +145,7 @@ class StockMove(models.Model): 'Availability', compute='_compute_string_qty_information', readonly=True, help='Show various information on stock availability for this move') restrict_partner_id = fields.Many2one('res.partner', 'Owner ', help="Technical field used to depict a restriction on the ownership of quants to consider when marking this move as 'done'") - route_ids = fields.Many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route to be followed by the procurement order") + route_ids = fields.Many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route") warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', help="Technical field depicting the warehouse to consider for the route selection on the next procurement (if any).") has_tracking = fields.Selection(related='product_id.tracking', string='Product with Tracking') quantity_done = fields.Float('Quantity Done', compute='_quantity_done_compute', digits=dp.get_precision('Product Unit of Measure'), inverse='_quantity_done_set', @@ -589,11 +588,8 @@ class StockMove(models.Model): to_assign[key] |= move # create procurements for make to order moves - procurements = self.env['procurement.order'] for move in move_create_proc: - procurements |= procurements.create(move._prepare_procurement_from_move()) - if procurements: - procurements.run() + self.env['procurement.group'].run(move._prepare_procurement_from_move()) move_to_confirm.write({'state': 'confirmed'}) (move_waiting | move_create_proc).write({'state': 'waiting'}) @@ -607,25 +603,25 @@ class StockMove(models.Model): def _prepare_procurement_from_move(self): self.ensure_one() origin = (self.group_id and (self.group_id.name + ":") or "") + (self.rule_id and self.rule_id.name or self.origin or self.picking_id.name or "/") - group_id = self.group_id and self.group_id.id or False + group_id = self.group_id or False if self.rule_id: if self.rule_id.group_propagation_option == 'fixed' and self.rule_id.group_id: - group_id = self.rule_id.group_id.id + group_id = self.rule_id.group_id elif self.rule_id.group_propagation_option == 'none': group_id = False return { 'name': self.rule_id and self.rule_id.name or "/", 'origin': origin, - 'company_id': self.company_id.id, + 'company_id': self.company_id, 'date_planned': self.date, - 'product_id': self.product_id.id, + 'product_id': self.product_id, 'product_qty': self.product_uom_qty, - 'product_uom': self.product_uom.id, - 'location_id': self.location_id.id, - 'move_dest_id': self.id, + 'product_uom': self.product_uom, + 'location_id': self.location_id, + 'move_dest_ids': self, 'group_id': group_id, - 'route_ids': [(4, x.id) for x in self.route_ids], - 'warehouse_id': self.warehouse_id.id or (self.picking_type_id and self.picking_type_id.warehouse_id.id or False), + 'route_ids': self.route_ids, + 'warehouse_id': self.warehouse_id or self.picking_id.picking_type_id.warehouse_id or self.picking_type_id.warehouse_id, 'priority': self.priority, } @@ -813,7 +809,6 @@ class StockMove(models.Model): move.move_dest_ids.write({'procure_method': 'make_to_stock'}) move.move_dest_ids.write({'move_orig_ids': [(3, move.id, 0)]}) self.write({'state': 'cancel', 'move_orig_ids': [(5, 0, 0)]}) - self.mapped('procurement_id').check() return True def _prepare_extra_move_vals(self, qty): @@ -832,7 +827,6 @@ class StockMove(models.Model): The rationale for the creation of an extra move is the application of a potential push rule that will handle the extra quantities. """ - self.ensure_one() extra_move = self.env['stock.move'] rounding = self.product_uom.rounding # moves created after the picking is assigned do not have `product_uom_qty`, but we shouldn't create extra moves for them @@ -933,7 +927,6 @@ class StockMove(models.Model): vals = { 'product_uom_qty': uom_qty, 'procure_method': 'make_to_stock', - 'procurement_id': self.procurement_id.id, 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if x.state not in ('done', 'cancel')], 'move_orig_ids': [(4, x.id) for x in self.move_orig_ids], 'origin_returned_move_id': self.origin_returned_move_id.id, @@ -960,6 +953,7 @@ class StockMove(models.Model): # HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM uom_qty = self.product_id.uom_id._compute_quantity(qty, self.product_uom, rounding_method='HALF-UP') defaults = self._prepare_move_split_vals(uom_qty) + if restrict_partner_id: defaults['restrict_partner_id'] = restrict_partner_id diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index ea404b41e5c..318be0a3023 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -9,8 +9,8 @@ from itertools import groupby from odoo import api, fields, models, _ from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.tools.float_utils import float_compare, float_round -from odoo.addons.procurement.models import procurement from odoo.exceptions import UserError +from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES from operator import itemgetter @@ -213,7 +213,7 @@ class Picking(models.Model): readonly=True, related='move_lines.group_id', store=True) priority = fields.Selection( - procurement.PROCUREMENT_PRIORITIES, string='Priority', + PROCUREMENT_PRIORITIES, string='Priority', compute='_compute_priority', inverse='_set_priority', store=True, # default='1', required=True, # TDE: required, depending on moves ? strange index=True, track_visibility='onchange', @@ -709,7 +709,6 @@ class Picking(models.Model): 'location_id': picking.location_id.id, 'location_dest_id': picking.location_dest_id.id, 'product_id': product.id, - 'procurement_id': proc_id, 'product_uom': uom_id, 'product_uom_qty': qty, 'name': _('Extra Move: ') + name, diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index d8bd64253c6..740ac533921 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -8,7 +8,7 @@ from dateutil import relativedelta from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError, ValidationError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT import logging @@ -769,7 +769,6 @@ class Orderpoint(models.Model): 'Qty Multiple', digits=dp.get_precision('Product Unit of Measure'), default=1, required=True, help="The procurement quantity will be rounded up to this multiple. If it is 0, the exact quantity will be used.") - procurement_ids = fields.One2many('procurement.order', 'orderpoint_id', 'Created Procurements') group_id = fields.Many2one( 'procurement.group', 'Procurement Group', copy=False, help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by procurement rules will be grouped into one big picking.") @@ -787,6 +786,12 @@ class Orderpoint(models.Model): ('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'), ] + @api.multi + def _quantity_in_progress(self): + """Return Quantities that are not yet in virtual stock but should be deduced from orderpoint rule + (example: purchases created from orderpoints)""" + return dict(self.mapped(lambda x: (x.id, 0.0))) + @api.constrains('product_id') def _check_product_uom(self): ''' Check if the UoM has the same category as the product standard UoM ''' @@ -806,32 +811,6 @@ class Orderpoint(models.Model): return {'domain': {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}} return {'domain': {'product_uom': []}} - @api.multi - def subtract_procurements_from_orderpoints(self): - '''This function returns quantity of product that needs to be deducted from the orderpoint computed quantity because there's already a procurement created with aim to fulfill it. - ''' - self._cr.execute("""SELECT orderpoint.id, procurement.id, procurement.product_uom, procurement.product_qty, template.uom_id, move.product_qty - FROM stock_warehouse_orderpoint orderpoint - JOIN procurement_order AS procurement ON procurement.orderpoint_id = orderpoint.id - JOIN product_product AS product ON product.id = procurement.product_id - JOIN product_template AS template ON template.id = product.product_tmpl_id - LEFT JOIN stock_move AS move ON move.procurement_id = procurement.id - WHERE procurement.state not in ('done', 'cancel') - AND (move.state IS NULL OR move.state != 'draft') - AND orderpoint.id IN %s - ORDER BY orderpoint.id, procurement.id - """, (tuple(self.ids),)) - UoM = self.env["product.uom"] - procurements_done = set() - res = dict.fromkeys(self.ids, 0.0) - for orderpoint_id, procurement_id, product_uom_id, procurement_qty, template_uom_id, move_qty in self._cr.fetchall(): - if procurement_id not in procurements_done: # count procurement once, if multiple move in this orderpoint/procurement combo - procurements_done.add(procurement_id) - res[orderpoint_id] += UoM.browse(product_uom_id)._compute_quantity(procurement_qty, UoM.browse(template_uom_id), round=False) - if move_qty: - res[orderpoint_id] -= move_qty - return res - def _get_date_planned(self, product_qty, start_date): days = self.lead_days or 0.0 if self.lead_type == 'supplier': @@ -841,20 +820,20 @@ class Orderpoint(models.Model): date=fields.Date.to_string(start_date), uom_id=self.product_uom).delay or 0.0 date_planned = start_date + relativedelta.relativedelta(days=days) - return date_planned.strftime(DEFAULT_SERVER_DATE_FORMAT) + return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT) @api.multi def _prepare_procurement_values(self, product_qty, date=False, group=False): return { 'name': self.name, 'date_planned': date or self._get_date_planned(product_qty, datetime.today()), - 'product_id': self.product_id.id, + 'product_id': self.product_id, 'product_qty': product_qty, - 'company_id': self.company_id.id, - 'product_uom': self.product_uom.id, - 'location_id': self.location_id.id, + 'company_id': self.company_id, + 'product_uom': self.product_uom, + 'location_id': self.location_id, 'origin': self.name, - 'warehouse_id': self.warehouse_id.id, - 'orderpoint_id': self.id, - 'group_id': group or self.group_id.id, + 'warehouse_id': self.warehouse_id, + 'orderpoint_id': self, + 'group_id': group or self.group_id, } diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv index 06bab5388b9..a81e08cbd9c 100644 --- a/addons/stock/security/ir.model.access.csv +++ b/addons/stock/security/ir.model.access.csv @@ -1,4 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_procurement_group,procurement.group,model_procurement_group,base.group_user,1,1,1,0 +access_procurement_rule,procurement.rule,model_procurement_rule,base.group_user,1,1,1,0 access_stock_incoterms_all,stock.incoterms all,model_stock_incoterms,,1,0,0,0 access_stock_incoterms_manager,stock.incoterms manager,model_stock_incoterms,stock.group_stock_manager,1,1,1,1 access_stock_warehouse_manager,stock.warehouse.manager,model_stock_warehouse,stock.group_stock_manager,1,1,1,1 @@ -72,8 +74,7 @@ access_barcode_rule_stock_manager,barcode.rule.stock.manager,barcodes.model_barc access_stock_forecast_user,report.stock.forecast.user,model_report_stock_forecast,stock.group_stock_user,1,0,0,0 access_stock_forecast_manager,report.stock.forecast.manager,model_report_stock_forecast,stock.group_stock_manager,1,1,1,1 access_stock_scrap_user,stock.scrap.user,model_stock_scrap,stock.group_stock_user,1,1,1,0 -access_procurement_order,procurement.order,stock.model_procurement_order,base.group_portal,1,0,0,0 access_product_attribute_manager,product.attribute manager,product.model_product_attribute,stock.group_stock_manager,1,1,1,1 access_product_attribute_value_manager,product.attribute manager value,product.model_product_attribute_value,stock.group_stock_manager,1,1,1,1 access_product_attribute_price_manager,product.attribute manager price,product.model_product_attribute_price,stock.group_stock_manager,1,1,1,1 -access_product_attribute_line_manager,product.attribute manager line,product.model_product_attribute_line,stock.group_stock_manager,1,1,1,1
\ No newline at end of file +access_product_attribute_line_manager,product.attribute manager line,product.model_product_attribute_line,stock.group_stock_manager,1,1,1,1 diff --git a/addons/stock/security/stock_security.xml b/addons/stock/security/stock_security.xml index 4f69671f6a1..cd380df04f4 100644 --- a/addons/stock/security/stock_security.xml +++ b/addons/stock/security/stock_security.xml @@ -155,5 +155,13 @@ <field name="global" eval="True"/> <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> </record> + + <record model="ir.rule" id="product_pulled_flow_comp_rule"> + <field name="name">product_pulled_flow multi-company</field> + <field name="model_id" ref="model_procurement_rule"/> + <field name="global" eval="True"/> + <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + </record> + </data> </odoo> diff --git a/addons/stock/test/procrule.yml b/addons/stock/test/procrule.yml index 7cc81da3a3f..d2cf3b29c79 100644 --- a/addons/stock/test/procrule.yml +++ b/addons/stock/test/procrule.yml @@ -38,7 +38,7 @@ - I run the scheduler. - - !python {model: procurement.order, id: False}: | + !python {model: procurement.group, id: False}: | self.run_scheduler() - Check that a picking was created from stock to output. diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 6291f804595..fb01d9edec9 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -2,7 +2,7 @@ from odoo.addons.stock.tests.common import TestStockCommon from odoo.tools import mute_logger, float_round - +from odoo.exceptions import UserError class TestStockFlow(TestStockCommon): @@ -1794,7 +1794,8 @@ class TestStockFlow(TestStockCommon): 'location_id': self.stock_location, 'location_dest_id': self.customer_location}) - move_mto_alone.action_confirm() + with self.assertRaises(UserError): + move_mto_alone.action_confirm() move_with_ancestors.action_confirm() other_move.action_confirm() diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index 23a44df81c6..749cd5cbc28 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -249,23 +249,4 @@ class TestResupply(TestStockCommon): 'product_min_qty': 10, 'product_max_qty': 100, 'product_uom': self.uom_unit.id, - }) - - def test_resupply_from_wh(self): - # TDE NOTE: replaces tests/test_resupply.py, present until saas-10 - OrderScheduler = self.env['procurement.order'] - OrderScheduler.run_scheduler() - # we generated 2 procurements for product A: one on small wh and the other one on the transit location - procs = OrderScheduler.search([('product_id', '=', self.product_1.id)]) - self.assertEqual(len(procs), 2) - - proc1 = procs.filtered(lambda order: order.warehouse_id == self.warehouse_2) - self.assertEqual(proc1.state, 'running') - - proc2 = procs.filtered(lambda order: order.warehouse_id == self.warehouse_1) - self.assertEqual(proc2.location_id.usage, 'transit') - self.assertNotEqual(proc2.state, 'exception') - - proc2.run() - self.assertEqual(proc2.state, 'running') - self.assertTrue(proc2.rule_id) + })
\ No newline at end of file diff --git a/addons/stock/views/procurement_views.xml b/addons/stock/views/procurement_views.xml index c36e2de7a6c..620ac1773bd 100644 --- a/addons/stock/views/procurement_views.xml +++ b/addons/stock/views/procurement_views.xml @@ -1,67 +1,80 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <record id="procurement_group_form_view_herited" model="ir.ui.view"> - <field name="name">procurement.group.view.form.inherit.picking</field> - <field name="model">procurement.group</field> - <field name="inherit_id" ref="procurement.procurement_group_form_view"/> - <field name="arch" type="xml"> - <div name="button_box" position="inside"> - <button name="%(do_view_pickings)d" string="Pickings" type="action"/> - </div> - </field> - </record> + <data> + <!-- + Procurement + --> - <!-- Procurements --> - - <record id="view_procurement_form_stock_inherit" model="ir.ui.view"> - <field name="name">procurement.order.form.stock.inherit</field> - <field name="model">procurement.order</field> - <field name="inherit_id" ref="procurement.procurement_form_view"/> + <record id="procurement_group_form_view" model="ir.ui.view"> + <field name="name">procurement.group.form</field> + <field name="model">procurement.group</field> <field name="arch" type="xml"> - <xpath expr="//group[@name='scheduling']" position="before"> - <group> - <field name="warehouse_id" groups="stock.group_stock_multi_warehouses"/> - <field name="location_id" domain="[('usage', '=', 'internal')]"/> - <field name="route_ids" widget="many2many_tags" groups="stock.group_adv_location"/> - </group> - </xpath> - <div name="button_box" position="inside"> - <button name="do_view_pickings" class="oe_stat_button" icon="fa-bars" string="Group's Pickings" type="object"/> - </div> - <xpath expr="//field[@name='rule_id']" position="replace"> - <field name="rule_id" domain="['|', ('location_id', '=', False), ('location_id', '=', location_id)]"/> - <field name="partner_dest_id"/> - </xpath> + <form string="Procurement group"> + <sheet> + <div class="oe_button_box" name="button_box"> + <button name="%(do_view_pickings)d" string="Pickings" type="action"/> + </div> + <group> + <field name="name"/> + <field name="move_type"/> + </group> + </sheet> + </form> </field> </record> - <record id="view_procurement_tree_stock_inherit" model="ir.ui.view"> - <field name="name">procurement.order.tree.stock.inherit</field> - <field name="model">procurement.order</field> - <field name="inherit_id" ref="procurement.procurement_tree_view"/> + + <!-- Procurement Rules --> + + <record id="view_procurement_rule_filter" model="ir.ui.view"> + <field name="name">procurement.rule.select</field> + <field name="model">procurement.rule</field> <field name="arch" type="xml"> - <xpath expr="//field[@name='origin']" position="before"> - <field name="location_id"/> - </xpath> + <search string="Search Procurement"> + <filter string="Global" name="route_false" domain="[('route_id','=',False)]" help="If the route is global"/> + <separator/> + <filter string="Archived" name="inactive" domain="[('active','=',False)]"/> + </search> </field> </record> - <record id="view_procurement_rule_tree_stock_inherit" model="ir.ui.view"> - <field name="name">procurement.rule.tree.stock.inherit</field> + + <record model="ir.ui.view" id="view_procurement_rule_tree"> + <field name="name">procurement.rule.tree</field> <field name="model">procurement.rule</field> - <field name="inherit_id" ref="procurement.view_procurement_rule_tree"/> + <field eval="10" name="priority"/> <field name="arch" type="xml"> - <xpath expr="//field[@name='action']" position="after"> + <tree string="Procurement Rules"> + <field name="sequence" widget="handle"/> + <field name="name"/> + <field name="active" invisible="1"/> + <field name="action"/> <field name="picking_type_id"/> - </xpath> + <field name='company_id' groups="base.group_multi_company"/> + </tree> </field> </record> - <record id="view_procurement_rule_form_stock_inherit" model="ir.ui.view"> - <field name="name">procurement.rule.form.stock.inherit</field> + <record model="ir.ui.view" id="view_procurement_rule_form"> + <field name="name">procurement.rule.form</field> <field name="model">procurement.rule</field> - <field name="inherit_id" ref="procurement.view_procurement_rule_form"/> + <field eval="10" name="priority"/> <field name="arch" type="xml"> - <xpath expr="//group[@name='general']" position="after"> - <group> + <form string="Procurement Rule"> + <div class="oe_button_box" name="button_box"> + <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive"> + <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/> + </button> + </div> + <div class="oe_title"> + <label for="name" class="oe_edit_only"/> + <h1><field name="name" placeholder="e.g. Buy"/></h1> + </div> + <group name="general"> + <group> + <field name="action"/> + </group> + <group> + <field name="sequence" groups="base.group_no_one"/> + </group> <group string="Applied On"> <field name="location_id"/> <field name="warehouse_id" groups="base.group_no_one"/> @@ -77,24 +90,14 @@ </div> </group> </group> - </xpath> - <xpath expr="//field[@name='group_id']" position="after"> - <field name="propagate"/> - <field name="propagate_warehouse_id"/> - </xpath> + <group name="propagation_group" string="Propagation Options" groups="base.group_no_one"> + <field name="group_propagation_option"/> + <field name="group_id" attrs="{'invisible': [('group_propagation_option', '!=', 'fixed')], 'required': [('group_propagation_option', '=', 'fixed')]}"/> + <field name="propagate"/> + <field name="propagate_warehouse_id"/> + </group> + </form> </field> </record> - - <record model="ir.actions.act_window" id="procrules"> - <field name="context">{'search_default_route_false':1}</field> - <field name="name">Procurement Rules</field> - <field name="res_model">procurement.rule</field> - </record> - - <!-- Procurements are located in Warehouse menu hierarchy, MRP users should come to Stock application to use it. --> - <menuitem id="procurement_order_compute_all_menu" name="Schedulers" sequence="5" groups="group_stock_manager" parent="menu_stock_warehouse_mgmt"/> - <menuitem action="procurement.action_compute_schedulers" id="menu_stock_proc_schedulers" parent="procurement_order_compute_all_menu" name="Run Scheduler" sequence="5" groups="stock.group_stock_manager"/> - <menuitem action="action_procurement_compute" id="menu_procurement_compute" parent="procurement_order_compute_all_menu" sequence="10" groups="base.group_no_one"/> - <menuitem id="menu_stock_procurement" name="Automatic Procurements" parent="stock.menu_stock_config_settings" sequence="5"/> - <menuitem action="procurement.procurement_order_action_exceptions" id="menu_stock_procurement_action" parent="stock.menu_warehouse_report" sequence="130" groups="stock.group_stock_manager"/> + </data> </odoo> diff --git a/addons/stock/views/product_strategy_views.xml b/addons/stock/views/product_strategy_views.xml index c93a1428bbd..503812ad9a4 100644 --- a/addons/stock/views/product_strategy_views.xml +++ b/addons/stock/views/product_strategy_views.xml @@ -7,7 +7,7 @@ <form string="Putaway"> <group colspan="4"> <field name="name"/> - <field name="method"/> + <field name="method"/> </group> <div attrs="{'invisible': [('method', '!=', 'fixed')]}"> <separator string="Fixed Locations Per Categories"/> @@ -35,4 +35,4 @@ </form> </field> </record> -</odoo>
\ No newline at end of file +</odoo> diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml index e0567e04061..156cae45cac 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -109,7 +109,6 @@ <field name="tracking" widget="radio" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}"/> </group> <group string="Counterpart Locations" name="stock_property" groups="base.group_no_one"> - <field name="property_stock_procurement" domain="[('usage','=','procurement')]"/> <field name="property_stock_production" domain="[('usage','=','production')]"/> <field name="property_stock_inventory" domain="[('usage','=','inventory')]"/> </group> @@ -167,7 +166,6 @@ <sheet position="before"> <header> <button name="%(action_view_change_product_quantity)d" string="Update Qty On Hand" type="action" attrs="{'invisible': [('type', '!=', 'product')]}"/> - <button string="Procurement Request" name="%(stock.act_make_procurement)d" type="action" attrs="{'invisible': [('type', '!=', 'product')]}"/> </header> </sheet> </field> @@ -185,10 +183,6 @@ type="action" name="%(action_view_change_product_quantity)d" attrs="{'invisible': [('type', '!=', 'product')]}"/> - <button string="Procurement Request" - type="action" - name="%(stock.act_make_procurement)d" - attrs="{'invisible': [('type', '!=', 'product')]}"/> </header> <button name="toggle_active" position="before"> <button class="oe_stat_button" @@ -253,7 +247,6 @@ <data> <header position="inside" > <button name="%(action_view_change_product_quantity)d" string="Update Qty On Hand" type="action" attrs="{'invisible': [('type', '!=', 'product')]}"/> - <button string="Procurement Request" name="%(stock.act_make_procurement)d" type="action" attrs="{'invisible': [('type', '!=', 'product')]}"/> </header> <button name="toggle_active" position="before"> <button type="object" @@ -312,6 +305,10 @@ <xpath expr="//group[@name='group_lots_and_weight']" position="attributes"> <attribute name="attrs">{'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]}</attribute> </xpath> + + <xpath expr="//group[@name='group_lots_and_weight']" position="inside"> + <field name="responsible_id"/> + </xpath> </data> </field> </record> diff --git a/addons/stock/views/stock_inventory_views.xml b/addons/stock/views/stock_inventory_views.xml index 4026cd2e277..bd6547c194e 100644 --- a/addons/stock/views/stock_inventory_views.xml +++ b/addons/stock/views/stock_inventory_views.xml @@ -142,7 +142,6 @@ <group> <field name="location_id" domain="[('usage','not in', ['supplier','production'])]" groups="stock.group_stock_multi_locations"/> <field name="filter" string="Inventory of" widget='radio' attrs="{'readonly': [('state', '!=', 'draft')]}"/> - <field name="exhausted" attrs="{'invisible': [('filter', 'in', ('owner', 'product_owner','lot','pack','partial', 'product'))]}"/> </group> <group> <field name="date"/> @@ -152,6 +151,7 @@ <field name="lot_id" attrs="{'invisible': [('filter', '!=', 'lot')], 'required': [('filter', '=', 'lot')]}" groups="stock.group_production_lot" /> <field name="partner_id" attrs="{'invisible': [('filter', 'not in', ('owner', 'product_owner'))], 'required': [('filter', 'in', ('owner', 'product_owner'))]}" groups="stock.group_tracking_owner"/> <field name="package_id" attrs="{'invisible': [('filter', '!=', 'pack')], 'required': [('filter', '=', 'pack')]}" groups="stock.group_tracking_lot"/> + <field name="exhausted" attrs="{'invisible': [('filter', 'in', ('owner', 'product_owner','lot','pack','partial', 'product'))]}"/> </group> </group> <notebook attrs="{'invisible':[('state','=','draft')]}"> diff --git a/addons/stock/views/stock_warehouse_views.xml b/addons/stock/views/stock_warehouse_views.xml index 2b9917c649f..e92b4bb815c 100644 --- a/addons/stock/views/stock_warehouse_views.xml +++ b/addons/stock/views/stock_warehouse_views.xml @@ -119,14 +119,6 @@ parent="menu_warehouse_config" sequence="1"/> <!-- Order Point --> - <record id="do_view_procurement_op" model="ir.actions.act_window"> - <field name="name">Procurements</field> - <field name="res_model">procurement.order</field> - <field name="view_type">form</field> - <field name="view_mode">tree,form</field> - <field name="domain">[('orderpoint_id','=',active_id)]</field> - </record> - <record id="view_stock_warehouse_orderpoint_kanban" model="ir.ui.view"> <field name="name">stock.warehouse.orderpoint.kanban</field> <field name="model">stock.warehouse.orderpoint</field> @@ -209,7 +201,6 @@ <form string="Reordering Rules"> <sheet> <div class="oe_button_box" name="button_box"> - <button name="%(stock.do_view_procurement_op)d" type="action" string="Procurements" class="oe_stat_button" icon="fa-refresh"/> <button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive"> <field name="active" widget="boolean_button" options='{"terminology": "archive"}'/> </button> @@ -229,8 +220,8 @@ </group> <group> <group string="Rules"> - <field name="product_min_qty" /> - <field name="product_max_qty" /> + <field name="product_min_qty"/> + <field name="product_max_qty"/> <field name="qty_multiple" string="Quantity Multiple"/> </group> <group string="Misc"> diff --git a/addons/stock/wizard/__init__.py b/addons/stock/wizard/__init__.py index d176757a45b..7e494f64cee 100644 --- a/addons/stock/wizard/__init__.py +++ b/addons/stock/wizard/__init__.py @@ -3,7 +3,6 @@ from . import stock_picking_return from . import stock_change_product_qty -from . import make_procurement -from . import procurement_orderpoint_compute +from . import stock_scheduler_compute from . import stock_immediate_transfer -from . import stock_backorder_confirmation
\ No newline at end of file +from . import stock_backorder_confirmation diff --git a/addons/stock/wizard/make_procurement.py b/addons/stock/wizard/make_procurement.py deleted file mode 100644 index 2fa3db75b5d..00000000000 --- a/addons/stock/wizard/make_procurement.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models - - -class MakeProcurement(models.TransientModel): - _name = 'make.procurement' - _description = 'Make Procurements' - - qty = fields.Float('Quantity', default=1.0, digits=(16, 2), required=True) - res_model = fields.Char('Res Model') - product_id = fields.Many2one('product.product', 'Product', required=True) - product_tmpl_id = fields.Many2one('product.template', 'Template', required=True) - product_variant_count = fields.Integer(string='Variant Number', related='product_tmpl_id.product_variant_count') - uom_id = fields.Many2one('product.uom', 'Unit of Measure', required=True) - warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', required=True) - date_planned = fields.Date('Planned Date', default=fields.Date.context_today, required=True) - route_ids = fields.Many2many('stock.location.route', string='Preferred Routes') - - @api.model - def default_get(self, fields): - # TDE FIXME: clean this method, overly complicated (onchange ?) - res = super(MakeProcurement, self).default_get(fields) - - if self.env.context.get('active_id') and self.env.context.get('active_model') == 'product.template': - product = self.env['product.product'].search([('product_tmpl_id', '=', self.env.context['active_id'])], limit=1) - elif self.env.context.get('active_id') and self.env.context.get('active_model') == 'product.product': - product = self.env['product.product'].browse(self.env.context['active_id']) - else: - product = self.env['product.product'] - if 'product_id' in fields and not res.get('product_id') and product: - res['product_id'] = product.id - if 'product_tmpl_id' in fields and not res.get('product_tmpl_id') and product: - res['product_tmpl_id'] = product.product_tmpl_id.id - if 'uom_id' in fields and not res.get('uom_id') and product: - res['uom_id'] = product.uom_id.id - if 'warehouse_id' in fields and not res.get('warehouse_id'): - res['warehouse_id'] = self.env['stock.warehouse'].search([], limit=1).id - return res - - def onchange_product_id_dict(self, product_id): - product = self.env['product.product'].browse(product_id) - return { - 'uom_id': product.uom_id.id, - 'product_tmpl_id': product.product_tmpl_id.id, - 'product_variant_count': product.product_tmpl_id.product_variant_count - } - - @api.onchange('product_id') - def onchange_product_id(self): - if self.product_id: - for key, value in self.onchange_product_id_dict(self.product_id.id).items(): - self[key] = value - - @api.model - def create(self, values): - if values.get('product_id'): - values.update(self.onchange_product_id_dict(values['product_id'])) - return super(MakeProcurement, self).create(values) - - @api.multi - def make_procurement(self): - """ Creates procurement order for selected product. """ - ProcurementOrder = self.env['procurement.order'] - for wizard in self: - procurement = ProcurementOrder.create({ - 'name': 'INT: %s' % (self.env.user.login), - 'date_planned': wizard.date_planned, - 'product_id': wizard.product_id.id, - 'product_qty': wizard.qty, - 'product_uom': wizard.uom_id.id, - 'warehouse_id': wizard.warehouse_id.id, - 'location_id': wizard.warehouse_id.lot_stock_id.id, - 'company_id': wizard.warehouse_id.company_id.id, - 'route_ids': [(6, 0, wizard.route_ids.ids)]}) - return { - 'view_type': 'form', - 'view_mode': 'tree,form', - 'res_model': 'procurement.order', - 'res_id': procurement.id, - 'views': [(False, 'form'), (False, 'tree')], - 'type': 'ir.actions.act_window', - } diff --git a/addons/stock/wizard/make_procurement_views.xml b/addons/stock/wizard/make_procurement_views.xml deleted file mode 100644 index b597aae9f17..00000000000 --- a/addons/stock/wizard/make_procurement_views.xml +++ /dev/null @@ -1,44 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <record id="view_make_procurment_wizard" model="ir.ui.view"> - <field name="name">Procurement Request</field> - <field name="model">make.procurement</field> - <field name="arch" type="xml"> - <form string="Procurement Request"> - <p class="oe_gray"> - Use this assistant to generate a procurement request for this - product. According to the product configuration, this may - trigger a draft purchase order, a manufacturing order or - a new task. - </p> - <group> - <field name="warehouse_id" groups="stock.group_stock_multi_locations"/> - <field name="product_tmpl_id" invisible="1"/> - <field name="product_variant_count" invisible="1"/> - <field name="product_id" widget="selection" - domain="[('product_tmpl_id', '=', product_tmpl_id)]" - attrs="{'readonly': [('product_variant_count', '=', 1)]}"/> - <label for="qty"/> - <div> - <field name="qty" class="oe_inline"/> - <field name="uom_id" groups="product.group_uom" class="oe_inline"/> - </div> - <field name="date_planned"/> - <field name="route_ids" widget="many2many_tags" groups="stock.group_adv_location"/> - </group> - <footer> - <button name="make_procurement" string="Ask New Products" type="object" class="btn-primary"/> - <button string="Cancel" class="btn-default" special="cancel" /> - </footer> - </form> - </field> - </record> - - <record model="ir.actions.act_window" id="act_make_procurement"> - <field name="name">Procurement Request</field> - <field name="res_model">make.procurement</field> - <field name="src_model">product.product</field> - <field name="view_mode">form</field> - <field name="target">new</field> - </record> -</odoo> diff --git a/addons/stock/wizard/procurement_orderpoint_compute_views.xml b/addons/stock/wizard/procurement_orderpoint_compute_views.xml deleted file mode 100644 index 3631ada8530..00000000000 --- a/addons/stock/wizard/procurement_orderpoint_compute_views.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - <record id="view_procurement_compute_wizard" model="ir.ui.view"> - <field name="name">Run Reordering Rules</field> - <field name="model">procurement.orderpoint.compute</field> - <field name="arch" type="xml"> - <form string="Parameters"> - <group> - <label string="Wizard checks all the stock minimum rules and generate procurement order."/> - </group> - <footer> - <button name="procure_calculation" string="Compute Stock" type="object" class="btn-primary"/> - <button string="Cancel" class="btn-default" special="cancel" /> - </footer> - </form> - </field> - </record> - - <record id="action_procurement_compute" model="ir.actions.act_window"> - <field name="name">Run Reordering Rules</field> - <field name="res_model">procurement.orderpoint.compute</field> - <field name="view_type">form</field> - <field name="view_mode">form</field> - <field name="target">new</field> - </record> -</odoo>
\ No newline at end of file diff --git a/addons/stock/wizard/procurement_orderpoint_compute.py b/addons/stock/wizard/stock_scheduler_compute.py index da4f6969d89..e1ee3446a16 100644 --- a/addons/stock/wizard/procurement_orderpoint_compute.py +++ b/addons/stock/wizard/stock_scheduler_compute.py @@ -6,23 +6,28 @@ # - Order if the virtual stock of today is bellow the min of the defined order point # -from odoo import api, models, tools +from odoo import api, models, tools, fields import logging import threading _logger = logging.getLogger(__name__) -class ProcurementOrderpointConfirm(models.TransientModel): - _name = 'procurement.orderpoint.compute' - _description = 'Compute Minimum Stock Rules' +class StockSchedulerCompute(models.TransientModel): + _name = 'stock.scheduler.compute' + _description = 'Run Scheduler Manually' + + orderpoints_only = fields.Boolean('Only Compute Orderpoints', default=False, + help="When this is checked, it will only calculate the reordering rules for this company. " + "When it is not checked, it will try to reserve stock where there is needed, it will try to " + "solve exception problems (because of routes e.g.) and it will ") def _procure_calculation_orderpoint(self): with api.Environment.manage(): # As this function is in a new thread, I need to open a new cursor, because the old one may be closed new_cr = self.pool.cursor() self = self.with_env(self.env(cr=new_cr)) - scheduler_cron = self.sudo().env.ref('procurement.ir_cron_scheduler_action') + scheduler_cron = self.sudo().env.ref('stock.ir_cron_scheduler_action') # Avoid to run the scheduler multiple times in the same time try: with tools.mute_logger('odoo.sql_db'): @@ -33,9 +38,15 @@ class ProcurementOrderpointConfirm(models.TransientModel): self._cr.close() return {} - self.env['procurement.order']._procure_orderpoint_confirm( - use_new_cursor=new_cr.dbname, - company_id=self.env.user.company_id.id) + if self.orderpoints_only: + self.env['procurement.group']._procure_orderpoint_confirm( + use_new_cursor=new_cr.dbname, + company_id=self.env.user.company_id.id) + else: + for company in self.env.user.company_ids: + self.env['procurement.group'].run_scheduler( + use_new_cursor=self._cr.dbname, + company_id=company.id) new_cr.close() return {} diff --git a/addons/stock/wizard/stock_scheduler_compute_views.xml b/addons/stock/wizard/stock_scheduler_compute_views.xml new file mode 100644 index 00000000000..ec8b5fe8884 --- /dev/null +++ b/addons/stock/wizard/stock_scheduler_compute_views.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="view_procurement_compute_wizard" model="ir.ui.view"> + <field name="name">Run Schedulers Manually</field> + <field name="model">stock.scheduler.compute</field> + <field name="arch" type="xml"> + <form string="Parameters"> + <p> + This will run the schedulers. Which will try to reserve not available deliveries, + try to see if it can solve any exceptions in the stock flow that might have been solved + and will run the reordering rules. + </p> + <field name="orderpoints_only" groups="base.group_no_one"/> + <footer> + <button name="procure_calculation" string="Run Schedulers" type="object" class="btn-primary"/> + <button string="Cancel" class="btn-default" special="cancel" /> + </footer> + </form> + </field> + </record> + + <record id="action_procurement_compute" model="ir.actions.act_window"> + <field name="name">Run Schedulers</field> + <field name="res_model">stock.scheduler.compute</field> + <field name="view_type">form</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> + + <menuitem id="menu_stock_sched" name="Run Schedulers" parent="stock.menu_stock_root" groups="group_stock_manager" sequence="50"/> + <menuitem action="action_procurement_compute" id="menu_procurement_compute" parent="menu_stock_sched"/> + + +</odoo> diff --git a/addons/stock_calendar/__manifest__.py b/addons/stock_calendar/__manifest__.py index f771d78acf1..46cdbbe3e64 100644 --- a/addons/stock_calendar/__manifest__.py +++ b/addons/stock_calendar/__manifest__.py @@ -51,7 +51,7 @@ ], 'test': [ ], - 'installable': True, + 'installable': False, 'application': False, 'auto_install': False, 'qweb': [], diff --git a/addons/stock_calendar/tests/test_stock_calendar.py b/addons/stock_calendar/tests/test_stock_calendar.py index 6f00f5d0065..f3aaeec6f72 100644 --- a/addons/stock_calendar/tests/test_stock_calendar.py +++ b/addons/stock_calendar/tests/test_stock_calendar.py @@ -42,15 +42,17 @@ class TestsStockCalendar(common.TransactionCase): 'name': self.res_partner_id.id, 'delay': 1 })], - 'orderpoint_ids': [(0, 0, { - 'name': 'Product A Truck', - 'calendar_id': self.resource_calendar_id.id, - 'product_min_qty': 0, - 'product_max_qty': 10, - 'warehouse_id': self.stock_warehouse0_id - })] }) + self.env['stock.warehouse.orderpoint'].create({ + 'name': 'Product A Truck', + 'product_id': self.calendar_product_id.id, + 'calendar_id': self.resource_calendar_id.id, + 'product_min_qty': 0, + 'product_max_qty': 10, + 'warehouse_id': self.stock_warehouse0_id + })] + # Create delivery order with product A self.pick_out_calendar = self._create_stock_picking('Delivery order for procurement', self.calendar_product_id.name, 3.00) diff --git a/addons/stock_calendar/views/procurement_views.xml b/addons/stock_calendar/views/procurement_views.xml index b12f7d3d63a..6fa84137e47 100644 --- a/addons/stock_calendar/views/procurement_views.xml +++ b/addons/stock_calendar/views/procurement_views.xml @@ -1,14 +1,3 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <!-- For testing purposes --> - <record id="view_procurement_form_stock_inherit" model="ir.ui.view"> - <field name="name">procurement.order.form.stock.inherit</field> - <field name="model">procurement.order</field> - <field name="inherit_id" ref="procurement.procurement_form_view"/> - <field name="arch" type="xml"> - <field name="state" position="before"> - <button name="assign_group_date" string="Calculate dates according to Orderpoint" type="object"/> - </field> - </field> - </record> </odoo> diff --git a/addons/stock_dropshipping/__init__.py b/addons/stock_dropshipping/__init__.py index 67dee8c60db..e9917144f69 100644 --- a/addons/stock_dropshipping/__init__.py +++ b/addons/stock_dropshipping/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models
\ No newline at end of file diff --git a/addons/stock_dropshipping/__manifest__.py b/addons/stock_dropshipping/__manifest__.py index 67f85be9f66..c00b3985a63 100644 --- a/addons/stock_dropshipping/__manifest__.py +++ b/addons/stock_dropshipping/__manifest__.py @@ -26,7 +26,7 @@ internal transfer document is needed. 'test': [ '../account/test/account_minimal_test.xml', '../stock_account/test/stock_valuation_account.xml', - 'test/cancellation_propagated.yml', + #'test/cancellation_propagated.yml', 'test/crossdock.yml', 'test/dropship.yml', 'test/procurementexception.yml', diff --git a/addons/stock_dropshipping/models/__init__.py b/addons/stock_dropshipping/models/__init__.py new file mode 100644 index 00000000000..c617a28246d --- /dev/null +++ b/addons/stock_dropshipping/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import purchase
\ No newline at end of file diff --git a/addons/stock_dropshipping/models/purchase.py b/addons/stock_dropshipping/models/purchase.py new file mode 100644 index 00000000000..b1b54fa0209 --- /dev/null +++ b/addons/stock_dropshipping/models/purchase.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + sale_line_id = fields.Many2one('sale.order.line') + + @api.multi + def _prepare_stock_moves(self, picking): + res = super(PurchaseOrderLine, self)._prepare_stock_moves(picking) + for re in res: + re['sale_line_id'] = self.sale_line_id.id + return res + + +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + @api.model + def _prepare_purchase_order_line(self, values, rule, po, supplier): + res = super(ProcurementGroup, self)._prepare_purchase_order_line(values, rule, po, supplier) + res['sale_line_id'] = values.get('sale_line_id', False) + return res + diff --git a/addons/stock_dropshipping/test/crossdock.yml b/addons/stock_dropshipping/test/crossdock.yml index 9f83ba26557..b1960455075 100644 --- a/addons/stock_dropshipping/test/crossdock.yml +++ b/addons/stock_dropshipping/test/crossdock.yml @@ -4,6 +4,14 @@ !record {model: res.partner, id: supplier_crossdock}: name: Crossdocking supplier - + I first create a warehouse with pick-pack-ship and reception in 2 steps +- + !record {model: stock.warehouse, id: wh_pps}: + name: WareHouse PickPackShip + code: whpps + reception_steps: 'two_steps' + delivery_steps: 'pick_pack_ship' +- Use the warehouse created in cancellation_propagated.yml. - !python {model: stock.warehouse, id: wh_pps}: | @@ -36,7 +44,7 @@ product_uom_qty: 100.00 - !python {model: sale.order.line, id: False}: | - route_wh_pps_crossdock = self.env['stock.warehouse'].browse(ref('stock_dropshipping.wh_pps')).crossdock_route_id.id + route_wh_pps_crossdock = self.env['stock.warehouse'].browse(ref('wh_pps')).crossdock_route_id.id order = self.env['sale.order'].browse(ref('sale_order_crossdock_shpng')) order.order_line.write({'route_id': route_wh_pps_crossdock}) - @@ -47,7 +55,7 @@ - Run the scheduler. - - !python {model: procurement.order, id: False}: | + !python {model: procurement.group, id: False}: | self.run_scheduler() - Check a quotation was created for the created supplier and confirm it. diff --git a/addons/stock_dropshipping/test/dropship.yml b/addons/stock_dropshipping/test/dropship.yml index 1436cb275c1..3bb1f5eb8d4 100644 --- a/addons/stock_dropshipping/test/dropship.yml +++ b/addons/stock_dropshipping/test/dropship.yml @@ -41,26 +41,23 @@ - !python {model: procurement.group, id: False}: | sale_record = self.env["sale.order"].browse(ref('sale_order_drp_shpng')) - assert sale_record.procurement_group_id.procurement_ids[0].product_qty == 200 + assert sale_record.procurement_group_id, 'SO should have procurement group' - Check a quotation was created to a certain vendor and confirm so it becomes a confirmed purchase order - !python {model: purchase.order, id: False}: | from odoo import netsvc sale_record = self.env["sale.order"].browse(ref('sale_order_drp_shpng')) - procurement_order = sale_record.procurement_group_id.procurement_ids[0] - purchase = procurement_order.purchase_line_id.order_id - + purchase = self.env['purchase.order'].search([('partner_id', '=', ref('supplier_dropship'))]) purchase.button_confirm() - po_id = self.env['purchase.order'].search([('partner_id', '=', ref('supplier_dropship'))]) assert purchase.state == 'purchase', 'Purchase order should be in the approved state' - Use 'Receive Products' button to immediately view this picking, it should have created a picking with 200 pieces -- +- !python {model: purchase.order, id: False}: | po_id = self.search([('partner_id', '=', ref('supplier_dropship'))]).ids assert len(po_id) == 1, 'There should be one picking' -- +- Send the 200 pieces. - !python {model: stock.picking, id: False}: | @@ -75,4 +72,4 @@ !python {model: stock.move.line, id: False}: | move_line = self.search([('location_dest_id', '=', ref('stock.stock_location_customers')), ('product_id', '=', ref("drop_shop_product"))]) assert move_line, 'No move line found' - assert len(move_line.ids) == 1, 'There should be exactly one move line' + assert len(move_line.ids) == 1, 'There should be exactly one move line'
\ No newline at end of file diff --git a/addons/stock_dropshipping/test/procurementexception.yml b/addons/stock_dropshipping/test/procurementexception.yml index cb737401161..8162cc34abd 100644 --- a/addons/stock_dropshipping/test/procurementexception.yml +++ b/addons/stock_dropshipping/test/procurementexception.yml @@ -20,18 +20,15 @@ product_uom_qty: 1 route_id: route_drop_shipping - - I confirm the sales order. + I confirm the sales order, but it will raise an error - !python {model: sale.order, id: sale_order_route_dropship01}: | - self.action_confirm() -- - I check there is a procurement in exception that has the procurement group of the sales order created before. -- - !python {model: procurement.order, id: False}: | - self.run_scheduler() - sale_order = self.env['sale.order'].browse(ref("sale_order_route_dropship01")) - procs = self.search([('group_id.name', '=', sale_order.name), ('state', '=', 'exception')]) - assert procs, 'No Procurement!' + b=False + try: + self.action_confirm() + except: + b=True + assert b, 'SO confirmation should throw error' - I set the at least one supplier on the product. - @@ -41,24 +38,13 @@ name: base.res_partner_2 min_qty: 2.0 - - I run the Procurement. -- - !python {model: procurement.order, id: False}: | - sale_order = self.env['sale.order'].browse(ref("sale_order_route_dropship01")) - procs = self.search([('group_id.name', '=', sale_order.name), ('state', '=', 'exception')]) - procs.run() + I confirm the sales order, no error this time - - I check the status changed there is no procurement order in exception any more from that procurement group -- - !python {model: procurement.order, id: False}: | - sale_order = self.env['sale.order'].browse(ref("sale_order_route_dropship01")) - procs = self.search([('group_id.name', '=', sale_order.name), ('state', '=', 'exception')]) - assert not procs, 'Procurement should be in running state' + !python {model: sale.order, id: sale_order_route_dropship01}: | + self.action_confirm() - I check a purchase quotation was created. - - !python {model: procurement.order, id: False}: | - sale_order = self.env['sale.order'].browse(ref("sale_order_route_dropship01")) - procs = self.search([('group_id.name', '=', sale_order.name)]) - purchase_id = [proc.purchase_line_id.order_id for proc in procs if proc.purchase_line_id] - assert purchase_id, 'No Purchase Quotation is created' + !python {model: sale.order, id: sale_order_route_dropship01}: | + purchase = self.env['purchase.order.line'].search([('sale_line_id', '=', self.order_line.ids[0])]).order_id + assert purchase, 'No Purchase Quotation is created' diff --git a/odoo/addons/test_main_flows/static/src/js/tour.js b/odoo/addons/test_main_flows/static/src/js/tour.js index 9392035199e..749cb958dfb 100644 --- a/odoo/addons/test_main_flows/static/src/js/tour.js +++ b/odoo/addons/test_main_flows/static/src/js/tour.js @@ -403,16 +403,11 @@ tour.register('main_flow_tour', { }, { // Run the schedulers edition: "enterprise", - trigger: ".o_menu_sections a:contains('Operations')", - content: _t("Go to Operations"), + trigger: ".o_menu_sections a:contains('Run Schedulers')", + content: _t("Go to Run Schedulers"), position: "bottom" -}, { - edition: "community", - trigger: ".oe_menu_toggler[data-menu-xmlid='stock.procurement_order_compute_all_menu']", - content: _t("Click on submenu"), - position: "bottom" -}, { - trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_stock_proc_schedulers'], .oe_secondary_submenu a[data-menu-xmlid='stock.menu_stock_proc_schedulers']", +},{ + trigger: ".o_menu_sections a[data-menu-xmlid='stock.menu_procurement_compute'], .oe_secondary_submenu a[data-menu-xmlid='stock.menu_procurement_compute']", content: _t("Click on schedulers"), position: "bottom" }, { |
