From ff110b0ed8f9e50e5e523bbad696e40a8a5555e3 Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Fri, 29 Apr 2016 08:21:20 +0200 Subject: [PATCH] [IMP] purchase_requisition: blanket orders and usability cleaning Improved usability & code cleaning: - Support for blanket orders, in addition of purchase tenders ("Purchase Agreements") - Create a RfQ in one click, no wizard anymore - Control of quantities really purchased - Better control of side-effects thanks to purchase requisition types - JS code removed (not necessary) - Workflow removed Closes #11666 --- addons/purchase/res_config.py | 6 - addons/purchase/res_config_view.xml | 2 - .../purchase/security/purchase_security.xml | 5 - addons/purchase_requisition/__init__.py | 1 - addons/purchase_requisition/__openerp__.py | 20 +- .../purchase_requisition.py | 460 +++++++----------- .../purchase_requisition_data.xml | 20 +- .../purchase_requisition_demo.xml | 1 - .../purchase_requisition_sequence.xml | 12 - .../purchase_requisition_view.xml | 222 +++++---- .../purchase_requisition_workflow.xml | 69 --- .../security/ir.model.access.csv | 2 + .../static/src/js/web_addons.js | 38 -- .../static/src/xml/purchase_requisition.xml | 10 - .../test/cancel_purchase_requisition.yml | 4 +- .../test/purchase_requisition.yml | 57 --- .../test/purchase_requisition_demo.yml | 25 +- .../views/purchase_requisition.xml | 10 - .../views/report_purchaserequisition.xml | 3 +- .../purchase_requisition/wizard/__init__.py | 5 - .../wizard/bid_line_qty.py | 18 - .../wizard/bid_line_qty_view.xml | 30 -- .../wizard/purchase_requisition_partner.py | 31 -- .../purchase_requisition_partner_view.xml | 31 -- 24 files changed, 351 insertions(+), 731 deletions(-) delete mode 100644 addons/purchase_requisition/purchase_requisition_sequence.xml delete mode 100644 addons/purchase_requisition/purchase_requisition_workflow.xml delete mode 100644 addons/purchase_requisition/static/src/js/web_addons.js delete mode 100644 addons/purchase_requisition/static/src/xml/purchase_requisition.xml delete mode 100644 addons/purchase_requisition/views/purchase_requisition.xml delete mode 100644 addons/purchase_requisition/wizard/__init__.py delete mode 100644 addons/purchase_requisition/wizard/bid_line_qty.py delete mode 100644 addons/purchase_requisition/wizard/bid_line_qty_view.xml delete mode 100644 addons/purchase_requisition/wizard/purchase_requisition_partner.py delete mode 100644 addons/purchase_requisition/wizard/purchase_requisition_partner_view.xml diff --git a/addons/purchase/res_config.py b/addons/purchase/res_config.py index fb959e1d2fe..b65a4acbd63 100644 --- a/addons/purchase/res_config.py +++ b/addons/purchase/res_config.py @@ -34,12 +34,6 @@ class purchase_config_settings(osv.osv_memory): help="Calls for tenders are used when you want to generate requests for quotations to several vendors for a given set of products.\n" "You can configure per product if you directly do a Request for Quotation " "to one vendor or if you want a Call for Tenders to compare offers from several vendors."), - 'group_advance_purchase_requisition': fields.selection([ - (0, 'Simple call for tender (only choose from one RFQ)'), - (1, 'Advanced call for tender (choose products from different RFQ)') - ], "Advanced Calls for Tenders", - implied_group='purchase.group_advance_bidding', - help="""In the process of a public tendering, you can compare the tender lines and choose for each requested product which quantity you will buy from each bid."""), 'group_warning': fields.selection([ (0, 'All the products and the customers can be used in purchase orders'), (1, 'An informative or blocking warning can be set on a product or a customer') diff --git a/addons/purchase/res_config_view.xml b/addons/purchase/res_config_view.xml index d29aa70a348..84bd592dc43 100644 --- a/addons/purchase/res_config_view.xml +++ b/addons/purchase/res_config_view.xml @@ -15,8 +15,6 @@ - diff --git a/addons/purchase/security/purchase_security.xml b/addons/purchase/security/purchase_security.xml index 52d95abf237..3f7f4d750d0 100644 --- a/addons/purchase/security/purchase_security.xml +++ b/addons/purchase/security/purchase_security.xml @@ -25,11 +25,6 @@ - - Advance bidding process - - - Manage Vendor Price diff --git a/addons/purchase_requisition/__init__.py b/addons/purchase_requisition/__init__.py index 77f74ea0e52..5dc71cd70fe 100644 --- a/addons/purchase_requisition/__init__.py +++ b/addons/purchase_requisition/__init__.py @@ -1,4 +1,3 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import purchase_requisition -import wizard diff --git a/addons/purchase_requisition/__openerp__.py b/addons/purchase_requisition/__openerp__.py index 3ba618c1ac1..90f870c0399 100644 --- a/addons/purchase_requisition/__openerp__.py +++ b/addons/purchase_requisition/__openerp__.py @@ -14,16 +14,13 @@ keep track and order all your purchase orders. """, 'depends' : ['purchase'], 'demo': ['purchase_requisition_demo.xml'], - 'data': ['views/purchase_requisition.xml', - 'security/purchase_tender.xml', - 'wizard/purchase_requisition_partner_view.xml', - 'wizard/bid_line_qty_view.xml', - 'purchase_requisition_data.xml', - 'purchase_requisition_view.xml', - 'purchase_requisition_report.xml', - 'purchase_requisition_workflow.xml', - 'security/ir.model.access.csv','purchase_requisition_sequence.xml', - 'views/report_purchaserequisition.xml', + 'data': [ + 'security/purchase_tender.xml', + 'purchase_requisition_data.xml', + 'purchase_requisition_view.xml', + 'purchase_requisition_report.xml', + 'security/ir.model.access.csv', + 'views/report_purchaserequisition.xml', ], 'auto_install': False, 'test': [ @@ -32,8 +29,5 @@ keep track and order all your purchase orders. 'test/cancel_purchase_requisition.yml', 'test/purchase_requisition.yml', ], - 'qweb': [ - 'static/src/xml/purchase_requisition.xml', - ], 'installable': True, } diff --git a/addons/purchase_requisition/purchase_requisition.py b/addons/purchase_requisition/purchase_requisition.py index f4f8b013c1f..f9731f9070c 100644 --- a/addons/purchase_requisition/purchase_requisition.py +++ b/addons/purchase_requisition/purchase_requisition.py @@ -3,322 +3,153 @@ from datetime import datetime from openerp.osv import fields, osv -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools.translate import _ import openerp.addons.decimal_precision as dp from openerp.exceptions import UserError +class purchase_requisition_type(osv.osv): + _name = "purchase.requisition.type" + _description = "Purchase Agreement Type" + _order = "sequence" + _columns = { + 'name': fields.char('Agreement Type', required=True, translate=True), + 'sequence': fields.integer('Sequence'), + 'exclusive': fields.selection([ + ('exclusive', 'Select only one RFQ (exclusive)'), ('multiple', 'Select multiple RFQ')],\ + 'Agreement Selection Type', required=True,\ + help="""Select only one RFQ (exclusive): when a purchase order is confirmed, cancel the remaining purchase order.\n + Select multiple RFQ: allows multiple purchase orders. On confirmation of a purchase order it does not cancel the remaining orders"""), + 'quantity_copy': fields.selection([ + ('copy', 'Use quantities of agreement'), ('none', 'Set quantities manually')],\ + 'Quantities', required=True), + 'line_copy': fields.selection([ + ('copy', 'Use lines of agreement'), ('none', 'Do not create RfQ lines automatically')],\ + 'Lines', required=True), + } + _defaults = { + 'exclusive': 'multiple', + 'quantity_copy': 'none', + 'line_copy': 'copy', + 'sequence': 1 + } + class purchase_requisition(osv.osv): _name = "purchase.requisition" _description = "Purchase Requisition" _inherit = ['mail.thread', 'ir.needaction_mixin'] + _order = "id desc" - def _get_po_line(self, cr, uid, ids, field_names, arg=None, context=None): - result = dict((res_id, []) for res_id in ids) - for element in self.browse(cr, uid, ids, context=context): - for po in element.purchase_ids: - result[element.id] += [po_line.id for po_line in po.order_line] + def _compute_orders_number(self, cr, uid, ids, fieldnames, args, context=None): + result = dict.fromkeys(ids, 0) + for order in self.browse(cr, uid, ids, context=context): + result[order.id] = len(order.purchase_ids) return result _columns = { - 'name': fields.char('Call for Tenders Reference', required=True, copy=False), + 'name': fields.char('Agreement Reference', required=True, copy=False), 'origin': fields.char('Source Document'), - 'ordering_date': fields.date('Scheduled Ordering Date'), - 'date_end': fields.datetime('Tender Closing Deadline'), - 'schedule_date': fields.date('Scheduled Date', select=True, help="The expected and scheduled delivery date where all the products are received"), + 'order_count': fields.function(_compute_orders_number, 'Number of Orders', type='integer'), + 'vendor_id': fields.many2one('res.partner', string="Vendor"), + 'type_id': fields.many2one('purchase.requisition.type', string="Agreement Type", required=True), + 'ordering_date': fields.date('Ordering Date'), + 'date_end': fields.datetime('Agreement Deadline'), + 'schedule_date': fields.date('Delivery Date', select=True, help="The expected and scheduled delivery date where all the products are received"), 'user_id': fields.many2one('res.users', 'Responsible'), - 'exclusive': fields.selection([('exclusive', 'Select only one RFQ (exclusive)'), ('multiple', 'Select multiple RFQ')], 'Tender Selection Type', required=True, help="Select only one RFQ (exclusive): On the confirmation of a purchase order, it cancels the remaining purchase order.\nSelect multiple RFQ: It allows to have multiple purchase orders.On confirmation of a purchase order it does not cancel the remaining orders"""), 'description': fields.text('Description'), 'company_id': fields.many2one('res.company', 'Company', required=True), 'purchase_ids': fields.one2many('purchase.order', 'requisition_id', 'Purchase Orders', states={'done': [('readonly', True)]}), - 'po_line_ids': fields.function(_get_po_line, method=True, type='one2many', relation='purchase.order.line', string='Products by vendor'), 'line_ids': fields.one2many('purchase.requisition.line', 'requisition_id', 'Products to Purchase', states={'done': [('readonly', True)]}, copy=True), 'procurement_id': fields.many2one('procurement.order', 'Procurement', ondelete='set null', copy=False), 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'), 'state': fields.selection([('draft', 'Draft'), ('in_progress', 'Confirmed'), - ('open', 'Bid Selection'), ('done', 'PO Created'), + ('open', 'Bid Selection'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', track_visibility='onchange', required=True, copy=False), - 'multiple_rfq_per_supplier': fields.boolean('Multiple RFQ per vendor'), 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'), 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', required=True), + } def _get_picking_in(self, cr, uid, context=None): obj_data = self.pool.get('ir.model.data') return obj_data.get_object_reference(cr, uid, 'stock', 'picking_type_in')[1] + def _get_type_id(self, cr, uid, context=None): + types = self.pool.get('purchase.requisition.type').search(cr, uid, [], context=context, limit=1) + return types[0] if types else False + _defaults = { 'state': 'draft', - 'exclusive': 'multiple', 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.requisition', context=c), 'user_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).id, 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(cr, uid, 'purchase.order.requisition'), + 'type_id': _get_type_id, 'picking_type_id': _get_picking_in, } - def tender_cancel(self, cr, uid, ids, context=None): + def action_cancel(self, cr, uid, ids, context=None): purchase_order_obj = self.pool.get('purchase.order') # try to set all associated quotations to cancel state - for tender in self.browse(cr, uid, ids, context=context): - for purchase_order in tender.purchase_ids: - purchase_order_obj.button_cancel(cr, uid, [purchase_order.id], context=context) - purchase_order_obj.message_post(cr, uid, [purchase_order.id], body=_('Cancelled by the tender associated to this quotation.'), context=context) - return self.write(cr, uid, ids, {'state': 'cancel'}) + for requisition in self.browse(cr, uid, ids, context=context): + for po in requisition.purchase_ids: + purchase_order_obj.button_cancel(cr, uid, [po.id], context=context) + purchase_order_obj.message_post(cr, uid, [po.id], body=_('Cancelled by the agreement associated to this quotation.'), context=context) + return self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - def tender_in_progress(self, cr, uid, ids, context=None): + def action_in_progress(self, cr, uid, ids, context=None): if not all(obj.line_ids for obj in self.pool['purchase.requisition'].browse(cr, uid, ids, context=context)): - raise UserError(_('You can not confirm call because there is no product line.')) + raise UserError(_('You cannot confirm call because there is no product line.')) return self.write(cr, uid, ids, {'state': 'in_progress'}, context=context) - def tender_open(self, cr, uid, ids, context=None): + def action_open(self, cr, uid, ids, context=None): return self.write(cr, uid, ids, {'state': 'open'}, context=context) - def tender_reset(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'draft'}) - for p_id in ids: - # Deleting the existing instance of workflow for PO - self.delete_workflow(cr, uid, [p_id]) - self.create_workflow(cr, uid, [p_id]) - return True + def action_draft(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state': 'draft'}, context=context) - def tender_done(self, cr, uid, ids, context=None): + def action_done(self, cr, uid, ids, context=None): + """ + Generate all purchase order based on selected lines, should only be called on one agreement at a time + """ + requisitions = self.browse(cr, uid, ids, context=context) + if requisitions.mapped('purchase_ids') and any(r.state in ['draft', 'sent', 'to approve'] for r in requisitions.mapped('purchase_ids')): + raise UserError(_('You have to cancel or validate every RfQ before closing the purchase requisition.')) return self.write(cr, uid, ids, {'state': 'done'}, context=context) - def open_product_line(self, cr, uid, ids, context=None): - """ This opens product line view to view all lines from the different quotations, groupby default by product and partner to show comparaison - between vendor price - @return: the product line tree view - """ - if context is None: - context = {} - res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase_requisition', 'purchase_line_tree', context=context) - res['context'] = context - po_lines = self.browse(cr, uid, ids, context=context)[0].po_line_ids - res['context'] = { - 'search_default_groupby_product': True, - 'search_default_hide_cancelled': True, - 'tender_id': ids[0], - } - res['domain'] = [('id', 'in', [line.id for line in po_lines])] - return res - - def open_rfq(self, cr, uid, ids, context=None): - """ This opens rfq view to view all quotations associated to the call for tenders - @return: the RFQ tree view - """ - if context is None: - context = {} - res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase', 'purchase_rfq', context=context) - res['context'] = context - po_ids = [po.id for po in self.browse(cr, uid, ids, context=context)[0].purchase_ids] - res['domain'] = [('id', 'in', po_ids)] - return res - - def _prepare_purchase_order(self, cr, uid, requisition, supplier, context=None): - return { - 'origin': requisition.name, - 'date_order': requisition.date_end or fields.datetime.now(), - 'partner_id': supplier.id, - 'currency_id': requisition.company_id and requisition.company_id.currency_id.id, - 'company_id': requisition.company_id.id, - 'fiscal_position_id': self.pool.get('account.fiscal.position').get_fiscal_position(cr, uid, supplier.id, context=context), - 'requisition_id': requisition.id, - 'notes': requisition.description, - 'picking_type_id': requisition.picking_type_id.id - } - - def _prepare_purchase_order_line(self, cr, uid, requisition, requisition_line, purchase_id, supplier, context=None): - if context is None: - context = {} - po_obj = self.pool.get('purchase.order') - po_line_obj = self.pool.get('purchase.order.line') - product_uom = self.pool.get('product.uom') - product = requisition_line.product_id - default_uom_po_id = product.uom_po_id.id - ctx = context.copy() - ctx['tz'] = requisition.user_id.tz - date_order = requisition.ordering_date and fields.date.date_to_datetime(self, cr, uid, requisition.ordering_date, context=ctx) or fields.datetime.now() - qty = product_uom._compute_qty(cr, uid, requisition_line.product_uom_id.id, requisition_line.product_qty, default_uom_po_id) - - taxes = product.supplier_taxes_id - fpos = supplier.property_account_position_id - taxes_id = fpos.map_tax(taxes).ids if fpos else [] - - po = po_obj.browse(cr, uid, [purchase_id], context=context) - seller = requisition_line.product_id._select_seller( - requisition_line.product_id, - partner_id=supplier, - quantity=qty, - date=date_order and date_order[:10], - uom_id=product.uom_po_id) - - price_unit = seller.price 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) - - date_planned = po_line_obj._get_date_planned(cr, uid, seller, po=po, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - - product_lang = requisition_line.product_id.with_context({ - 'lang': supplier.lang, - 'partner_id': supplier.id, - }) - name = product_lang.display_name - if product_lang.description_purchase: - name += '\n' + product_lang.description_purchase - - vals = { - 'name': name, - 'order_id': purchase_id, - 'product_qty': qty, - 'product_id': product.id, - 'product_uom': default_uom_po_id, - 'price_unit': price_unit, - 'date_planned': date_planned, - 'taxes_id': [(6, 0, taxes_id)], - 'account_analytic_id': requisition_line.account_analytic_id.id, - } - - return vals - - def make_purchase_order(self, cr, uid, ids, partner_id, context=None): - """ - Create New RFQ for Vendor - """ - context = dict(context or {}) - assert partner_id, 'Vendor should be specified' - purchase_order = self.pool.get('purchase.order') - purchase_order_line = self.pool.get('purchase.order.line') - res_partner = self.pool.get('res.partner') - supplier = res_partner.browse(cr, uid, partner_id, context=context) - res = {} - for requisition in self.browse(cr, uid, ids, context=context): - if not requisition.multiple_rfq_per_supplier and supplier.id in filter(lambda x: x, [rfq.state != 'cancel' and rfq.partner_id.id or None for rfq in requisition.purchase_ids]): - raise UserError(_('You have already one %s purchase order for this partner, you must cancel this purchase order to create a new quotation.') % rfq.state) - context.update({'mail_create_nolog': True}) - purchase_id = purchase_order.create(cr, uid, self._prepare_purchase_order(cr, uid, requisition, supplier, context=context), context=context) - purchase_order.message_post(cr, uid, [purchase_id], body=_("RFQ created"), context=context) - res[requisition.id] = purchase_id - for line in requisition.line_ids: - purchase_order_line.create(cr, uid, self._prepare_purchase_order_line(cr, uid, requisition, line, purchase_id, supplier, context=context), context=context) - return res - - def check_valid_quotation(self, cr, uid, quotation, context=None): - """ - Check if a quotation has all his order lines bid in order to confirm it if its the case - return True if all order line have been selected during tendering process, else return False - - args : 'quotation' must be a browse record - """ - for line in quotation.order_line: - if line.product_qty != line.quantity_tendered: - return False - return True - - def _prepare_po_from_tender(self, cr, uid, tender, context=None): - """ Prepare the values to write in the purchase order - created from a tender. - - :param tender: the source tender from which we generate a purchase order - """ - return {'order_line': [], - 'requisition_id': tender.id, - 'origin': tender.name} - - def _prepare_po_line_from_tender(self, cr, uid, tender, line, purchase_id, context=None): - """ Prepare the values to write in the purchase order line - created from a line of the tender. - - :param tender: the source tender from which we generate a purchase order - :param line: the source tender's line from which we generate a line - :param purchase_id: the id of the new purchase - """ - return {'product_qty': line.quantity_tendered, - 'order_id': purchase_id} - - def generate_po(self, cr, uid, ids, context=None): - """ - Generate all purchase order based on selected lines, should only be called on one tender at a time - """ - po = self.pool.get('purchase.order') - poline = self.pool.get('purchase.order.line') - id_per_supplier = {} - for tender in self.browse(cr, uid, ids, context=context): - if tender.state == 'done': - raise UserError(_('You have already generate the purchase order(s).')) - - confirm = False - #check that we have at least confirm one line - for po_line in tender.po_line_ids: - if po_line.quantity_tendered > 0: - confirm = True - break - if not confirm: - raise UserError(_('You have no line selected for buying.')) - - #check for complete RFQ - for quotation in tender.purchase_ids: - if (self.check_valid_quotation(cr, uid, quotation, context=context)): - #Set PO state to confirm - po.button_confirm(cr, uid, [quotation.id], context=context) - - #get other confirmed lines per supplier - for po_line in tender.po_line_ids: - #only take into account confirmed line that does not belong to already confirmed purchase order - if po_line.quantity_tendered > 0 and po_line.order_id.state in ['draft', 'sent', 'to approve']: - if id_per_supplier.get(po_line.partner_id.id): - id_per_supplier[po_line.partner_id.id].append(po_line) - else: - id_per_supplier[po_line.partner_id.id] = [po_line] - - #generate po based on supplier and cancel all previous RFQ - ctx = dict(context or {}, force_requisition_id=True) - for supplier, product_line in id_per_supplier.items(): - #copy a quotation for this supplier and change order_line then validate it - quotation_id = po.search(cr, uid, [('requisition_id', '=', tender.id), ('partner_id', '=', supplier)], limit=1)[0] - vals = self._prepare_po_from_tender(cr, uid, tender, context=context) - new_po = po.copy(cr, uid, quotation_id, default=vals, context=context) - #duplicate po_line and change product_qty if needed and associate them to newly created PO - for line in product_line: - vals = self._prepare_po_line_from_tender(cr, uid, tender, line, new_po, context=context) - poline.copy(cr, uid, line.id, default=vals, context=context) - #use workflow to set new PO state to confirm - po.button_confirm(cr, uid, [new_po], context=context) - - #cancel other orders - self.cancel_unconfirmed_quotations(cr, uid, tender, context=context) - - #set tender to state done - self.signal_workflow(cr, uid, [tender.id], 'done') - return True - - def cancel_unconfirmed_quotations(self, cr, uid, tender, context=None): - #cancel other orders - po = self.pool.get('purchase.order') - for quotation in tender.purchase_ids: - if quotation.state in ['draft', 'sent', 'to approve']: - self.pool.get('purchase.order').button_cancel(cr, uid, [quotation.id], context=context) - po.message_post(cr, uid, [quotation.id], body=_('Cancelled by the call for tenders associated to this request for quotation.'), context=context) - return True - class purchase_requisition_line(osv.osv): _name = "purchase.requisition.line" _description = "Purchase Requisition Line" _rec_name = 'product_id' + def _compute_ordered_qty(self, cr, uid, ids, fieldnames, args, context=None): + result = dict.fromkeys(ids, 0) + for line in self.browse(cr, uid, ids, context=context): + total = 0.0 + for po in line.requisition_id.purchase_ids.filtered(lambda r: r.state in ['purchase', 'done']): + for po_line in po.order_line.filtered(lambda r: r.product_id == line.product_id): + if po_line.product_uom != line.product_uom_id: + total += self.pool.get('product.uom')._compute_qty_obj(po_line.product_uom, po_line.product_qty, line.product_uom_id) + else: + total += po_line.product_qty + result[line.id] = total + return result + _columns = { - 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok', '=', True)]), + 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok', '=', True)], required=True), 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'), 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')), - 'requisition_id': fields.many2one('purchase.requisition', 'Call for Tenders', ondelete='cascade'), + 'price_unit': fields.float('Unit Price', digits_compute=dp.get_precision('Product Price')), + 'qty_ordered': fields.function(_compute_ordered_qty, string='Ordered Quantities', type='float'), + 'requisition_id': fields.many2one('purchase.requisition', 'Purchase Agreement', ondelete='cascade'), 'company_id': fields.related('requisition_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'), 'schedule_date': fields.date('Scheduled Date'), } - def onchange_product_id(self, cr, uid, ids, product_id, product_uom_id, parent_analytic_account, analytic_account, parent_date, date, context=None): + def _onchange_product_id(self, cr, uid, ids, product_id, product_uom_id, parent_analytic_account, analytic_account, parent_date, date, context=None): """ Changes UoM and name if product_id changes. @param name: Name of the field @param product_id: Changed product_id @@ -338,62 +169,117 @@ class purchase_requisition_line(osv.osv): 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.requisition.line', context=c), } + class purchase_order(osv.osv): _inherit = "purchase.order" _columns = { - 'requisition_id': fields.many2one('purchase.requisition', 'Call for Tenders', copy=False), + 'requisition_id': fields.many2one('purchase.requisition', 'Purchase Agreement', copy=False), } + def _onchange_requisition_id(self, cr, uid, ids, requisition_id, partner_id, context=None): + if not requisition_id: + return {} + + requisition = self.pool.get('purchase.requisition').browse(cr, uid, [requisition_id], context=context) + if partner_id: + partner = self.pool.get('res.partner').browse(cr, uid, [partner_id], context=context) + else: + partner = requisition.vendor_id + payment_term = partner.property_supplier_payment_term_id + currency = partner.property_purchase_currency_id or requisition.company_id.currency_id + + fpos_obj = self.pool.get('account.fiscal.position') + fpos = fpos_obj.get_fiscal_position(cr, uid, partner.id, context=context) + fpos = fpos_obj.browse(cr, uid, [fpos], context=context) + + value = { + 'partner_id': partner.id, + 'fiscal_position_id': fpos.id, + 'payment_term_id': payment_term.id, + 'company_id': requisition.company_id.id, + 'currency_id': currency.id, + 'origin': requisition.name, + 'partner_ref': requisition.name, # to control vendor bill based on agreement reference + 'notes': requisition.description, + 'date_order': requisition.date_end or fields.datetime.now(), + 'picking_type_id': requisition.picking_type_id.id, + } + + if requisition.type_id.line_copy != 'copy': + return {'value': value} + + # Create PO lines if necessary + order_lines = [] + for line in requisition.line_ids: + # Compute name + product_lang = line.product_id.with_context({ + 'lang': partner.lang, + 'partner_id': partner.id, + }) + name = product_lang.display_name + if product_lang.description_purchase: + name += '\n' + product_lang.description_purchase + + # Compute taxes + if fpos: + taxes_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos,\ + line.product_id.supplier_taxes_id.filtered(lambda r: r.company_id == requisition.company_id), context=context) + else: + taxes_ids = line.product_id.supplier_taxes_id.filtered(lambda r: r.company_id == requisition.company_id).ids + + # Compute quantity and price_unit + if requisition.type_id.quantity_copy != 'copy': + product_qty = 0 + price_unit = line.price_unit + elif line.product_uom_id != line.product_id.uom_po_id: + product_uom_obj = self.pool.get('product.uom') + product_qty = product_uom_obj._compute_qty_obj( + cr, uid, line.product_uom_id, line.product_qty, line.product_id.uom_po_id, context=context) + price_unit = product_uom_obj._compute_price( + cr, uid, line.product_uom_id.id, line.price_unit, to_uom_id=line.product_id.uom_po_id.id) + else: + product_qty = line.product_qty + price_unit = line.price_unit + + # Compute price_unit in appropriate currency + if requisition.company_id.currency_id != currency: + price_unit = requisition.company_id.currency_id.compute(price_unit, currency) + + # Create PO line + order_lines.append((0, 0, { + 'name': name, + 'product_id': line.product_id.id, + 'product_uom': line.product_id.uom_po_id.id, + 'product_qty': product_qty, + 'price_unit': price_unit, + 'taxes_id': [(6, 0, taxes_ids)], + 'date_planned': requisition.schedule_date or datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'procurement_ids': [(6, 0, [requisition.procurement_id.id])] if requisition.procurement_id else False, + 'account_analytic_id': line.account_analytic_id and line.account_analytic_id.id or False, + })) + value['order_line'] = order_lines + + return {'value': value} + def button_confirm(self, cr, uid, ids, context=None): res = super(purchase_order, self).button_confirm(cr, uid, ids, context=context) - proc_obj = self.pool.get('procurement.order') for po in self.browse(cr, uid, ids, context=context): - if po.requisition_id and (po.requisition_id.exclusive == 'exclusive'): - for order in po.requisition_id.purchase_ids: - if order.id != po.id: - proc_ids = proc_obj.search(cr, uid, [('purchase_id', '=', order.id)]) - if proc_ids and po.state == 'confirmed': - proc_obj.write(cr, uid, proc_ids, {'purchase_id': po.id}) - order.button_cancel() - po.requisition_id.tender_done(context=context) - for element in po.order_line: - if not element.quantity_tendered: - element.write({'quantity_tendered': element.product_qty}) + if po.requisition_id and (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(context=context) return res -class purchase_order_line(osv.osv): - _inherit = 'purchase.order.line' - - _columns = { - 'quantity_tendered': fields.float('Quantity Tendered', digits_compute=dp.get_precision('Product Unit of Measure'), help="Technical field for not loosing the initial information about the quantity proposed in the tender", oldname='quantity_bid'), - } - - def generate_po(self, cr, uid, tender_id, context=None): - #call generate_po from tender with active_id. Called from js widget - return self.pool.get('purchase.requisition').generate_po(cr, uid, [tender_id], context=context) - - def button_confirm(self, cr, uid, ids, context=None): - for element in self.browse(cr, uid, ids, context=context): - self.write(cr, uid, element.id, {'quantity_tendered': element.product_qty}, context=context) - return True - - def button_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'quantity_tendered': 0}, context=context) - return True - - class product_template(osv.osv): _inherit = 'product.template' - _columns = { 'purchase_requisition': fields.selection( [('rfq', 'Create a draft purchase order'), ('tenders', 'Propose a call for tenders')], string='Procurement'), } - _defaults = { 'purchase_requisition': 'rfq', } diff --git a/addons/purchase_requisition/purchase_requisition_data.xml b/addons/purchase_requisition/purchase_requisition_data.xml index 4ccc45d1edc..39bb2fd5146 100644 --- a/addons/purchase_requisition/purchase_requisition_data.xml +++ b/addons/purchase_requisition/purchase_requisition_data.xml @@ -6,5 +6,23 @@ id="purchase_default_set" model="ir.values" name="set"/> + + + Blanket Order + 3 + none + + + Purchase Tender + 1 + copy + + + + Purchase Requisition Order + purchase.order.requisition + TE + 5 + - \ No newline at end of file + diff --git a/addons/purchase_requisition/purchase_requisition_demo.xml b/addons/purchase_requisition/purchase_requisition_demo.xml index 0d12518c5a0..e09ac689017 100644 --- a/addons/purchase_requisition/purchase_requisition_demo.xml +++ b/addons/purchase_requisition/purchase_requisition_demo.xml @@ -14,7 +14,6 @@ - exclusive diff --git a/addons/purchase_requisition/purchase_requisition_sequence.xml b/addons/purchase_requisition/purchase_requisition_sequence.xml deleted file mode 100644 index 16f3619fbb1..00000000000 --- a/addons/purchase_requisition/purchase_requisition_sequence.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Purchase Requisition Order - purchase.order.requisition - TE - 5 - - - diff --git a/addons/purchase_requisition/purchase_requisition_view.xml b/addons/purchase_requisition/purchase_requisition_view.xml index 1e50f21606d..19ddad4be4b 100644 --- a/addons/purchase_requisition/purchase_requisition_view.xml +++ b/addons/purchase_requisition/purchase_requisition_view.xml @@ -2,14 +2,66 @@ + + + + purchase.requisition.type.tree + purchase.requisition.type + + + + + + + + + + purchase.requisition.type.form + purchase.requisition.type + +
+ + + + + + + + + + + + +
+
+
+ + Purchase Agreement Types + purchase.requisition.type + {} + form + tree,form + + + + + purchase.order.form.inherit purchase.order - - + + + + + @@ -19,31 +71,55 @@ - +
+ + Request for Quotation + ir.actions.act_window + purchase.order + form + form,tree + [('requisition_id','=',active_id)] + {"default_requisition_id":active_id} + + + Request for Quotations + ir.actions.act_window + purchase.order + form + tree,form + [('requisition_id','=',active_id)] + {"default_requisition_id":active_id} + + purchase.requisition.form purchase.requisition -
-
-
-
- - + + + - - - - + + + @@ -69,19 +145,21 @@ - + - + + - + - + + @@ -89,28 +167,8 @@ - - - - - \ No newline at end of file diff --git a/addons/purchase_requisition/test/cancel_purchase_requisition.yml b/addons/purchase_requisition/test/cancel_purchase_requisition.yml index 71111d89e0f..d0ebd43a7ec 100644 --- a/addons/purchase_requisition/test/cancel_purchase_requisition.yml +++ b/addons/purchase_requisition/test/cancel_purchase_requisition.yml @@ -7,7 +7,7 @@ I cancel requisition. - !python {model: purchase.requisition}: | - self.tender_cancel(cr, uid, [ref("requisition1")]) + self.action_cancel(cr, uid, [ref("requisition1")]) - I check requisition after cancelled. - @@ -17,7 +17,7 @@ I reset requisition as "New". - !python {model: purchase.requisition}: | - self.tender_reset(cr, uid, [ref('requisition1')]) + self.action_draft(cr, uid, [ref('requisition1')]) - I duplicate requisition. - diff --git a/addons/purchase_requisition/test/purchase_requisition.yml b/addons/purchase_requisition/test/purchase_requisition.yml index ad8b5914a42..64accb7de5d 100644 --- a/addons/purchase_requisition/test/purchase_requisition.yml +++ b/addons/purchase_requisition/test/purchase_requisition.yml @@ -31,60 +31,3 @@ assert line.product_id.id == procurement.product_id.id, "Product is not correspond." assert line.product_uom_id.id == procurement.product_uom.id, "UOM is not correspond." assert float_compare(line.product_qty, procurement.product_qty, precision_digits=2) == 0, "Quantity is not correspond." -- - I send the purchase order associated to the requisition. -- - !python {model: purchase.requisition}: | - po = self.pool.get('purchase.order') - for element in self.browse(cr, uid, [ref("requisition1")], context=context): - for purchase in element.purchase_ids: - po.write(cr, uid, purchase.id, {'state': 'sent'}, context=context) - -- - Give access rights of Purchase Requisition User to open requisition -- - !context - uid: 'res_users_purchase_requisition_user' -- - I open another requisition and set tender state to choose tendering line. -- - !python {model: purchase.requisition}: | - self.tender_in_progress(cr, uid, [ref("requisition1")], context=context) - self.tender_open(cr, uid, [ref("requisition1")], context=context) -- - Vendor send one RFQ so I create requisition request of that supplier. -- - !python {model: purchase.requisition.partner}: | - context.update({"active_model": "purchase.requisition","active_ids": [ref("requisition1")],"active_id": ref("requisition1")}) -- - !record {model: purchase.requisition.partner, id: requisition_partner_0}: - partner_ids: [base.res_partner_12] -- - !python {model: purchase.requisition.partner}: | - context.update({"mail_create_nolog" : True }) - self.create_order(cr, uid, [ref("requisition_partner_0")], context=context) -- - I check that the RFQ details which created for supplier. -- - !python {model: purchase.order}: | - purchase_requisition = self.pool.get("purchase.requisition") - purchase_ids = self.search(cr, uid, [('requisition_id','=',ref("requisition1"))]) - assert purchase_ids, "RFQ is not created." - for rfq in self.browse(cr, uid, purchase_ids, context=context): - if rfq.partner_id.id == ref('base.res_partner_12'): - break - else: - assert False, "No PO found for res_partner_12." -- - I confirmed RFQ which has best price. -- - !python {model: purchase.order}: | - purchase = self.browse(cr, uid, ref('rfq2'), context=context) - purchase.signal_workflow('purchase_confirm') - -- - I check status of requisition after confirmed best RFQ. -- - !python {model: purchase.requisition}: | - requisition = self.browse(cr, uid, ref('requisition1'), context=context) - requisition.state == 'done', "Requisition should be closed." diff --git a/addons/purchase_requisition/test/purchase_requisition_demo.yml b/addons/purchase_requisition/test/purchase_requisition_demo.yml index 8265e6c72da..e7526895ba6 100644 --- a/addons/purchase_requisition/test/purchase_requisition_demo.yml +++ b/addons/purchase_requisition/test/purchase_requisition_demo.yml @@ -5,11 +5,30 @@ uid: 'res_users_purchase_requisition_user' - In order to test process of the purchase requisition ,I create requisition -- +- !record {model: purchase.requisition, id: requisition1}: - exclusive: exclusive line_ids: - product_id: product.product_product_9 product_qty: 10.0 product_uom_id: product.product_uom_unit - +- + I create another requisition and set tender state to choose tendering line. +- + !record {model: purchase.requisition, id: requisition2}: + line_ids: + - product_id: product.product_product_9 + product_qty: 10.0 + product_uom_id: product.product_uom_unit +- + !python {model: purchase.requisition}: | + self.action_in_progress(cr, uid, [ref("requisition2")], context=context) + self.action_open(cr, uid, [ref("requisition2")], context=context) +- + Vendor send one RFQ so I create a RfQ of that agreement. +- + !record {model: purchase.order, id: po_requisition}: + partner_id: base.res_partner_1 + requisition_id: purchase_requisition.requisition2 +- + !python {model: purchase.order, id: po_requisition}: | + assert len(self.order_line)==1, 'Purchase order should have one line' diff --git a/addons/purchase_requisition/views/purchase_requisition.xml b/addons/purchase_requisition/views/purchase_requisition.xml deleted file mode 100644 index c3619497f89..00000000000 --- a/addons/purchase_requisition/views/purchase_requisition.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/addons/purchase_requisition/views/report_purchaserequisition.xml b/addons/purchase_requisition/views/report_purchaserequisition.xml index 940079b0cdc..1250b305a0e 100644 --- a/addons/purchase_requisition/views/report_purchaserequisition.xml +++ b/addons/purchase_requisition/views/report_purchaserequisition.xml @@ -21,8 +21,7 @@
Selection Type:
- Purchase Requisitions (exclusive) - Multiple Requisitions + Multiple Requisitions
Source:
diff --git a/addons/purchase_requisition/wizard/__init__.py b/addons/purchase_requisition/wizard/__init__.py deleted file mode 100644 index 533c9061d0b..00000000000 --- a/addons/purchase_requisition/wizard/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import purchase_requisition_partner -import bid_line_qty diff --git a/addons/purchase_requisition/wizard/bid_line_qty.py b/addons/purchase_requisition/wizard/bid_line_qty.py deleted file mode 100644 index a564c2ab3df..00000000000 --- a/addons/purchase_requisition/wizard/bid_line_qty.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -import openerp.addons.decimal_precision as dp - -class bid_line_qty(osv.osv_memory): - _name = "bid.line.qty" - _description = "Change Bid line quantity" - _columns = { - 'qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True), - } - - def change_qty(self, cr, uid, ids, context=None): - active_ids = context and context.get('active_ids', []) - data = self.browse(cr, uid, ids, context=context)[0] - self.pool.get('purchase.order.line').write(cr, uid, active_ids, {'quantity_tendered': data.qty}) - return {'type': 'ir.actions.act_window_close'} diff --git a/addons/purchase_requisition/wizard/bid_line_qty_view.xml b/addons/purchase_requisition/wizard/bid_line_qty_view.xml deleted file mode 100644 index 160b44964e1..00000000000 --- a/addons/purchase_requisition/wizard/bid_line_qty_view.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - Quantity to Purchase - bid.line.qty - -
- - - -
-
-
-
-
- - - Bid Line Qty - ir.actions.act_window - bid.line.qty - form - form - {'record_id' : active_id} - new - -
-
diff --git a/addons/purchase_requisition/wizard/purchase_requisition_partner.py b/addons/purchase_requisition/wizard/purchase_requisition_partner.py deleted file mode 100644 index 2fabb1fe31a..00000000000 --- a/addons/purchase_requisition/wizard/purchase_requisition_partner.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError - -class purchase_requisition_partner(osv.osv_memory): - _name = "purchase.requisition.partner" - _description = "Purchase Requisition Partner" - _columns = { - 'partner_ids': fields.many2many('res.partner', 'purchase_requisition_supplier_rel', 'requisition_id', 'partner_id', string='Vendors', required=True, domain=[('supplier', '=', True)]) - } - - def view_init(self, cr, uid, fields_list, context=None): - if context is None: - context = {} - res = super(purchase_requisition_partner, self).view_init(cr, uid, fields_list, context=context) - record_id = context and context.get('active_id', False) or False - tender = self.pool.get('purchase.requisition').browse(cr, uid, record_id, context=context) - if not tender.line_ids: - raise UserError(_('Define product(s) you want to include in the call for tenders.')) - return res - - def create_order(self, cr, uid, ids, context=None): - active_ids = context and context.get('active_ids', []) - purchase_requisition = self.pool.get('purchase.requisition') - for wizard in self.browse(cr, uid, ids, context=context): - for partner_id in wizard.partner_ids: - purchase_requisition.make_purchase_order(cr, uid, active_ids, partner_id.id, context=context) - return {'type': 'ir.actions.act_window_close'} diff --git a/addons/purchase_requisition/wizard/purchase_requisition_partner_view.xml b/addons/purchase_requisition/wizard/purchase_requisition_partner_view.xml deleted file mode 100644 index a01a5f7d15d..00000000000 --- a/addons/purchase_requisition/wizard/purchase_requisition_partner_view.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - Choose Vendor - purchase.requisition.partner - -
- - - -
-
-
-
-
- - - Choose Vendor - ir.actions.act_window - purchase.requisition.partner - form - form - {'record_id' : active_id} - new - - -
-