[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
This commit is contained in:
Fabien Pinckaers
2017-09-08 17:08:05 +02:00
committed by amoyaux
parent ad2dc1e7cd
commit 0e4f3bb959
101 changed files with 1079 additions and 1698 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 -->
+2 -13
View File
@@ -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
+54 -78
View File
@@ -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')
class ProcurementGroup(models.Model):
_inherit = 'procurement.group'
@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()
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 _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()
@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
+1 -1
View File
@@ -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']
@@ -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">
-2
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_mrp_bom mrp.bom model_mrp_bom group_mrp_user 1 0 0 0
9 access_mrp_bom_line mrp.bom.line model_mrp_bom_line group_mrp_user 1 0 0 0
10 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
11 access_mrp_workcenter_manager mrp.workcenter.manager model_mrp_workcenter mrp.group_mrp_manager 1 1 1 1
12 access_mrp_resource_manager access_mrp_resource manager resource.model_resource_resource mrp.group_mrp_manager 1 1 1 1
13 access_mrp_routing_manager mrp.routing.manager model_mrp_routing mrp.group_mrp_manager 1 1 1 1
18 access_stock_move_mrp_worker stock.move mrp_worker stock.model_stock_move mrp.group_mrp_user 1 1 1 0
19 access_stock_picking_mrp_worker stock.picking mrp_worker stock.model_stock_picking mrp.group_mrp_user 1 1 1 1
20 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
21 access_mrp_production_stock_worker mrp.production stock_worker model_mrp_production stock.group_stock_user 1 0 0 0
22 access_product_product_user product.product user product.model_product_product mrp.group_mrp_user 1 0 0 0
23 access_product_template_user product.template user product.model_product_template mrp.group_mrp_user 1 0 0 0
+14 -20
View File
@@ -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,
})
+2 -15
View File
@@ -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>
</odoo>
@@ -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>
+2 -2
View File
@@ -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,
}
-6
View File
@@ -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"
+1 -1
View File
@@ -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"
+1
View File
@@ -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>
-1
View File
@@ -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',
+145 -262
View File
@@ -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()
# 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)
def _get_purchase_schedule_date(self):
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):
+12
View File
@@ -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
@@ -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'
+12 -8
View File
@@ -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):
@@ -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.')
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.')
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_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..')
+2 -2
View File
@@ -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')
@@ -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>
-13
View File
@@ -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">
+15
View File
@@ -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>
@@ -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
@@ -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.
@@ -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}"
+2 -2
View File
@@ -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,
}
}
+1 -2
View File
@@ -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
from . import sales_team
-9
View File
@@ -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')
+8 -64
View File
@@ -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)
+2 -4
View File
@@ -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', '&gt;', 0), ('procurement_ids', '!=', [])]}"
attrs="{'readonly': [('qty_invoiced', '&gt;', 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', '&gt;', 0), ('procurement_ids', '!=', [])]}"
attrs="{'readonly': [('qty_invoiced', '&gt;', 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"/>
-1
View File
@@ -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
-1
View File
@@ -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,
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import procurement
from . import sale_mrp
-24
View File
@@ -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
@@ -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
@@ -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)
+1 -26
View File
@@ -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'
+4 -13
View File
@@ -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.')
@@ -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
@@ -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")
-2
View File
@@ -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
+1 -2
View File
@@ -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
-18
View File
@@ -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')
-15
View File
@@ -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()
@@ -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')
+79 -30
View File
@@ -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 _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):
"""Computes the delivered quantity on sales order lines, based on done stock moves related to its procurements
"""
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,
+15 -28
View File
@@ -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
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_picking_salesman stock_picking salesman stock.model_stock_picking sales_team.group_sale_salesman 1 1 1 0
3 access_stock_move_salesman stock_move salesman stock.model_stock_move sales_team.group_sale_salesman 1 1 1 0
4 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
5 access_sale_order_stock_worker sale.order stock worker model_sale_order stock.group_stock_user 1 1 0 0
6 access_sale_order_line_stock_worker sale.order.line stock worker model_sale_order_line stock.group_stock_user 1 1 0 0
7 access_stock_picking_sales stock.picking.sales stock.model_stock_picking sales_team.group_sale_manager 1 1 1 1
@@ -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)
@@ -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>
+1 -2
View File
@@ -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,
}
}
-1
View File
@@ -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
@@ -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
-10
View File
@@ -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()
+1 -3
View File
@@ -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
@@ -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'
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_procurement_project_user procurement.order project_user model_procurement_order project.group_project_user 1 1 1 1
@@ -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>
@@ -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>
+5 -5
View File
@@ -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,
+15
View File
@@ -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>
-5
View File
@@ -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')]"/>
+1 -1
View File
@@ -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>
+19
View File
@@ -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>
+1 -1
View File
@@ -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
+175 -168
View File
@@ -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
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)")
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"
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.model
def _get_action(self):
result = super(ProcurementRule, self)._get_action()
return result + [('move', _('Move From Another Location'))]
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)
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)])
@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()
@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.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
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 _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
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 {}
+2 -6
View File
@@ -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')],
+4 -6
View File
@@ -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
+1 -2
View File
@@ -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
+14 -20
View File
@@ -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
+2 -3
View File
@@ -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,
+15 -36
View File
@@ -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,
}
+3 -2
View File
@@ -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
access_product_attribute_line_manager,product.attribute manager line,product.model_product_attribute_line,stock.group_stock_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_procurement_group procurement.group model_procurement_group base.group_user 1 1 1 0
3 access_procurement_rule procurement.rule model_procurement_rule base.group_user 1 1 1 0
4 access_stock_incoterms_all stock.incoterms all model_stock_incoterms 1 0 0 0
5 access_stock_incoterms_manager stock.incoterms manager model_stock_incoterms stock.group_stock_manager 1 1 1 1
6 access_stock_warehouse_manager stock.warehouse.manager model_stock_warehouse stock.group_stock_manager 1 1 1 1
74 access_stock_forecast_user report.stock.forecast.user model_report_stock_forecast stock.group_stock_user 1 0 0 0
75 access_stock_forecast_manager report.stock.forecast.manager model_report_stock_forecast stock.group_stock_manager 1 1 1 1
76 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
77 access_product_attribute_manager product.attribute manager product.model_product_attribute stock.group_stock_manager 1 1 1 1
78 access_product_attribute_value_manager product.attribute manager value product.model_product_attribute_value stock.group_stock_manager 1 1 1 1
79 access_product_attribute_price_manager product.attribute manager price product.model_product_attribute_price stock.group_stock_manager 1 1 1 1
80 access_product_attribute_line_manager product.attribute manager line product.model_product_attribute_line stock.group_stock_manager 1 1 1 1
+8
View File
@@ -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>
+1 -1
View File
@@ -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.
+3 -2
View File
@@ -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()
+1 -20
View File
@@ -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)
})
+71 -68
View File
@@ -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"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='origin']" position="before">
<field name="location_id"/>
</xpath>
</field>
</record>
<record id="view_procurement_rule_tree_stock_inherit" model="ir.ui.view">
<field name="name">procurement.rule.tree.stock.inherit</field>
<!-- 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="inherit_id" ref="procurement.view_procurement_rule_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='action']" position="after">
<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 model="ir.ui.view" id="view_procurement_rule_tree">
<field name="name">procurement.rule.tree</field>
<field name="model">procurement.rule</field>
<field eval="10" name="priority"/>
<field name="arch" type="xml">
<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>
@@ -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>
</odoo>
+4 -7
View File
@@ -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', '&gt;', 1)]}</attribute>
</xpath>
<xpath expr="//group[@name='group_lots_and_weight']" position="inside">
<field name="responsible_id"/>
</xpath>
</data>
</field>
</record>
+1 -1
View File
@@ -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')]}">
+2 -11
View File
@@ -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">
+2 -3
View File
@@ -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
from . import stock_backorder_confirmation
-84
View File
@@ -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',
}
@@ -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>
@@ -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>
@@ -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 {}
@@ -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>
+1 -1
View File
@@ -51,7 +51,7 @@
],
'test': [
],
'installable': True,
'installable': False,
'application': False,
'auto_install': False,
'qweb': [],
@@ -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)
@@ -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>
+2
View File
@@ -1,2 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+1 -1
View File
@@ -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',
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import purchase
@@ -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
+10 -2
View File
@@ -3,6 +3,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.
-
@@ -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.
+5 -8
View File
@@ -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'
@@ -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.
I confirm the sales order, no error this time
-
!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 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'

Some files were not shown because too many files have changed in this diff Show More