diff options
Diffstat (limited to 'addons/sale_mrp/models')
| -rw-r--r-- | addons/sale_mrp/models/__init__.py | 6 | ||||
| -rw-r--r-- | addons/sale_mrp/models/account_move.py | 31 | ||||
| -rw-r--r-- | addons/sale_mrp/models/mrp_production.py | 38 | ||||
| -rw-r--r-- | addons/sale_mrp/models/sale.py | 145 |
4 files changed, 220 insertions, 0 deletions
diff --git a/addons/sale_mrp/models/__init__.py b/addons/sale_mrp/models/__init__.py new file mode 100644 index 00000000..6e09b9fa --- /dev/null +++ b/addons/sale_mrp/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import sale +from . import account_move +from . import mrp_production diff --git a/addons/sale_mrp/models/account_move.py b/addons/sale_mrp/models/account_move.py new file mode 100644 index 00000000..6f338835 --- /dev/null +++ b/addons/sale_mrp/models/account_move.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from odoo import models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + def _stock_account_get_anglo_saxon_price_unit(self): + price_unit = super(AccountMoveLine, self)._stock_account_get_anglo_saxon_price_unit() + + so_line = self.sale_line_ids and self.sale_line_ids[-1] or False + if so_line: + bom = so_line.product_id.product_tmpl_id.bom_ids.filtered(lambda b: not b.company_id or b.company_id == so_line.company_id)[:1] + if bom and bom.type == 'phantom': + qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id) + qty_invoiced = sum([x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in so_line.invoice_lines if x.move_id.state == 'posted']) + moves = so_line.move_ids + average_price_unit = 0 + components = so_line._get_bom_component_qty(bom) + for product_id in components: + product = self.env['product.product'].browse(product_id) + factor = components[product_id]['qty'] + prod_moves = moves.filtered(lambda m: m.product_id == product) + prod_qty_invoiced = factor * qty_invoiced + prod_qty_to_invoice = factor * qty_to_invoice + average_price_unit += factor * product._compute_average_price(prod_qty_invoiced, prod_qty_to_invoice, prod_moves) + price_unit = average_price_unit / bom.product_qty or price_unit + price_unit = self.product_id.uom_id._compute_price(price_unit, self.product_uom_id) + return price_unit + diff --git a/addons/sale_mrp/models/mrp_production.py b/addons/sale_mrp/models/mrp_production.py new file mode 100644 index 00000000..5b6df8fd --- /dev/null +++ b/addons/sale_mrp/models/mrp_production.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + sale_order_count = fields.Integer( + "Count of Source SO", + compute='_compute_sale_order_count', + groups='sales_team.group_sale_salesman') + + @api.depends('procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id') + def _compute_sale_order_count(self): + for production in self: + production.sale_order_count = len(production.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id) + + def action_view_sale_orders(self): + self.ensure_one() + sale_order_ids = self.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id.ids + action = { + 'res_model': 'sale.order', + 'type': 'ir.actions.act_window', + } + if len(sale_order_ids) == 1: + action.update({ + 'view_mode': 'form', + 'res_id': sale_order_ids[0], + }) + else: + action.update({ + 'name': _("Sources Sale Orders of %s", self.name), + 'domain': [('id', 'in', sale_order_ids)], + 'view_mode': 'tree,form', + }) + return action diff --git a/addons/sale_mrp/models/sale.py b/addons/sale_mrp/models/sale.py new file mode 100644 index 00000000..8152b67f --- /dev/null +++ b/addons/sale_mrp/models/sale.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + mrp_production_count = fields.Integer( + "Count of MO generated", + compute='_compute_mrp_production_count', + groups='mrp.group_mrp_user') + + @api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids') + def _compute_mrp_production_count(self): + for sale in self: + sale.mrp_production_count = len(sale.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids) + + def action_view_mrp_production(self): + self.ensure_one() + mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids + action = { + 'res_model': 'mrp.production', + 'type': 'ir.actions.act_window', + } + if len(mrp_production_ids) == 1: + action.update({ + 'view_mode': 'form', + 'res_id': mrp_production_ids[0], + }) + else: + action.update({ + 'name': _("Manufacturing Orders Generated by %s", self.name), + 'domain': [('id', 'in', mrp_production_ids)], + 'view_mode': 'tree,form', + }) + return action + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + @api.depends('product_uom_qty', 'qty_delivered', 'product_id', 'state') + def _compute_qty_to_deliver(self): + """The inventory widget should now be visible in more cases if the product is consumable.""" + super(SaleOrderLine, self)._compute_qty_to_deliver() + for line in self: + # Hide the widget for kits since forecast doesn't support them. + boms = self.env['mrp.bom'] + if line.state == 'sale': + boms = line.move_ids.mapped('bom_line_id.bom_id') + elif line.state in ['draft', 'sent'] and line.product_id: + boms = boms._bom_find(product=line.product_id, company_id=line.company_id.id, bom_type='phantom') + relevant_bom = boms.filtered(lambda b: b.type == 'phantom' and + (b.product_id == line.product_id or + (b.product_tmpl_id == line.product_id.product_tmpl_id and not b.product_id))) + if relevant_bom: + line.display_qty_widget = False + continue + if line.state == 'draft' and line.product_type == 'consu': + components = line.product_id.get_components() + if components and components != [line.product_id.id]: + line.display_qty_widget = True + + def _compute_qty_delivered(self): + super(SaleOrderLine, self)._compute_qty_delivered() + for order_line in self: + if order_line.qty_delivered_method == 'stock_move': + boms = order_line.move_ids.filtered(lambda m: m.state != 'cancel').mapped('bom_line_id.bom_id') + dropship = False + if not boms and any(m._is_dropshipped() for m in order_line.move_ids): + boms = boms._bom_find(product=order_line.product_id, company_id=order_line.company_id.id, bom_type='phantom') + dropship = True + # We fetch the BoMs of type kits linked to the order_line, + # the we keep only the one related to the finished produst. + # This bom shoud be the only one since bom_line_id was written on the moves + relevant_bom = boms.filtered(lambda b: b.type == 'phantom' and + (b.product_id == order_line.product_id or + (b.product_tmpl_id == order_line.product_id.product_tmpl_id and not b.product_id))) + if relevant_bom: + # In case of dropship, we use a 'all or nothing' policy since 'bom_line_id' was + # not written on a move coming from a PO. + # FIXME: if the components of a kit have different suppliers, multiple PO + # are generated. If one PO is confirmed and all the others are in draft, receiving + # the products for this PO will set the qty_delivered. We might need to check the + # state of all PO as well... but sale_mrp doesn't depend on purchase. + if dropship: + moves = order_line.move_ids.filtered(lambda m: m.state != 'cancel') + if moves and all(m.state == 'done' for m in moves): + order_line.qty_delivered = order_line.product_uom_qty + else: + order_line.qty_delivered = 0.0 + continue + moves = order_line.move_ids.filtered(lambda m: m.state == 'done' and not m.scrapped) + filters = { + 'incoming_moves': lambda m: m.location_dest_id.usage == 'customer' and (not m.origin_returned_move_id or (m.origin_returned_move_id and m.to_refund)), + 'outgoing_moves': lambda m: m.location_dest_id.usage != 'customer' and m.to_refund + } + order_qty = order_line.product_uom._compute_quantity(order_line.product_uom_qty, relevant_bom.product_uom_id) + qty_delivered = moves._compute_kit_quantities(order_line.product_id, order_qty, relevant_bom, filters) + order_line.qty_delivered = relevant_bom.product_uom_id._compute_quantity(qty_delivered, order_line.product_uom) + + # If no relevant BOM is found, fall back on the all-or-nothing policy. This happens + # when the product sold is made only of kits. In this case, the BOM of the stock moves + # do not correspond to the product sold => no relevant BOM. + elif boms: + if all(m.state == 'done' for m in order_line.move_ids): + order_line.qty_delivered = order_line.product_uom_qty + else: + order_line.qty_delivered = 0.0 + + def _get_bom_component_qty(self, bom): + bom_quantity = self.product_uom._compute_quantity(1, bom.product_uom_id) + boms, lines = bom.explode(self.product_id, bom_quantity) + components = {} + for line, line_data in lines: + product = line.product_id.id + uom = line.product_uom_id + qty = line_data['qty'] + if components.get(product, False): + if uom.id != components[product]['uom']: + from_uom = uom + to_uom = self.env['uom.uom'].browse(components[product]['uom']) + qty = from_uom._compute_quantity(qty, to_uom) + components[product]['qty'] += qty + else: + # To be in the uom reference of the product + to_uom = self.env['product.product'].browse(product).uom_id + if uom.id != to_uom.id: + from_uom = uom + qty = from_uom._compute_quantity(qty, to_uom) + components[product] = {'qty': qty, 'uom': to_uom.id} + return components + + def _get_qty_procurement(self, previous_product_uom_qty=False): + self.ensure_one() + # Specific case when we change the qty on a SO for a kit product. + # We don't try to be too smart and keep a simple approach: we compare the quantity before + # and after update, and return the difference. We don't take into account what was already + # sent, or any other exceptional case. + bom = self.env['mrp.bom']._bom_find(product=self.product_id, bom_type='phantom') + if bom and previous_product_uom_qty: + return previous_product_uom_qty and previous_product_uom_qty.get(self.id, 0.0) + return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty) |
