diff --git a/purchase_suggest/__init__.py b/purchase_suggest/__init__.py new file mode 100644 index 0000000..b22a526 --- /dev/null +++ b/purchase_suggest/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- + +from . import stock +from . import wizard diff --git a/purchase_suggest/__openerp__.py b/purchase_suggest/__openerp__.py new file mode 100644 index 0000000..0fe66ab --- /dev/null +++ b/purchase_suggest/__openerp__.py @@ -0,0 +1,49 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Purchase Suggest module for Odoo +# Copyright (C) 2015 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +{ + 'name': 'Purchase Suggest', + 'version': '0.1', + 'category': 'Purchase', + 'license': 'AGPL-3', + 'summary': 'Suggest POs from special suggest orderpoints', + 'description': """ +Purchase Suggest +================ + +This module is an ALTERNATIVE to the module *procurement_suggest* ; it is similar but it only handles the purchase orders and doesn't generate any procurement : the suggestions create a new purchase order directly. + +The advantage is that you are not impacted by the faulty procurements (for example : a procurement generates a PO ; the PO is confirmed ; the related picking is cancelled and deleted -> the procurements will always stay in running without related stock moves !) + +This module has been written by Alexis de Lattre from Akretion . + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['purchase'], + 'conflicts': ['procurement_suggest'], + 'data': [ + 'stock_view.xml', + 'wizard/purchase_suggest_view.xml', + ], + 'installable': True, +} diff --git a/purchase_suggest/odoo-purchase_suggest.patch b/purchase_suggest/odoo-purchase_suggest.patch new file mode 100644 index 0000000..f415aa3 --- /dev/null +++ b/purchase_suggest/odoo-purchase_suggest.patch @@ -0,0 +1,12 @@ +diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py +index 7b067ed..98857e6 100644 +--- a/addons/stock/procurement.py ++++ b/addons/stock/procurement.py +@@ -342,6 +342,7 @@ class procurement_order(osv.osv): + + procurement_obj = self.pool.get('procurement.order') + dom = company_id and [('company_id', '=', company_id)] or [] ++ dom.append(('suggest', '=', False)) + orderpoint_ids = orderpoint_obj.search(cr, uid, dom) + prev_ids = [] + while orderpoint_ids: diff --git a/purchase_suggest/stock.py b/purchase_suggest/stock.py new file mode 100644 index 0000000..8fe34c5 --- /dev/null +++ b/purchase_suggest/stock.py @@ -0,0 +1,29 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Purchase Suggest module for Odoo +# Copyright (C) 2015 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields + + +class StockWarehouseOrderpoint(models.Model): + _inherit = 'stock.warehouse.orderpoint' + + suggest = fields.Boolean(string='Suggest', default=True) diff --git a/purchase_suggest/stock_view.xml b/purchase_suggest/stock_view.xml new file mode 100644 index 0000000..1455cf9 --- /dev/null +++ b/purchase_suggest/stock_view.xml @@ -0,0 +1,48 @@ + + + + + + + + + procurement_suggest.orderpoint.form + stock.warehouse.orderpoint + + + + + + + + + + procurement_suggest.orderpoint.tree + stock.warehouse.orderpoint + + + + + + + + + + procurement_suggest.orderpoint.search + stock.warehouse.orderpoint + + + + + + + + + + + + diff --git a/purchase_suggest/wizard/__init__.py b/purchase_suggest/wizard/__init__.py new file mode 100644 index 0000000..103f0b2 --- /dev/null +++ b/purchase_suggest/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- encoding: utf-8 -*- + +from . import purchase_suggest diff --git a/purchase_suggest/wizard/purchase_suggest.py b/purchase_suggest/wizard/purchase_suggest.py new file mode 100644 index 0000000..6d8d429 --- /dev/null +++ b/purchase_suggest/wizard/purchase_suggest.py @@ -0,0 +1,242 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Purchase Suggest module for Odoo +# Copyright (C) 2015 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, fields, api, _ +import openerp.addons.decimal_precision as dp +from openerp.tools import float_compare +from openerp.exceptions import Warning +import logging + +logger = logging.getLogger(__name__) + + +class PurchaseSuggestionGenerate(models.TransientModel): + _name = 'purchase.suggest.generate' + _description = 'Start to generate the purchase suggestions' + + categ_id = fields.Many2one( + 'product.category', string='Product Category') + seller_id = fields.Many2one( + 'res.partner', string='Supplier', + domain=[('supplier', '=', True), ('is_company', '=', True)]) + location_id = fields.Many2one( + 'stock.location', string='Stock Location', required=True, + default=lambda self: self.env.ref('stock.stock_location_stock')) + + @api.model + def _prepare_suggest_line(self, orderpoint): + porderline_id = False + if orderpoint.product_id.seller_id: + porderlines = self.env['purchase.order.line'].search([ + ('state', 'not in', ('draft', 'cancel')), + ('product_id', '=', orderpoint.product_id.id)], + order='id desc', limit=1) + # I cannot filter on 'date_order' because it is not a stored field + porderline_id = porderlines and porderlines[0].id or False + sline = { + 'product_id': orderpoint.product_id.id, + 'seller_id': orderpoint.product_id.seller_id.id or False, + 'qty_available': orderpoint.product_id.qty_available, + 'incoming_qty': orderpoint.product_id.incoming_qty, + 'outgoing_qty': orderpoint.product_id.outgoing_qty, + 'orderpoint_id': orderpoint.id, + 'last_po_line_id': porderline_id, + } + return sline + + @api.multi + def run(self): + self.ensure_one() + pso = self.env['purchase.suggest'] + polo = self.env['purchase.order.line'] + swoo = self.env['stock.warehouse.orderpoint'] + poo = self.env['procurement.order'] + op_domain = [ + ('suggest', '=', True), + ('company_id', '=', self.env.user.company_id.id), + ('location_id', 'child_of', self.location_id.id), + ] + if self.categ_id or self.seller_id: + product_domain = [] + if self.categ_id: + product_domain.append( + ('categ_id', 'child_of', self.categ_id.id)) + if self.seller_id: + product_domain.append( + ('seller_id', '=', self.seller_id.id)) + products = self.env['product.product'].search(product_domain) + op_domain.append(('product_id', 'in', products.ids)) + ops = swoo.search(op_domain) + p_suggest_lines = [] + lines = {} # key = product_id ; value = {'min_qty', ...} + for op in ops: + # TODO : take into account the draft PO lines + virtual_qty = poo._product_virtual_get(op) + draft_po_qty = 0 + cur_qty = virtual_qty + draft_po_qty + logger.debug( + 'Product: %s Virtual qty = %s Draft PO qty = %s ' + 'Min. qty = %s', + op.product_id.name, virtual_qty, draft_po_qty, + op.product_min_qty) + if float_compare( + cur_qty, op.product_min_qty, + precision_rounding=op.product_uom.rounding) < 0: + if op.product_id.id in lines: + raise Warning( + _("There are 2 orderpoints (%s and %s) for the same " + "product on stock location %s or its " + "children.") % ( + lines[op.product_id.id]['orderpoint'].name, + op.name, + self.location_id.complete_name)) + p_suggest_lines.append(self._prepare_suggest_line(op)) + logger.debug( + 'Create a procurement suggestion for %s', + op.product_id.name) + p_suggest_lines_sorted = sorted( + p_suggest_lines, key=lambda to_sort: to_sort['seller_id']) + if p_suggest_lines_sorted: + p_suggest_ids = [] + for p_suggest_line in p_suggest_lines_sorted: + p_suggest = pso.create(p_suggest_line) + p_suggest_ids.append(p_suggest.id) + action = self.env['ir.actions.act_window'].for_xml_id( + 'purchase_suggest', 'purchase_suggest_action') + action.update({ + 'target': 'current', + 'domain': [('id', 'in', p_suggest_ids)], + }) + return action + else: + raise Warning(_( + "The virtual stock for all related products is above the " + "minimum stock level.")) + + +class PurchaseSuggest(models.TransientModel): + _name = 'purchase.suggest' + _description = 'Purchase Suggestions' + _rec_name = 'product_id' + + product_id = fields.Many2one( + 'product.product', string='Product', required=True, readonly=True) + seller_id = fields.Many2one( + 'res.partner', string='Supplier', readonly=True) + qty_available = fields.Float( + string='Quantity On Hand', readonly=True, + digits=dp.get_precision('Product Unit of Measure')) + incoming_qty = fields.Float( + string='Incoming Quantity', readonly=True, + digits=dp.get_precision('Product Unit of Measure')) + outgoing_qty = fields.Float( + string='Outgoing Quantity', readonly=True, + digits=dp.get_precision('Product Unit of Measure')) + draft_po_qty = fields.Float( + string='Draft PO Quantity', readonly=True, + digits=dp.get_precision('Product Unit of Measure')) + last_po_line_id = fields.Many2one( + 'purchase.order.line', string='Last Purchase Order Line', + readonly=True) + last_po_date = fields.Datetime( + related='last_po_line_id.order_id.date_order', + string='Date of the last PO', readonly=True) + last_po_qty = fields.Float( + related='last_po_line_id.product_qty', readonly=True, + string='Quantity of the Last Order') + orderpoint_id = fields.Many2one( + 'stock.warehouse.orderpoint', string='Re-ordering Rule', + readonly=True) + min_qty = fields.Float( + string="Min Quantity", readonly=True, + related='orderpoint_id.product_min_qty', + digits=dp.get_precision('Product Unit of Measure')) + qty_to_order = fields.Float( + string='Quantity to Order', + digits=dp.get_precision('Product Unit of Measure')) + + +class PurchaseSuggestPoCreate(models.TransientModel): + _name = 'purchase.suggest.po.create' + _description = 'PurchaseSuggestPoCreate' + + @api.model + def _prepare_purchase_order_vals(self, partner, po_lines): + polo = self.pool['purchase.order.line'] + ponull = self.env['purchase.order'].browse(False) + po_vals = {'partner_id': partner.id} + partner_change_dict = ponull.onchange_partner_id(partner.id) + po_vals.update(partner_change_dict['value']) + picking_type_id = self.env['purchase.order']._get_picking_in() + picking_type_dict = ponull.onchange_picking_type_id(picking_type_id) + po_vals.update(picking_type_dict['value']) + order_lines = [] + for product, qty_to_order in po_lines: + product_change_res = polo.onchange_product_id( + self._cr, self._uid, [], + partner.property_product_pricelist_purchase.id, + product.id, qty_to_order, False, partner.id, + fiscal_position_id=partner.property_account_position.id, + context=self.env.context) + product_change_vals = product_change_res['value'] + taxes_id_vals = [] + if product_change_vals.get('taxes_id'): + for tax_id in product_change_vals['taxes_id']: + taxes_id_vals.append((4, tax_id)) + product_change_vals['taxes_id'] = taxes_id_vals + order_lines.append( + [0, 0, dict(product_change_vals, product_id=product.id)]) + po_vals['order_line'] = order_lines + return po_vals + + @api.multi + def create_po(self): + self.ensure_one() + # group by supplier + po_to_create = {} # key = seller_id, value = [(product, qty)] + psuggest_ids = self.env.context.get('active_ids') + for line in self.env['purchase.suggest'].browse(psuggest_ids): + if not line.qty_to_order: + continue + if line.seller_id in po_to_create: + po_to_create[line.seller_id].append( + (line.product_id, line.qty_to_order)) + else: + po_to_create[line.seller_id] = [ + (line.product_id, line.qty_to_order)] + new_po_ids = [] + for seller, po_lines in po_to_create.iteritems(): + po_vals = self._prepare_purchase_order_vals( + seller, po_lines) + new_po = self.env['purchase.order'].create(po_vals) + new_po_ids.append(new_po.id) + + if not new_po_ids: + raise Warning(_('No purchase orders created')) + action = self.env['ir.actions.act_window'].for_xml_id( + 'purchase', 'purchase_rfq') + action.update({ + 'nodestroy': False, + 'target': 'current', + 'domain': [('id', 'in', new_po_ids)], + }) + return action diff --git a/purchase_suggest/wizard/purchase_suggest_view.xml b/purchase_suggest/wizard/purchase_suggest_view.xml new file mode 100644 index 0000000..843f44e --- /dev/null +++ b/purchase_suggest/wizard/purchase_suggest_view.xml @@ -0,0 +1,110 @@ + + + + + + + + + purchase_suggest_generate.form + purchase.suggest.generate + +
+ + + + + +
+
+
+
+
+ + + Purchase Suggestions + purchase.suggest.generate + form + new + + + + + + purchase_suggest.tree + purchase.suggest + + + + + + + + + + + + + + + + + + purchase_suggest.search + purchase.suggest + + + + + + + + + + + + + + Purchase Suggestions + purchase.suggest + tree + new + + + + purchase_suggest_po_create.form + purchase.suggest.po.create + +
+

+ This wizard will create Purchase Orders. +

+
+
+
+
+
+ + + + +
+