openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #08081
[Merge] lp:~camptocamp/openerp-humanitarian-ngo/purchase-wkfl-fix-js into lp:openerp-humanitarian-ngo
Nicolas Bessi - Camptocamp has proposed merging lp:~camptocamp/openerp-humanitarian-ngo/purchase-wkfl-fix-js into lp:openerp-humanitarian-ngo.
Requested reviews:
OpenERP for Humanitarian Core Editors (humanitarian-core-editors)
For more details, see:
https://code.launchpad.net/~camptocamp/openerp-humanitarian-ngo/purchase-wkfl-fix-js/+merge/246901
Fix javascript to be resilient to return values
--
Your team OpenERP for Humanitarian Core Editors is requested to review the proposed merge of lp:~camptocamp/openerp-humanitarian-ngo/purchase-wkfl-fix-js into lp:openerp-humanitarian-ngo.
=== added directory 'purchase_extended'
=== added file 'purchase_extended/__init__.py'
--- purchase_extended/__init__.py 1970-01-01 00:00:00 +0000
+++ purchase_extended/__init__.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+from . import model
+from . import wizard
=== added file 'purchase_extended/__openerp__.py'
--- purchase_extended/__openerp__.py 1970-01-01 00:00:00 +0000
+++ purchase_extended/__openerp__.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright 2013 Camptocamp SA
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{"name": "Purchase Extended",
+ "version": "0.1",
+ "author": "Camptocamp",
+ "category": "Purchase Management",
+ "license": "AGPL-3",
+ 'complexity': "normal",
+ "images": [],
+ "description": """
+This module improves the standard Purchase module.
+==================================================
+In standard, RFQs, Bids and PO are all the same object. The purchase workflow
+has been improved with a new 'Draft PO' state to clearly differentiate the
+RFQ->Bid workflow and the PO workflow. A type field has also been added to
+identify if a document is of type 'rfq' or 'purchase'. This is particularly
+usefull for canceled state and for datawarehouse.
+
+The 'Requests for Quotation' menu entry shows only documents of type 'rfq' and
+the new documents are created in state 'Draft RFQ'. Those documents have lines
+with a price, by default, set to 0; it will have to be encoded when the bid is
+received. The state 'Bid Received' has been renamed 'Bid Encoded'. This clearly
+indicates that the price has been filled in. The bid received date will be
+requested when moving to that state.
+
+The 'Purchase Orders' menu entry shows only documents of type 'purchase' and
+the new documents are created in state 'Draft PO'.
+
+The logged messages have been improved to notify users at the state changes and
+with the right naming.
+
+
+In the scope of internation transactions, some fields have been added:
+ - Consignee: the person to whom the shipment is to be delivered
+ - Incoterms Place: the standard incoterms field specifies the incoterms rule
+ that applies. This field allows to name the place where the goods will be
+ available
+
+TODO: describe onchange warehouse
+""",
+ "depends": ["purchase",
+ ],
+ "demo": [],
+ "data": ["view/purchase_order.xml",
+ "view/purchase_cancel.xml",
+ "data/purchase_order.xml",
+ "data/purchase.cancelreason.yml",
+ "workflow/purchase_order.xml",
+ "wizard/modal.xml",
+ "wizard/action_cancel_reason.xml",
+ "security/ir.model.access.csv",
+ ],
+ "auto_install": False,
+ "test": ["test/process/rfq2order.yml",
+ "test/process/bid2order.yml",
+ "test/process/po2order.yml",
+ "test/process/rfq2cancel.yml",
+ ],
+ "installable": True,
+ "certificate": "",
+ }
=== added directory 'purchase_extended/data'
=== added file 'purchase_extended/data/purchase.cancelreason.yml'
--- purchase_extended/data/purchase.cancelreason.yml 1970-01-01 00:00:00 +0000
+++ purchase_extended/data/purchase.cancelreason.yml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,30 @@
+-
+ !context {noupdate: True}
+-
+ Creating the cancel reasons
+-
+ !record {model: purchase.cancelreason, id: purchase_cancelreason_rfq_canceled}:
+ name: RFQ canceled
+ type: rfq
+ nounlink: 1
+-
+ !record {model: purchase.cancelreason, id: purchase_cancelreason_bid_regretfromsupplier}:
+ name: Regret from supplier
+ type: rfq
+-
+ !record {model: purchase.cancelreason, id: purchase_cancelreason_bid_noreply}:
+ name: No reply
+ type: rfq
+-
+ !record {model: purchase.cancelreason, id: purchase_cancelreason_bid_latereply}:
+ name: Late reply
+ type: rfq
+-
+ !record {model: purchase.cancelreason, id: purchase_cancelreason_bid_bidnotselected}:
+ name: Bid not selected
+ type: rfq
+-
+ !record {model: purchase.cancelreason, id: purchase_cancelreason_purchase_canceled}:
+ name: PO canceled
+ type: purchase
+ nounlink: 1
=== added file 'purchase_extended/data/purchase_order.xml'
--- purchase_extended/data/purchase_order.xml 1970-01-01 00:00:00 +0000
+++ purchase_extended/data/purchase_order.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+<data>
+ <record id="purchase.mt_rfq_confirmed" model="mail.message.subtype">
+ <field name="name">Purchase Order confirmed</field>
+ <field name="default" eval="False"/>
+ <field name="res_model">purchase.order</field>
+ </record>
+</data>
+</openerp>
=== added directory 'purchase_extended/model'
=== added file 'purchase_extended/model/__init__.py'
--- purchase_extended/model/__init__.py 1970-01-01 00:00:00 +0000
+++ purchase_extended/model/__init__.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import purchase_order
+import purchase_cancel
=== added file 'purchase_extended/model/purchase_cancel.py'
--- purchase_extended/model/purchase_cancel.py 1970-01-01 00:00:00 +0000
+++ purchase_extended/model/purchase_cancel.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import fields, orm
+from openerp.tools.translate import _
+
+
+class purchase_cancel(orm.Model):
+ _name = "purchase.cancelreason"
+ _columns = {
+ 'name': fields.char('Reason', size=64, required=True, translate=True),
+ 'type': fields.selection([('rfq', 'RFQ/Bid'), ('purchase', 'Purchase Order')], 'Type', required=True),
+ 'nounlink': fields.boolean('No unlink'),
+ }
+
+ def unlink(self, cr, uid, ids, context=None):
+ """ Prevent to unlink records that are used in the code
+ """
+ unlink_ids = []
+ for value in self.read(cr, uid, ids, ['nounlink'], context=context):
+ if not value['nounlink']:
+ unlink_ids.append(value['id'])
+ if unlink_ids:
+ return super(purchase_cancel, self).unlink(cr, uid, unlink_ids, context=context)
+ return True
=== added file 'purchase_extended/model/purchase_order.py'
--- purchase_extended/model/purchase_order.py 1970-01-01 00:00:00 +0000
+++ purchase_extended/model/purchase_order.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,264 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import fields, orm
+from openerp import netsvc
+from openerp.tools.translate import _
+from openerp import SUPERUSER_ID
+
+
+class PurchaseOrder(orm.Model):
+ _inherit = "purchase.order"
+
+ STATE_SELECTION = [
+ ('draft', 'Draft RFQ'),
+ ('sent', 'RFQ Sent'),
+ ('draftbid', 'Draft Bid'), # added
+ ('bid', 'Bid Encoded'), # Bid Received renamed into Bid Encoded
+ ('bid_selected', 'Bid selected'), # added
+ ('draftpo', 'Draft PO'), # added
+ ('confirmed', 'Waiting Approval'),
+ ('approved', 'Purchase Confirmed'),
+ ('except_picking', 'Shipping Exception'),
+ ('except_invoice', 'Invoice Exception'),
+ ('done', 'Done'),
+ ('cancel', 'Canceled')
+ ]
+ TYPE_SELECTION = [
+ ('rfq', 'Request for Quotation'),
+ ('bid', 'Bid'),
+ ('purchase', 'Purchase Order')
+ ]
+
+ _columns = {
+ 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, select=True,
+ help="The status of the purchase order or the quotation request. A "
+ "quotation is a purchase order in a 'Draft' status. Then the order "
+ "has to be confirmed by the user, the status switch to 'Confirmed'. "
+ "Then the supplier must confirm the order to change the status to "
+ "'Approved'. When the purchase order is paid and received, the "
+ "status becomes 'Done'. If a cancel action occurs in the invoice or "
+ "in the reception of goods, the status becomes in exception."),
+ 'type': fields.selection(TYPE_SELECTION, 'Type', required=True, readonly=True),
+ 'consignee_id': fields.many2one('res.partner', 'Consignee', help="the person to whom the shipment is to be delivered"),
+ 'incoterm_address': fields.char(
+ 'Incoterms Place',
+ help="Incoterms Place of Delivery. "
+ "International Commercial Terms are a series of "
+ "predefined commercial terms used in "
+ "international transactions."),
+ 'cancel_reason_id': fields.many2one('purchase.cancelreason', 'Reason for Cancellation', readonly=True),
+ }
+ _defaults = {
+ 'state': lambda self, cr, uid, context: 'draftpo' if context.get('draft_po')
+ else 'draftbid' if context.get('draft_bid')
+ else 'draft',
+ 'type': lambda self, cr, uid, context: 'purchase' if context.get('draft_po')
+ else 'bid' if context.get('draft_bid')
+ else 'rfq',
+ }
+
+ def create(self, cr, uid, vals, context=None):
+ # Document can be created as Draft RFQ or Draft PO. We need to log the right message.
+ if context is None:
+ context = {}
+ description = self._description
+ if context.get('draft_bid'):
+ self._description = 'Draft Bid'
+ elif not context.get('draft_po'):
+ self._description = 'Request for Quotation'
+ id = super(PurchaseOrder, self).create(cr, uid, vals, context=context)
+ self._description = description
+ if context.get('draft_bid'):
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', id, 'draft_bid', cr)
+ if context.get('draft_po'):
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', id, 'draft_po', cr)
+ return id
+
+ def copy(self, cr, uid, id, default=None, context=None):
+ newid = super(PurchaseOrder, self).copy(cr, uid, id, default=default, context=context)
+ po = self.read(cr, SUPERUSER_ID, newid, ['type', 'order_line'], context=context, load='_classic_write')
+ if po['type'] == 'rfq' and po['order_line']:
+ self.pool.get('purchase.order.line').write(cr, SUPERUSER_ID, po['order_line'], {'price_unit': 0}, context=context)
+ return newid
+
+ def wkf_draft_po(self, cr, uid, ids, context=None):
+ self.message_post(cr, uid, ids, body=_("Converted to draft Purchase Order"), subtype="mail.mt_comment", context=context)
+ return self.write(cr, uid, ids, {'state': 'draftpo', 'type': 'purchase'}, context=context)
+
+ def action_cancel(self, cr, uid, ids, context=None):
+ """ Ask a cancel reason
+ """
+ if context is None:
+ context = {}
+ context['action'] = 'action_cancel_ok'
+ view_id = self.pool.get('ir.model.data').get_object_reference(cr, SUPERUSER_ID, 'purchase_extended', 'action_modal_cancel_reason')[1]
+ #TODO: filter based on po type
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'purchase.action_modal_cancelreason',
+ 'view_id': view_id,
+ 'views': [(view_id, 'form')],
+ 'target': 'new',
+ 'context': context,
+ }
+
+ def action_cancel_no_reason(self, cr, uid, ids, context=None):
+ return super(PurchaseOrder, self).action_cancel(cr, uid, ids,
+ context=context)
+
+ def action_cancel_ok(self, cr, uid, ids, context=None):
+ reason_id = self.pool.get('purchase.action_modal_cancelreason').read(cr, uid,
+ context['active_id'], ['reason_id'], context=context,
+ load='_classic_write')['reason_id']
+ self.write(cr, uid, ids, {'cancel_reason_id': reason_id}, context=context)
+ return super(PurchaseOrder, self).action_cancel(cr, uid, ids, context=context)
+
+ def purchase_cancel(self, cr, uid, ids, context=None):
+ """ Ask a cancel reason
+ """
+ if context is None:
+ context = {}
+ ctx = context.copy()
+ ctx['action'] = 'purchase_cancel_ok'
+ for e in ('active_model', 'active_ids', 'active_id'): # those will be set by the web layer unless they are already defined
+ if e in ctx:
+ del ctx[e]
+ view_id = self.pool.get('ir.model.data').get_object_reference(cr, SUPERUSER_ID, 'purchase_extended', 'action_modal_cancel_reason')[1]
+ #TODO: filter based on po type
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'purchase.action_modal_cancelreason',
+ 'view_id': view_id,
+ 'views': [(view_id, 'form')],
+ 'target': 'new',
+ 'context': ctx,
+ }
+
+ def purchase_cancel_ok(self, cr, uid, ids, context=None):
+ reason_id = self.pool.get('purchase.action_modal_cancelreason').read(cr, uid,
+ context['active_id'], ['reason_id'], context=context,
+ load='_classic_write')['reason_id']
+ self.write(cr, uid, ids, {'cancel_reason_id': reason_id}, context=context)
+ for id in ids:
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
+ return {}
+
+ def wkf_action_cancel(self, cr, uid, ids, context=None):
+ for element in self.browse(cr, uid, ids, context=context):
+ if element.state in ('draft', 'sent'):
+ message = _("Request for Quotation")
+ elif element.state == 'bid':
+ message = _("Bid")
+ else:
+ message = self._description
+ message += " " + _("canceled")
+ self.message_post(cr, uid, [element.id], body=message, subtype="mail.mt_comment", context=context)
+ return super(PurchaseOrder, self).wkf_action_cancel(cr, uid, ids, context=context)
+
+ def bid_received(self, cr, uid, ids, context=None):
+ assert len(ids) == 1, 'This action should only be used for a single id at a time'
+ if context is None:
+ context = {}
+ order = self.read(cr, uid, ids[0], ['bid_date'], context=context)
+ ctx = context.copy()
+ ctx.update({
+ 'action': 'bid_received_ok',
+ 'default_datetime': order['bid_date'] or fields.date.context_today(self, cr, uid, context=context),
+ })
+ for e in ('active_model', 'active_ids', 'active_id'): # those will be set by the web layer unless they are already defined
+ if e in ctx:
+ del ctx[e]
+ view_id = self.pool.get('ir.model.data').get_object_reference(cr, SUPERUSER_ID, 'purchase_extended', 'action_modal_bid_date')[1]
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'purchase.action_modal_datetime',
+ 'view_id': view_id,
+ 'views': [(view_id, 'form')],
+ 'target': 'new',
+ 'context': ctx,
+ }
+
+ def bid_received_ok(self, cr, uid, ids, context=None):
+ # TODO: send warning if not all lines have a price
+ value = self.pool.get('purchase.action_modal_datetime').read(cr, uid, context['active_id'], ['datetime'], context=context)['datetime']
+ self.write(cr, uid, ids, {'bid_date': value}, context=context)
+ self.message_post(cr, uid, ids, body=_("Bid received and encoded"), subtype="mail.mt_comment", context=context)
+ for id in ids:
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', id, 'bid_received', cr)
+ return {}
+
+ def wkf_bid_received(self, cr, uid, ids, context=None):
+ return self.write(cr, uid, ids, {'state': 'bid', 'type': 'bid'}, context=context)
+
+ def _has_lines(self, cr, uid, ids, context=None):
+ for rfq in self.browse(cr, uid, ids, context=context):
+ if not rfq.order_line:
+ return False
+ return True
+
+ def wkf_send_rfq(self, cr, uid, ids, context=None):
+ if not self._has_lines(cr, uid, ids, context=context):
+ raise orm.except_orm(_('Error!'), _('You cannot send a Request for Quotation without any product line.'))
+ return super(PurchaseOrder, self).wkf_send_rfq(cr, uid, ids, context=context)
+
+ def print_quotation(self, cr, uid, ids, context=None):
+ if not self._has_lines(cr, uid, ids, context=context):
+ raise orm.except_orm(_('Error!'), _('You cannot print a Request for Quotation without any product line.'))
+ self.message_post(cr, uid, ids, body=_("Request for Quotation printed"), subtype="mail.mt_comment", context=context)
+ return super(PurchaseOrder, self).print_quotation(cr, uid, ids, context=context)
+
+ def onchange_dest_address_id_mod(self, cr, uid, ids, dest_address_id,
+ warehouse_id, context=None):
+ value = self.onchange_dest_address_id(cr, uid, ids, dest_address_id)
+ warehouse_obj = self.pool.get('stock.warehouse')
+ dest_ids = warehouse_obj.search(cr, uid,
+ [('partner_id', '=', dest_address_id)],
+ context=context)
+ if dest_ids:
+ if warehouse_id not in dest_ids:
+ warehouse_id = dest_ids[0]
+ else:
+ warehouse_id = False
+ value['value']['warehouse_id'] = warehouse_id
+ return value
+
+ def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
+ value = super(PurchaseOrder, self).onchange_warehouse_id(cr, uid, ids, warehouse_id)
+ if not warehouse_id:
+ return {}
+ warehouse_obj = self.pool.get('stock.warehouse')
+ dest_id = warehouse_obj.browse(cr, uid, warehouse_id, context=context).partner_id.id
+ value['value']['dest_address_id'] = dest_id
+ return value
+
+ def po_tender_requisition_selected(self, cr, uid, ids, context=None):
+ """Workflow function that write state 'bid selected'"""
+ return self.write(cr, uid, ids, {'state': 'bid_selected'},
+ context=context)
+
+
+class purchase_order_line(orm.Model):
+ _inherit = 'purchase.order.line'
+
+ def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
+ partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
+ name=False, price_unit=False, context=None, state='draftpo', type='purchase', **kwargs):
+ res = super(purchase_order_line, self).onchange_product_id(cr, uid, ids,
+ pricelist_id, product_id, qty, uom_id, partner_id, date_order,
+ fiscal_position_id, date_planned, name, price_unit, context)
+ if state == 'draft' and type == 'rfq':
+ res['value'].update({'price_unit': 0})
+ elif state in ('sent', 'draftbid', 'bid'):
+ if 'price_unit' in res['value']:
+ del res['value']['price_unit']
+ return res
=== added directory 'purchase_extended/security'
=== added file 'purchase_extended/security/ir.model.access.csv'
--- purchase_extended/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
+++ purchase_extended/security/ir.model.access.csv 2015-01-19 14:42:10 +0000
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_purchase_cancelreason_user,access_purchase_cancelreason,model_purchase_cancelreason,purchase.group_purchase_user,1,0,0,0
+access_purchase_cancelreason_manager,access_purchase_cancelreason,model_purchase_cancelreason,purchase.group_purchase_manager,1,1,1,1
=== added directory 'purchase_extended/test'
=== added directory 'purchase_extended/test/process'
=== added file 'purchase_extended/test/process/bid2cancel.yml'
--- purchase_extended/test/process/bid2cancel.yml 1970-01-01 00:00:00 +0000
+++ purchase_extended/test/process/bid2cancel.yml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,79 @@
+-
+ Cancel a new RFQ.
+-
+ Create RFQ
+-
+ !record {model: purchase.order, id: purchase_order_ext_bid2cancel1}:
+ partner_id: base.res_partner_1
+ invoice_method: order
+ date_order: '2013-08-02'
+ bid_validity: '2013-08-15'
+ order_line:
+ - product_id: product.product_product_15
+ product_qty: 15.0
+ date_planned: '2013-08-30'
+ - product_id: product.product_product_25
+ product_qty: 5.0
+ - product_id: product.product_product_27
+ product_qty: 4.0
+-
+ I print the RFQ.
+-
+ !python {model: purchase.order}: |
+ self.print_quotation(cr, uid, [ref("purchase_order_ext_bid2cancel1")])
+-
+ I run the 'Cancel' wizard. I fill the reason.
+-
+ !record {model: purchase.action_modal_cancelreason, id: purchase_order_ext_bid2cancel1_cancelreason}:
+ reason_id: purchase_cancelreason_rfq_canceled
+-
+ I run the 'Cancel' wizard. I confirm the wizard.
+-
+ !python {model: purchase.order}: |
+ self.purchase_cancel_ok(cr, uid, [ref("purchase_order_ext_bid2cancel1")],{'active_id':ref("purchase_order_ext_bid2cancel1_cancelreason")})
+-
+ I check the "Canceled" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_bid2cancel1}:
+ - type == 'rfq'
+ - state == 'cancel'
+ - cancel_reason_id.id == ref("purchase_cancelreason_rfq_canceled")
+-
+-
+ Cancel a new Bid.
+-
+ Create a Bid
+-
+ !record {model: purchase.order, id: purchase_order_ext_bid2cancel2, context: '{"draft_bid": 1}'}:
+ partner_id: base.res_partner_1
+ invoice_method: order
+ date_order: '2013-08-02'
+ bid_validity: '2013-08-15'
+ order_line:
+ - product_id: product.product_product_15
+ product_qty: 15.0
+ date_planned: '2013-08-30'
+ price_unit: 43.35
+ - product_id: product.product_product_25
+ product_qty: 5.0
+ price_unit: 43.35
+ - product_id: product.product_product_27
+ product_qty: 4.0
+ price_unit: 43.35
+-
+ I run the 'Cancel' wizard. I fill the reason.
+-
+ !record {model: purchase.action_modal_cancelreason, id: purchase_order_ext_bid2cancel1_cancelreason}:
+ reason_id: purchase_cancelreason_rfq_canceled
+-
+ I run the 'Cancel' wizard. I confirm the wizard.
+-
+ !python {model: purchase.order}: |
+ self.purchase_cancel_ok(cr, uid, [ref("purchase_order_ext_bid2cancel2")],{'active_id':ref("purchase_order_ext_bid2cancel1_cancelreason")})
+-
+ I check the "Canceled" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_bid2cancel2}:
+ - type == 'rfq'
+ - state == 'cancel'
+ - cancel_reason_id.id == ref("purchase_cancelreason_rfq_canceled")
=== added file 'purchase_extended/test/process/bid2order.yml'
--- purchase_extended/test/process/bid2order.yml 1970-01-01 00:00:00 +0000
+++ purchase_extended/test/process/bid2order.yml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,79 @@
+-
+ Standard flow from a new Bid to a PO.
+-
+ Create Bid
+-
+ !record {model: purchase.order, id: purchase_order_ext_bid2order1}:
+ partner_id: base.res_partner_1
+ invoice_method: order
+ date_order: '2013-08-02'
+ bid_validity: '2013-08-15'
+ order_line:
+ - product_id: product.product_product_15
+ product_qty: 15.0
+ date_planned: '2013-08-30'
+ price_unit: 43.35
+ - product_id: product.product_product_25
+ product_qty: 5.0
+ date_planned: '2013-08-30'
+ price_unit: 63.12
+ - product_id: product.product_product_27
+ product_qty: 4.0
+ date_planned: '2013-08-30'
+ price_unit: 52.53
+-
+ I print the RFQ.
+-
+ !python {model: purchase.order}: |
+ self.print_quotation(cr, uid, [ref("purchase_order_ext_bid2order1")])
+-
+ Type must be 'rfq' and the total untaxed amount of the RFQ must be computed.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_bid2order1, string: The amount of RFQ is not correctly computed}:
+ - type == 'rfq'
+ - state == 'sent'
+ - round(sum([l.price_subtotal for l in order_line]), 2) == round(amount_untaxed, 2)
+-
+ I run the 'Bid encoded' wizard. I fill the date.
+-
+ !record {model: purchase.action_modal_datetime, id: purchase_order_ext_bid2order1_bidencoded}:
+ datetime: '2013-08-10 00:00:00'
+-
+ I run the 'Bid encoded' wizard. I confirm the wizard.
+-
+ !python {model: purchase.order}: |
+ self.bid_received_ok(cr, uid, [ref("purchase_order_ext_bid2order1")],{'active_id':ref("purchase_order_ext_bid2order1_bidencoded")})
+-
+ I check the "Bid Encoded" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_bid2order1}:
+ - type == 'bid'
+ - state == 'bid'
+-
+ I convert to PO
+-
+ !python {model: purchase.order}: |
+ import netsvc
+ purchase_order = self.browse(cr, uid, ref("purchase_order_ext_bid2order1"))
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', purchase_order.id, 'draft_po', cr)
+-
+ I check the "Draft PO" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_bid2order1}:
+ - type == 'purchase'
+ - state == 'draftpo'
+-
+ I confirm the draft PO.
+-
+ !python {model: purchase.order}: |
+ import netsvc
+ purchase_order = self.browse(cr, uid, ref("purchase_order_ext_bid2order1"))
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', purchase_order.id, 'purchase_confirm', cr)
+-
+ I check the "Approved" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_bid2order1}:
+ - type == 'purchase'
+ - state == 'approved'
=== added file 'purchase_extended/test/process/po2order.yml'
--- purchase_extended/test/process/po2order.yml 1970-01-01 00:00:00 +0000
+++ purchase_extended/test/process/po2order.yml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,43 @@
+-
+ Standard flow from a new draft PO to a PO.
+-
+ Create draft PO
+-
+ !record {model: purchase.order, id: purchase_order_ext_po2order1, context: {"draft_po": 1}}:
+ partner_id: base.res_partner_1
+ invoice_method: order
+ date_order: '2013-08-02'
+ order_line:
+ - product_id: product.product_product_15
+ product_qty: 15.0
+ date_planned: '2013-08-30'
+ price_unit: 43.35
+ - product_id: product.product_product_25
+ product_qty: 5.0
+ date_planned: '2013-08-30'
+ price_unit: 63.12
+ - product_id: product.product_product_27
+ product_qty: 4.0
+ date_planned: '2013-08-30'
+ price_unit: 52.53
+-
+ Type must be 'purchase' and the total untaxed amount of the PO must be computed.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_po2order1, string: The amount of RFQ is not correctly computed}:
+ - type == 'purchase'
+ - state == 'draftpo'
+ - round(sum([l.price_subtotal for l in order_line]), 2) == round(amount_untaxed, 2)
+-
+ I confirm the draft PO.
+-
+ !python {model: purchase.order}: |
+ import netsvc
+ purchase_order = self.browse(cr, uid, ref("purchase_order_ext_po2order1"))
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', purchase_order.id, 'purchase_confirm', cr)
+-
+ I check the "Approved" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_po2order1}:
+ - type == 'purchase'
+ - state == 'approved'
=== added file 'purchase_extended/test/process/rfq2cancel.yml'
--- purchase_extended/test/process/rfq2cancel.yml 1970-01-01 00:00:00 +0000
+++ purchase_extended/test/process/rfq2cancel.yml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,35 @@
+-
+ Cancel a new RFQ.
+-
+ Create RFQ
+-
+ !record {model: purchase.order, id: purchase_order_ext_rfq2cancel1}:
+ partner_id: base.res_partner_1
+ invoice_method: order
+ date_order: '2013-08-02'
+ bid_validity: '2013-08-15'
+ order_line:
+ - product_id: product.product_product_15
+ product_qty: 15.0
+ date_planned: '2013-08-30'
+ - product_id: product.product_product_25
+ product_qty: 5.0
+ - product_id: product.product_product_27
+ product_qty: 4.0
+-
+ I run the 'Cancel' wizard. I fill the reason.
+-
+ !record {model: purchase.action_modal_cancelreason, id: purchase_order_ext_rfq2cancel1_cancelreason}:
+ reason_id: purchase_cancelreason_rfq_canceled
+-
+ I run the 'Cancel' wizard. I confirm the wizard.
+-
+ !python {model: purchase.order}: |
+ self.purchase_cancel_ok(cr, uid, [ref("purchase_order_ext_rfq2cancel1")],{'active_id':ref("purchase_order_ext_rfq2cancel1_cancelreason")})
+-
+ I check the "Canceled" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_rfq2cancel1}:
+ - type == 'rfq'
+ - state == 'cancel'
+ - cancel_reason_id.id == ref("purchase_cancelreason_rfq_canceled")
=== added file 'purchase_extended/test/process/rfq2order.yml'
--- purchase_extended/test/process/rfq2order.yml 1970-01-01 00:00:00 +0000
+++ purchase_extended/test/process/rfq2order.yml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,93 @@
+-
+ Standard flow from a new RFQ to a PO.
+-
+ Create RFQ
+-
+ !record {model: purchase.order, id: purchase_order_ext_rfq2order1}:
+ partner_id: base.res_partner_1
+ invoice_method: order
+ date_order: '2013-08-02'
+ bid_validity: '2013-08-15'
+ order_line:
+ - product_id: product.product_product_15
+ product_qty: 15.0
+ date_planned: '2013-08-30'
+ - product_id: product.product_product_25
+ product_qty: 5.0
+ - product_id: product.product_product_27
+ product_qty: 4.0
+-
+ All prices must be 0.
+-
+ !python {model: purchase.order}: |
+ purchase_order = self.browse(cr, uid, ref("purchase_order_ext_rfq2order1"))
+ for line in purchase_order.order_line:
+ assert line.price_subtotal == 0, "The price must be 0 in the RFQ"
+-
+ Type must be 'rfq' and the total untaxed amount of the RFQ is 0.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_rfq2order1, string: The amount of RFQ is not correctly computed}:
+ - type == 'rfq'
+ - state == 'draft'
+ - amount_untaxed == 0
+-
+ I print the RFQ.
+-
+ !python {model: purchase.order}: |
+ self.print_quotation(cr, uid, [ref("purchase_order_ext_rfq2order1")])
+-
+ I check the "RFQ sent" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_rfq2order1}:
+ - type == 'rfq'
+ - state == 'sent'
+-
+ I encode the Bid. I set a price on the lines.
+-
+ !python {model: purchase.order}: |
+ line_ids = self.read(cr, uid, ref("purchase_order_ext_rfq2order1"), ['order_line'], load='_classic_write')['order_line']
+ self.pool.get('purchase.order.line').write(cr, uid, line_ids, {'price_unit': 79.80})
+-
+ I run the 'Bid encoded' wizard. I fill the date.
+-
+ !record {model: purchase.action_modal_datetime, id: purchase_order_ext_rfq2order1_bidencoded}:
+ datetime: '2013-08-10 00:00:00'
+-
+ I run the 'Bid encoded' wizard. I confirm the wizard.
+-
+ !python {model: purchase.order}: |
+ self.bid_received_ok(cr, uid, [ref("purchase_order_ext_rfq2order1")],{'active_id':ref("purchase_order_ext_rfq2order1_bidencoded")})
+-
+ I check the "Bid Encoded" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_rfq2order1}:
+ - type == 'bid'
+ - state == 'bid'
+-
+ I convert to PO
+-
+ !python {model: purchase.order}: |
+ import netsvc
+ purchase_order = self.browse(cr, uid, ref("purchase_order_ext_rfq2order1"))
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', purchase_order.id, 'draft_po', cr)
+-
+ I check the "Draft PO" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_rfq2order1}:
+ - type == 'purchase'
+ - state == 'draftpo'
+-
+ I confirm the draft PO.
+-
+ !python {model: purchase.order}: |
+ import netsvc
+ purchase_order = self.browse(cr, uid, ref("purchase_order_ext_rfq2order1"))
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', purchase_order.id, 'purchase_confirm', cr)
+-
+ I check the "Approved" status.
+-
+ !assert {model: purchase.order, id: purchase_order_ext_rfq2order1}:
+ - type == 'purchase'
+ - state == 'approved'
=== added directory 'purchase_extended/view'
=== added file 'purchase_extended/view/purchase_cancel.xml'
--- purchase_extended/view/purchase_cancel.xml 1970-01-01 00:00:00 +0000
+++ purchase_extended/view/purchase_cancel.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record model="ir.ui.view" id="view_purchase_cancelreason_form">
+ <field name="name">Purchase Cancel Reason</field>
+ <field name="model">purchase.cancelreason</field>
+ <field name="arch" type="xml">
+ <form string="Purchase Cancel Reason">
+ <field name="name"/>
+ <field name="type"/>
+ </form>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="view_purchase_cancelreason_tree">
+ <field name="name">Purchase Cancel Reasons</field>
+ <field name="model">purchase.cancelreason</field>
+ <field name="arch" type="xml">
+ <tree string="Purchase Cancel Reasons">
+ <field name="name"/>
+ <field name="type"/>
+ </tree>
+ </field>
+ </record>
+ <record model="ir.actions.act_window" id="action_purchase_cancelreason">
+ <field name="name">Purchase Cancel Reasons</field>
+ <field name="res_model">purchase.cancelreason</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+ <menuitem id="menu_purchase_config_cancelreason" parent="purchase.menu_purchase_config_purchase" name="Purchase Cancel Reasons" action="action_purchase_cancelreason" sequence="150" groups="purchase.group_purchase_manager"/>
+ </data>
+</openerp>
=== added file 'purchase_extended/view/purchase_order.xml'
--- purchase_extended/view/purchase_order.xml 1970-01-01 00:00:00 +0000
+++ purchase_extended/view/purchase_order.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record model="ir.ui.view" id="view_purchase_order_form">
+ <field name="name">purchase.order.form.inherit</field>
+ <field name="model">purchase.order</field>
+ <field name="inherit_id" ref="purchase.purchase_order_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//sheet/div[@class='oe_title']/h1/label[@string='Request for Quotation ']" position="replace"/>
+ <xpath expr="//sheet/div[@class='oe_title']/h1/label[@string='Purchase Order ']" position="replace"/>
+ <xpath expr="//sheet/div[@class='oe_title']/h1/field[@name='name']" position="before">
+ <field name="type" nolabel="1" class="oe_inline"/>
+ <label string=" "/>
+ </xpath>
+ <xpath expr="//sheet/div[@class='oe_title']/h1" position="after">
+ <h2 attrs="{'invisible': [('state', '!=', 'cancel')]}">
+ <label for="cancel_reason_id" string="Reason for Cancellation:"/>
+ <field name="cancel_reason_id" class="oe_inline" />
+ </h2>
+ </xpath>
+ <xpath expr="//field[@name='state']" position="attributes">
+ <attribute name="statusbar_visible">draft,sent,bid,draftpo,approved,done</attribute>
+ </xpath>
+ <xpath expr="//button[@name='action_cancel_draft']" position="attributes">
+ <attribute name="states">cancel,draftpo</attribute>
+ <attribute name="string">Reset to Draft RFQ</attribute>
+ </xpath>
+ <xpath expr="//button[@string='Send PO by Email']" position="after">
+ <button name="wkf_send_rfq" states="draftpo" string="Send Draft PO by Email" type="object" context="{'send_rfq':True}"/>
+ </xpath>
+ <xpath expr="//button[@name='bid_received']" position="attributes">
+ <attribute name="string">Bid Encoded</attribute>
+ <attribute name="type">object</attribute>
+ <attribute name="states">sent,draftbid</attribute>
+ </xpath>
+ <xpath expr="//button[@id='bid_confirm']" position="attributes">
+ <attribute name="states">draftpo</attribute>
+ </xpath>
+ <xpath expr="//button[@id='draft_confirm']" position="attributes">
+ <attribute name="states">draftpo</attribute>
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ <xpath expr="//field[@name='incoterm_id']" position="after">
+ <field name="incoterm_address"/>
+ </xpath>
+ <xpath expr="//field[@name='dest_address_id']" position="replace"/>
+ <field name="warehouse_id" position="before">
+ <field name="consignee_id"/>
+ <field name="dest_address_id" on_change="onchange_dest_address_id_mod(dest_address_id, warehouse_id)" string="Delivery Address"/>
+ </field>
+ <field name="warehouse_id" position="attributes">
+ <attribute name="on_change">onchange_warehouse_id(warehouse_id)</attribute>
+ </field>
+ <button name="purchase_cancel" position="attributes">
+ <attribute name="type">object</attribute>
+ <attribute name="states">draft,sent,draftbid,bid,draftpo,confirmed</attribute>
+ </button>
+ <xpath expr="//button[@name='purchase_confirm']" position="after">
+ <button name="draft_po" states="draft" string="Convert to PO"/>
+ <button name="draft_po" states="bid" string="Convert to PO" class="oe_highlight"/>
+ </xpath>
+ <field name="product_id" position="replace">
+ <field name="product_id" on_change="onchange_product_id(parent.pricelist_id,product_id,0,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context,parent.state,parent.type)"/>
+ </field>
+ <field name="product_qty" position="replace">
+ <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context,parent.state,parent.type)"/>
+ </field>
+ </field>
+ </record>
+ <record id="purchase.purchase_rfq" model="ir.actions.act_window">
+ <field name="domain">[('type','in',('rfq','bid'))]</field>
+ </record>
+ <record id="purchase.purchase_form_action" model="ir.actions.act_window">
+ <field name="context">{'draft_po':True}</field>
+ <field name="domain">[('type','=','purchase')]</field>
+ </record>
+
+ <record model="ir.ui.view" id="view_purchase_order_invoice_form">
+ <field name="name">purchase.order.form.invoice.inherit</field>
+ <field name="model">purchase.order</field>
+ <field name="inherit_id" ref="purchase.purchase_order_2_stock_picking"/>
+ <field name="arch" type="xml">
+ <xpath expr="//button[@name='invoice_open']" position="attributes">
+ <attribute name="attrs">{'invisible': [('state', 'in', ['draft','draftbid','draftpo'])]}</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="on_change_on_po_line_form" model="ir.ui.view">
+ <field name="name">on change on po line form</field>
+ <field name="model">purchase.order.line</field>
+ <field name="inherit_id" ref="purchase.purchase_order_line_form" />
+ <field name="arch" type="xml">
+ <field name="product_id" position="replace">
+ <field name="product_id" on_change="onchange_product_id(parent.pricelist_id,product_id,0,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context,parent.state,parent.type)"/>
+ </field>
+ <field name="product_qty" position="replace">
+ <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context,parent.state,parent.type)"/>
+ </field>
+ </field>
+ </record>
+ </data>
+</openerp>
=== added directory 'purchase_extended/wizard'
=== added directory 'purchase_extended/wizard.moved'
=== added file 'purchase_extended/wizard/__init__.py'
--- purchase_extended/wizard/__init__.py 1970-01-01 00:00:00 +0000
+++ purchase_extended/wizard/__init__.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,2 @@
+import modal
+import action_cancel_reason
=== added file 'purchase_extended/wizard/action_cancel_reason.py'
--- purchase_extended/wizard/action_cancel_reason.py 1970-01-01 00:00:00 +0000
+++ purchase_extended/wizard/action_cancel_reason.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,9 @@
+from osv import fields, osv
+
+
+class action_modal_cancelreason(osv.TransientModel):
+ _name = "purchase.action_modal_cancelreason"
+ _inherit = "purchase.action_modal"
+ _columns = {
+ 'reason_id': fields.many2one('purchase.cancelreason', 'Reason for Cancellation', required=True),
+ }
=== added file 'purchase_extended/wizard/action_cancel_reason.xml'
--- purchase_extended/wizard/action_cancel_reason.xml 1970-01-01 00:00:00 +0000
+++ purchase_extended/wizard/action_cancel_reason.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="action_modal_cancel_reason" model="ir.ui.view">
+ <field name="name">action.modal</field>
+ <field name="model">purchase.action_modal_cancelreason</field>
+ <field name="type">form</field>
+ <field name="arch" type="xml">
+ <form string="Confirm" version="7.0">
+ <p>
+ Please confirm operation
+ </p>
+ <group>
+ <field name="reason_id"/>
+ </group>
+ <footer>
+ <button string="Confirm" class="oe_highlight" type="object" name="action"/>
+ or
+ <button special="cancel" string="Cancel" class="oe_link"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+ </data>
+</openerp>
=== added file 'purchase_extended/wizard/modal.py'
--- purchase_extended/wizard/modal.py 1970-01-01 00:00:00 +0000
+++ purchase_extended/wizard/modal.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,26 @@
+from osv import fields, osv
+
+
+class action_modal(osv.TransientModel):
+ _name = "purchase.action_modal"
+ _columns = {}
+
+ def action(self, cr, uid, ids, context):
+ for e in ('active_model', 'active_ids', 'action'):
+ if e not in context:
+ return False
+ ctx = context.copy()
+ ctx['active_ids'] = ids
+ ctx['active_id'] = ids[0]
+ res = getattr(self.pool.get(context['active_model']), context['action'])(cr, uid, context['active_ids'], context=ctx)
+ if isinstance(res, dict):
+ return res
+ return {'type': 'ir.actions.act_window_close'}
+
+
+class action_modal_datetime(osv.TransientModel):
+ _name = "purchase.action_modal_datetime"
+ _inherit = "purchase.action_modal"
+ _columns = {
+ 'datetime': fields.datetime('Date'),
+ }
=== added file 'purchase_extended/wizard/modal.xml'
--- purchase_extended/wizard/modal.xml 1970-01-01 00:00:00 +0000
+++ purchase_extended/wizard/modal.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="action_modal_bid_date" model="ir.ui.view">
+ <field name="name">action.modal</field>
+ <field name="model">purchase.action_modal_datetime</field>
+ <field name="type">form</field>
+ <field name="arch" type="xml">
+ <form string="Confirm" version="7.0">
+ <p>
+ Please confirm operation
+ </p>
+ <group>
+ <field string="Bid received date" name="datetime"/>
+ </group>
+ <footer>
+ <button string="Confirm" class="oe_highlight" type="object" name="action"/>
+ or
+ <button special="cancel" string="Cancel" class="oe_link"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+ </data>
+</openerp>
=== added directory 'purchase_extended/workflow'
=== added file 'purchase_extended/workflow/purchase_order.xml'
--- purchase_extended/workflow/purchase_order.xml 1970-01-01 00:00:00 +0000
+++ purchase_extended/workflow/purchase_order.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <!-- Prevent to send a RFQ without any line -->
+ <record id="purchase.trans_draft_sent" model="workflow.transition">
+ <field name="condition">_has_lines()</field>
+ </record>
+
+ <!-- Differenciate draft RFQ and draft PO. Add a state draft PO and adapt workflow -->
+ <record id="act_draft_po" model="workflow.activity">
+ <field name="wkf_id" ref="purchase.purchase_order"/>
+ <field name="name">draft PO</field>
+ <field name="kind">function</field>
+ <field name="action">wkf_draft_po()</field>
+ </record>
+ <record id="trans_draft_draftpo" model="workflow.transition">
+ <field name="act_from" ref="purchase.act_draft"/>
+ <field name="act_to" ref="act_draft_po"/>
+ <field name="signal">draft_po</field>
+ </record>
+ <record id="purchase.trans_draft_confirmed" model="workflow.transition">
+ <field name="act_from" ref="act_draft_po"/>
+ </record>
+ <record id="trans_bid_draftpo" model="workflow.transition">
+ <field name="act_from" ref="purchase.act_bid"/>
+ <field name="act_to" ref="act_draft_po"/>
+ <field name="signal">draft_po</field>
+ </record>
+ <record id="trans_draftpo_cancel" model="workflow.transition">
+ <field name="act_from" ref="act_draft_po"/>
+ <field name="act_to" ref="purchase.act_cancel"/>
+ <field name="signal">purchase_cancel</field>
+ </record>
+
+ <!-- Allow to create directly a draft Bid -->
+ <record id="act_draftbid" model="workflow.activity">
+ <field name="wkf_id" ref="purchase.purchase_order"/>
+ <field name="name">draft Bid</field>
+ <field name="kind">function</field>
+ <field name="action">write({'state': 'draftbid','type': 'bid'})</field>
+ </record>
+ <record id="trans_draft_draftbid" model="workflow.transition">
+ <field name="act_from" ref="purchase.act_draft"/>
+ <field name="act_to" ref="act_draftbid"/>
+ <field name="signal">draft_bid</field>
+ </record>
+ <record id="trans_draftbid_bid" model="workflow.transition">
+ <field name="act_from" ref="act_draftbid"/>
+ <field name="act_to" ref="purchase.act_bid"/>
+ <field name="signal">bid_received</field>
+ </record>
+ <record id="trans_draftbid_cancel" model="workflow.transition">
+ <field name="act_from" ref="act_draftbid"/>
+ <field name="act_to" ref="purchase.act_cancel"/>
+ <field name="signal">purchase_cancel</field>
+ </record>
+
+ <record id="act_po_requisition_selected" model="workflow.activity">
+ <field name="wkf_id" ref="purchase.purchase_order"/>
+ <field name="name">Bid selected</field>
+ <field name="kind">function</field>
+ <field name="action">po_tender_requisition_selected()</field>
+ <field name="flow_stop">True</field>
+ </record>
+ <record id="trans_po_requisition_selected" model="workflow.transition">
+ <field name="act_from" ref="purchase.act_bid"/>
+ <field name="act_to" ref="act_po_requisition_selected"/>
+ <field name="signal">select_requisition</field>
+ </record>
+
+
+ <!-- Rename some activities to make the workflow clear -->
+ <record id="purchase.act_draft" model="workflow.activity">
+ <field name="name">draft RFQ</field>
+ </record>
+ <record id="purchase.act_sent" model="workflow.activity">
+ <field name="name">RFQ sent</field>
+ </record>
+ <record id="purchase.act_bid" model="workflow.activity">
+ <field name="name">Bid encoded</field>
+ </record>
+
+ </data>
+</openerp>
=== added directory 'purchase_requisition_extended'
=== added file 'purchase_requisition_extended/__init__.py'
--- purchase_requisition_extended/__init__.py 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/__init__.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+from . import model
+from . import wizard
=== added file 'purchase_requisition_extended/__openerp__.py'
--- purchase_requisition_extended/__openerp__.py 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/__openerp__.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright 2013 Camptocamp SA
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{"name": "Purchase Requisition Extended",
+ "version": "0.1",
+ "author": "Camptocamp",
+ "license": "AGPL-3",
+ "category": "Purchase Management",
+ "complexity": "normal",
+ "images": [],
+ "description": """
+This module improves the standard Purchase Requisition module.
+==============================================================
+This module allows to make calls for bids by generating RFQ for selected
+suppliers, encode the bids, compare and select bids, generate draft POs.
+
+First, a list of products is established. The call for bid is then confirmed
+and RFQs can be generated. They are in the state 'Draft RFQ' until it is send
+to the supplier and marked as 'RFQ Sent'. The bids has to be encoded and moved
+to state 'Bid Encoded'. When closing the call for bids in order to start the
+bids selection, all RFQ that have not been sent will be canceled. However,
+send RFQs can still be encoded. Bids that are not received will remain in state
+'RFQ Sent' and can be manually canceled.
+
+Afterwards, the bids selection can be started by choosing product lines. The
+workflow has been modified to allow to mark that the selection of bids has
+occurred but without having to generate the POs yet, those can be created at a
+new later state called 'Bids Selected'.
+
+When generating POs, the are created in the state 'Draft PO' introduced by the
+module purchase_extended.
+
+
+Some fields have been added to specify with more details the call for bids and
+prefill fields of the generated RFQs.
+
+
+A link has been added between a call for bids line and the corresponding line
+of each generated RFQ. This is used for the bids comparison in order to compare
+bid lines and group then properly.
+""",
+ "depends": ["purchase_requisition",
+ "stock", # For incoterms
+ "purchase_extended", # for field incoterms place
+ ],
+ "demo": [],
+ "data": ["wizard/modal.xml",
+ "wizard/purchase_requisition_partner_view.xml",
+ "view/purchase_requisition.xml",
+ "view/purchase_order.xml",
+ "workflow/purchase_requisition.xml",
+ "data/purchase.cancelreason.yml",
+ ],
+ "js": [
+ "static/src/js/web_addons.js",
+ ],
+ "auto_install": False,
+ "test": ["test/process/restricted.yml",
+ ],
+ "installable": True,
+ "certificate": "",
+ }
=== added directory 'purchase_requisition_extended/data'
=== added file 'purchase_requisition_extended/data/purchase.cancelreason.yml'
--- purchase_requisition_extended/data/purchase.cancelreason.yml 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/data/purchase.cancelreason.yml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,9 @@
+-
+ !context {noupdate: True}
+-
+ Creating the cancel reasons
+-
+ !record {model: purchase.cancelreason, id: purchase_cancelreason_callforbids_canceled}:
+ name: Call for bids canceled
+ type: rfq
+ nounlink: 1
=== added directory 'purchase_requisition_extended/model'
=== added file 'purchase_requisition_extended/model/__init__.py'
--- purchase_requisition_extended/model/__init__.py 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/model/__init__.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import purchase_requisition
+import purchase_order
=== added file 'purchase_requisition_extended/model/purchase_order.py'
--- purchase_requisition_extended/model/purchase_order.py 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/model/purchase_order.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import fields, orm
+from openerp import SUPERUSER_ID
+
+
+class purchase_order(orm.Model):
+ _inherit = 'purchase.order'
+ _columns = {
+ 'bid_partial': fields.boolean(
+ 'Bid partially selected',
+ readonly=True,
+ help="True if the bid has been partially selected"),
+ 'tender_bid_receipt_mode': fields.function(
+ lambda self, *args, **kwargs: self._get_tender(*args, **kwargs),
+ multi="callforbids",
+ readonly=True,
+ type='selection',
+ selection=[('open', 'Open'), ('sealed', 'Sealed')],
+ string='Bid Receipt Mode'),
+ }
+ #TODO: lines should not be deleted or created if linked to a callforbids
+
+ def _get_tender(self, cr, uid, ids, fields, args, context=None):
+ # when _classic_write load is used, the many2one are only the
+ # ids instead of the tuples (id, name_get)
+ purch_req_obj = self.pool.get('purchase.requisition')
+ orders = self.read(cr, uid, ids, ['requisition_id'],
+ context=context, load='_classic_write')
+ req_ids = list(set(order['requisition_id'] for order in orders
+ if order['requisition_id']))
+ # we'll read the fields without the 'tender_' prefix
+ # and copy their value in the fields with the prefix
+ read_fields = [x[len('tender_'):] for x in fields]
+ requisitions = purch_req_obj.read(cr, uid,
+ req_ids,
+ read_fields,
+ context=context,
+ load='_classic_write')
+ # copy the dict but rename the fields with 'tender_' prefix
+ tender_reqs = {}
+ for req in requisitions:
+ tender_reqs[req['id']] = dict(('tender_' + field, value)
+ for field, value
+ in req.iteritems()
+ if 'tender_' + field in fields)
+ res = {}
+ for po in orders:
+ if po['requisition_id']:
+ res[po['id']] = tender_reqs[po['requisition_id']]
+ else:
+ res[po['id']] = {}.fromkeys(fields, False)
+ return res
+
+ def _prepare_purchase_order(self, cr, uid, requisition, supplier, context=None):
+ values = super(purchase_order, self)._prepare_purchase_order(
+ cr, uid, requisition, supplier, context=context)
+ values.update({
+ 'bid_validity': requisition.req_validity,
+ 'payment_term_id': requisition.req_payment_term_id,
+ 'incoterm_id': requisition.req_incoterm_id,
+ 'incoterm_address': requisition.req_incoterm_address,
+ 'transport_mode_id': requisition.req_transport_mode_id,
+ })
+ if requisition.pricelist_id:
+ values.update({'pricelist_id': requisition.pricelist_id.id})
+ return values
+
+ def copy(self, cr, uid, id, default=None, context=None):
+ """ Need to set origin after copy because original copy clears origin """
+ if default is None:
+ default = {}
+ initial_origin = default.get('origin')
+ newid = super(purchase_order, self).copy(cr, uid, id, default=default,
+ context=context)
+ if initial_origin and 'requisition_id' in default:
+ self.write(cr, SUPERUSER_ID, [newid], {'origin': initial_origin}, context=context)
+ return newid
+
+
+class purchase_order_line(orm.Model):
+ _inherit = 'purchase.order.line'
+ _columns = {
+ 'requisition_line_id': fields.many2one('purchase.requisition.line', 'Call for Bid Line', readonly=True),
+ }
+
+ def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
+ """Do not aggregate price and qty. We need to do it this way as there
+ is no group_operator that can be set to prevent aggregating float"""
+ result = super(purchase_order_line, self).read_group(cr, uid, domain, fields, groupby,
+ offset=offset, limit=limit, context=context, orderby=orderby)
+ for res in result:
+ if 'price_unit' in res:
+ del res['price_unit']
+ if 'product_qty' in res:
+ del res['product_qty']
+ if 'lead_time' in res:
+ del res['lead_time']
+ return result
=== added file 'purchase_requisition_extended/model/purchase_requisition.py'
--- purchase_requisition_extended/model/purchase_requisition.py 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/model/purchase_requisition.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,421 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import fields, orm
+import openerp.osv.expression as expression
+from openerp.tools.safe_eval import safe_eval as eval
+from openerp.tools.translate import _
+from openerp import netsvc
+from openerp.tools.float_utils import float_compare
+
+
+class PurchaseRequisition(orm.Model):
+ _inherit = "purchase.requisition"
+ _description = "Call for Bids"
+ _columns = {
+ # modified
+ 'state': fields.selection([('draft', 'Draft'),
+ ('in_progress', 'Confirmed'),
+ ('open', 'Bids Selection'),
+ ('closed', 'Bids Selected'), # added
+ ('done', 'PO Created'),
+ ('cancel', 'Canceled')],
+ 'Status', track_visibility='onchange',
+ required=True),
+ 'purchase_ids': fields.one2many('purchase.order', 'requisition_id',
+ 'Purchase Orders',
+ states={'done': [('readonly', True)]},
+ domain=[('type', 'in', ('rfq', 'bid'))]),
+ # new
+ 'req_validity': fields.date("Requested Bid's End of Validity",
+ help="Requested validity period requested to the bidder, "
+ "i.e. please send bids that stay valid until that "
+ "date.\n The bidder is allowed to send a bid with "
+ "another validity end date that gets encoded in the "
+ "bid."),
+ 'bid_tendering_mode': fields.selection([('open', 'Open'),
+ ('restricted', 'Restricted')],
+ 'Call for Bids Mode',
+ help="- Restricted : you select yourself the "
+ "bidders and generate a RFQ for each of "
+ "those. \n"
+ "- Open : anybody can bid (you have to "
+ "advertise the call for bids) and you "
+ "directly encode the bids you received. "
+ "You are still able to generate RFQ if "
+ "you want to contact usual bidders."),
+ 'bid_receipt_mode': fields.selection([('open', 'Open'),
+ ('sealed', 'Sealed')],
+ 'Bid Receipt Mode',
+ required=True,
+ help="- Open : The bids can be opened when "
+ "received and encoded. \n"
+ "- Closed : The bids can be marked as "
+ "received but they have to be opened \n"
+ "all at the same time after an opening "
+ "ceremony (probably specific to public "
+ "sector)."),
+ 'consignee_id': fields.many2one('res.partner',
+ 'Consignee',
+ help="Person responsible of delivery"),
+ 'dest_address_id': fields.many2one('res.partner',
+ 'Delivery Address'),
+ 'req_incoterm_id': fields.many2one(
+ 'stock.incoterms',
+ 'Requested Incoterms',
+ help="Default value requested to the supplier. "
+ "International Commercial Terms are a series of predefined "
+ "commercial terms used in international transactions."
+ ),
+ 'req_incoterm_address': fields.char(
+ 'Requested Incoterms Place',
+ help="Incoterm Place of Delivery. "
+ "International Commercial Terms are a series of "
+ "predefined commercial terms used in "
+ "international transactions."),
+ 'req_payment_term_id': fields.many2one(
+ 'account.payment.term',
+ 'Requested Payment Term',
+ help="Default value requested to the supplier."
+ ),
+ 'pricelist_id': fields.many2one(
+ 'product.pricelist',
+ 'Pricelist',
+ domain=[('type', '=', 'purchase')],
+ help="If set that pricelist will be used to generate the RFQ."
+ "Mostely used to ask a requisition in a given currency."
+ ),
+ 'date_end': fields.datetime('Bid Submission Deadline',
+ help="All bids received after that date won't be valid "
+ " (probably specific to public sector)."),
+ }
+ _defaults = {
+ 'bid_receipt_mode': 'open',
+ }
+
+ def _has_product_lines(self, cr, uid, ids, context=None):
+ """
+ Check there are products lines when confirming Call for Bids.
+ Called from workflow transition draft->sent.
+ """
+ for callforbids in self.browse(cr, uid, ids, context=context):
+ if not callforbids.line_ids:
+ raise orm.except_orm(
+ _('Error!'),
+ _('You have to define some products before confirming the call for bids.'))
+ return True
+
+ def _prepare_purchase_order(self, cr, uid, requisition, supplier, context=None):
+ values = super(PurchaseRequisition, self)._prepare_purchase_order(
+ cr, uid, requisition, supplier, context=context)
+ values.update({
+ 'dest_address_id': requisition.dest_address_id.id,
+ 'consignee_id': requisition.consignee_id.id,
+ 'bid_validity': requisition.req_validity,
+ 'payment_term_id': requisition.req_payment_term_id.id,
+ 'incoterm_id': requisition.req_incoterm_id.id,
+ 'incoterm_address': requisition.req_incoterm_address,
+ })
+ if requisition.pricelist_id:
+ values['pricelist_id'] = requisition.pricelist_id.id
+ return values
+
+ def _prepare_purchase_order_line(self, cr, uid, requisition,
+ requisition_line, purchase_id,
+ supplier, context=None):
+ vals = super(PurchaseRequisition, self)._prepare_purchase_order_line(
+ cr, uid, requisition, requisition_line, purchase_id, supplier, context)
+ vals['price_unit'] = 0
+ vals['requisition_line_id'] = requisition_line.id
+ return vals
+
+ def onchange_dest_address_id(self, cr, uid, ids, dest_address_id,
+ warehouse_id, context=None):
+ warehouse_obj = self.pool.get('stock.warehouse')
+ dest_ids = warehouse_obj.search(cr, uid,
+ [('partner_id', '=', dest_address_id)],
+ context=context)
+ if dest_ids:
+ if warehouse_id not in dest_ids:
+ warehouse_id = dest_ids[0]
+ else:
+ warehouse_id = False
+ return {'value': {'warehouse_id': warehouse_id}}
+
+ def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
+ if not warehouse_id:
+ return {}
+ warehouse_obj = self.pool.get('stock.warehouse')
+ warehouse = warehouse_obj.browse(cr, uid,
+ warehouse_id, context=context)
+ dest_id = warehouse.partner_id.id
+ return {'value': {'dest_address_id': dest_id}}
+
+ def trigger_validate_po(self, cr, uid, po_id, context=None):
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.order', po_id, 'draft_po', cr)
+ po_obj = self.pool.get('purchase.order')
+ po_obj.write(cr, uid, po_id, {'bid_partial': False}, context=context)
+ return True
+
+ def check_valid_quotation(self, cr, uid, quotation, context=None):
+ return False
+
+ def generate_po(self, cr, uid, ids, context=None):
+ if isinstance(ids, (list, tuple)):
+ assert len(ids) == 1, "Only 1 ID expected"
+ ids = ids[0]
+ tender = self.browse(cr, uid, ids, context=context)
+ po_obj = self.pool.get('purchase.order')
+ for po_line in tender.po_line_ids:
+ # set bid selected boolean to true on RFQ containing confirmed lines
+ if (po_line.state == 'confirmed' and
+ not po_line.order_id.bid_partial):
+ po_obj.write(cr, uid,
+ po_line.order_id.id,
+ {'bid_partial': True},
+ context=context)
+ return super(PurchaseRequisition, self).generate_po(cr, uid, [ids], context=context)
+
+ def quotation_selected(self, cr, uid, quotation, context=None):
+ """Predicate that checks if a quotation has at least one line chosen
+ :param quotation: record of 'purchase.order'
+
+ :returns: True if one line has been chosen
+
+ """
+ # This topic is subject to changes
+ return quotation.bid_partial
+
+ def cancel_quotation(self, cr, uid, tender, context=None):
+ """
+ Called from generate_po. Cancel only draft and sent rfq
+ """
+ po = self.pool.get('purchase.order')
+ wf_service = netsvc.LocalService("workflow")
+ tender.refresh()
+ for quotation in tender.purchase_ids:
+ if quotation.state in ['draft', 'sent', 'bid']:
+ if self.quotation_selected(cr, uid, quotation, context=context):
+ wf_service.trg_validate(uid, 'purchase.order', quotation.id,
+ 'select_requisition', cr)
+ else:
+ wf_service.trg_validate(uid, 'purchase.order', quotation.id, 'purchase_cancel', cr)
+ po.message_post(cr, uid, [quotation.id],
+ body=_('Canceled by the call for bids associated'
+ ' to this request for quotation.'),
+ context=context)
+
+ return True
+
+ def tender_open(self, cr, uid, ids, context=None):
+ """
+ Cancel RFQ that have not been sent. Ensure that there are RFQs."
+ """
+ cancel_ids = []
+ rfq_valid = False
+ for callforbids in self.browse(cr, uid, ids, context=context):
+ for purchase in callforbids.purchase_ids:
+ if purchase.state == 'draft':
+ cancel_ids.append(purchase.id)
+ elif purchase.state != 'cancel':
+ rfq_valid = True
+ if cancel_ids:
+ reason_id = self.pool.get('ir.model.data').get_object_reference(cr, uid,
+ 'purchase_extended', 'purchase_cancelreason_rfq_canceled')[1]
+ purchase_order_obj = self.pool.get('purchase.order')
+ purchase_order_obj.write(cr, uid, cancel_ids, {'cancel_reason': reason_id}, context=context)
+ purchase_order_obj.action_cancel(cr, uid, cancel_ids, context=context)
+ if not rfq_valid:
+ raise orm.except_orm(
+ _('Error'),
+ _('You do not have valid sent RFQs.'))
+ return super(PurchaseRequisition, self).tender_open(cr, uid, ids, context=context)
+
+ def _get_po_to_cancel(self, cr, uid, callforbids, context=None):
+ """Get the list of PO/RFQ that can be canceled on RFQ
+
+ :param callforbids: `purchase.requisition` record
+
+ :returns: List of candidate PO/RFQ record
+
+ """
+ res = []
+ for purchase in callforbids.purchase_ids:
+ if purchase.state in ('draft', 'sent'):
+ res.append(purchase)
+ return res
+
+ def _check_can_be_canceled(self, callforbids, context=None):
+ """Raise an exception if callforbids can not be cancelled
+ :param callforbids: `purchase.requisition` record
+
+ :returns: True or raise exception
+
+ """
+ for purchase in callforbids.purchase_ids:
+ if purchase.state not in ('draft', 'sent'):
+ raise orm.except_orm(
+ _('Error'),
+ _('You cannot cancel a call for bids which '
+ 'has already received bids.'))
+ return True
+
+ def _cancel_po_with_reason(self, cr, uid, po_list, reason_id, context=None):
+ """Cancel purchase order of a tender, using given reasons
+ :param po_list: list of po record to cancel
+ :param reason_id: reason id of cancelation
+
+ :returns: cancel po record list
+
+ """
+ purchase_order_obj = self.pool.get('purchase.order')
+ purchase_order_obj.write(cr, uid,
+ [x.id for x in po_list],
+ {'cancel_reason': reason_id},
+ context=context)
+ for order in po_list:
+ # passing full list raises assert error
+ purchase_order_obj.action_cancel_no_reason(cr, uid, [order.id],
+ context=context)
+ return po_list
+
+ def _get_default_reason(self, cr, uid, context=None):
+ """Return default cancel reason"""
+ reason = self.pool.get('ir.model.data').get_object_reference(
+ cr,
+ uid,
+ 'purchase_requisition_extended',
+ 'purchase_cancelreason_callforbids_canceled'
+ )
+ return reason[1]
+
+ def tender_cancel(self, cr, uid, ids, context=None):
+ """
+ Cancel call for bids and try to cancelrelated RFQs/PO
+
+ """
+ reason_id = self._get_default_reason(cr, uid, context=context)
+ for callforbids in self.browse(cr, uid, ids, context=context):
+ self._check_can_be_canceled(callforbids, context=context)
+ po_to_cancel = self._get_po_to_cancel(cr, uid, callforbids, context=context)
+ if po_to_cancel:
+ self._cancel_po_with_reason(cr, uid, po_to_cancel, reason_id,
+ context=context)
+ return self.write(cr, uid, ids, {'state': 'cancel'})
+
+ def tender_close(self, cr, uid, ids, context=None):
+ return self.write(cr, uid, ids, {'state': 'closed'}, context=context)
+
+ def open_rfq(self, cr, uid, ids, context=None):
+ """
+ This opens rfq view to view all generated rfq/bids associated to the call for bids
+ """
+ if context is None:
+ context = {}
+ res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase', 'purchase_rfq', context=context)
+ res['domain'] = expression.AND([eval(res.get('domain', [])), [('requisition_id', 'in', ids)]])
+ # FIXME: need to disable create - temporarily set as invisible in view
+ return res
+
+ def open_po(self, cr, uid, ids, context=None):
+ """
+ This opens po view to view all generated po associated to the call for bids
+ """
+ if context is None:
+ context = {}
+ res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'purchase', 'purchase_form_action', context=context)
+ res['domain'] = expression.AND([eval(res.get('domain', [])), [('requisition_id', 'in', ids)]])
+ return res
+
+ def close_callforbids(self, cr, uid, ids, context=None):
+ """
+ Check all quantities have been sourced
+ """
+ # this method is called from a special JS event and ids is
+ # inferred from 'active_ids', in some cases, the webclient send
+ # no ids, so we prevent a crash
+ if not ids:
+ raise orm.except_orm(
+ _('Error'),
+ _('Impossible to proceed due to an error of the system.\n'
+ 'Please reopen the purchase requisition and try again '))
+ if isinstance(ids, (tuple, list)):
+ assert len(ids) == 1, "Only 1 ID expected, got %s" % ids
+ ids = ids[0]
+ purch_req = self.browse(cr, uid, ids, context=context)
+ dp_obj = self.pool.get('decimal.precision')
+ precision = dp_obj.precision_get(cr, uid, 'Product Unit of Measure')
+ for line in purch_req.line_ids:
+ qty = line.product_qty
+ for pol in line.purchase_line_ids:
+ if pol.state == 'confirmed':
+ qty -= pol.quantity_bid
+ if qty == line.product_qty:
+ break # nothing selected
+ compare = float_compare(qty, 0, precision_digits=precision)
+ if compare != 0:
+ break # too much or too few selected
+ else:
+ return self.close_callforbids_ok(cr, uid, [ids], context=context)
+
+ # open a dialog to confirm that we want more / less or no qty
+ ctx = context.copy()
+ ctx['action'] = 'close_callforbids_ok'
+ ctx['active_model'] = self._name
+
+ get_ref = self.pool.get('ir.model.data').get_object_reference
+ view_id = get_ref(cr, uid, 'purchase_requisition_extended',
+ 'action_modal_close_callforbids')[1]
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'purchase.action_modal',
+ 'view_id': view_id,
+ 'views': [(view_id, 'form')],
+ 'target': 'new',
+ 'context': ctx,
+ }
+
+ def open_product_line(self, cr, uid, ids, context=None):
+ """ Filter to show only lines from bids received. Group by requisition line instead of product for unicity
+ """
+ res = super(PurchaseRequisition, self).open_product_line(cr, uid, ids, context=context)
+ ctx = res.setdefault('context', {})
+ if 'search_default_groupby_product' in ctx:
+ del ctx['search_default_groupby_product']
+ if 'search_default_hide_cancelled' in ctx:
+ del ctx['search_default_hide_cancelled']
+ ctx['search_default_groupby_requisitionline'] = True
+ ctx['search_default_showbids'] = True
+ return res
+
+ def close_callforbids_ok(self, cr, uid, ids, context=None):
+ wf_service = netsvc.LocalService("workflow")
+ for id in ids:
+ wf_service.trg_validate(uid, 'purchase.requisition',
+ id, 'close_bid', cr)
+ return True
+
+
+class purchase_requisition_line(orm.Model):
+ _inherit = "purchase.requisition.line"
+ _columns = {
+ 'remark': fields.text('Remark'),
+ 'purchase_line_ids': fields.one2many('purchase.order.line',
+ 'requisition_line_id',
+ 'Bids Lines',
+ readonly=True),
+ }
+
+ def name_get(self, cr, uid, ids, context=None):
+ res = []
+ for line in self.read(cr, uid, ids,
+ ['product_id', 'product_qty', 'schedule_date'],
+ context=context):
+ name = ""
+ if line['schedule_date']:
+ name += '%s ' % line['schedule_date']
+ name += '%s %s' % (line['product_qty'], line['product_id'][1])
+ res.append((line['id'], name))
+ return res
=== added directory 'purchase_requisition_extended/static'
=== added directory 'purchase_requisition_extended/static/src'
=== added directory 'purchase_requisition_extended/static/src/js'
=== added file 'purchase_requisition_extended/static/src/js/web_addons.js'
--- purchase_requisition_extended/static/src/js/web_addons.js 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/static/src/js/web_addons.js 2015-01-19 14:42:10 +0000
@@ -0,0 +1,51 @@
+openerp.purchase_requisition_extended = function(instance) {
+ var QWeb = instance.web.qweb,
+ _t = instance.web._t;
+
+ instance.web.purchase_requisition.CompareListView.include(
+ {
+ init: function () {
+ var self = this;
+ this._super.apply(this, arguments);
+ this.on(
+ 'list_view_loaded',
+ this,
+ function() {
+ if(self.$buttons.find('.oe_close_bid').length == 0){
+ var button_body = "<button type='button' class='oe_button oe_highlight oe_close_bid'>Confirm Selection</button>"
+ var return_link = '<a accesskey="D" class="oe_bold oe_form_button_cancel" href="#">Close</a>'
+ var button = $(button_body).click(
+ this.proxy('close_bids_selection')
+ );
+ button.after('<span class="oe_fade" style="margin:0 4px">or</span>');
+ button.after(
+ $(return_link).click(
+ function(){self.do_action('history_back')}
+ )
+ );
+ self.$buttons.append(button);
+ }
+ self.$buttons.find('.oe_generate_po').remove();
+ }
+ );
+ },
+ close_bids_selection: function () {
+ var self = this;
+ new instance.web.Model('purchase.requisition').call(
+ "close_callforbids",
+ [self.dataset.context.active_id, self.dataset.context]
+ ).then(
+ function(result) {
+ self.do_action(
+ false,
+ {
+ on_close: function(){self.do_action('history_back')},
+ context: {}
+ }
+ );
+ }
+ );
+ },
+ }
+ );
+}
=== added directory 'purchase_requisition_extended/test'
=== added directory 'purchase_requisition_extended/test/process'
=== added file 'purchase_requisition_extended/test/process/restricted.yml'
--- purchase_requisition_extended/test/process/restricted.yml 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/test/process/restricted.yml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,194 @@
+-
+ Standard flow of a Call for Bids in mode restricted
+-
+ Create Call for Bids
+-
+ !record {model: purchase.requisition, id: purchase_requisition_ext_restricted1}:
+ date_start: '2013-08-02 00:00:00'
+ date_end: '2013-08-30 00:00:00'
+ bid_tendering_mode: 'restricted'
+ schedule_date: '2013-09-30'
+ req_validity: '2013-09-10'
+ line_ids:
+ - product_id: product.product_product_15
+ product_qty: 15.0
+ - product_id: product.product_product_25
+ product_qty: 5.0
+ - product_id: product.product_product_27
+ product_qty: 40.0
+-
+ Confirm Call
+-
+ !python {model: purchase.requisition}: |
+ import netsvc
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_ext_restricted1"), 'sent_suppliers', cr)
+-
+ Create RFQ1. I run the 'Request a quotation' wizard. I fill the supplier.
+-
+ !record {model: purchase.requisition.partner, id: purchase_requisition_ext_restricted1_partner1_create}:
+ partner_id: base.res_partner_2
+-
+ Create RFQ1. I confirm the wizard.
+-
+ !python {model: purchase.requisition.partner}: |
+ self.create_order(cr, uid, [ref("purchase_requisition_ext_restricted1_partner1_create")],{
+ 'active_model': 'purchase.requisition',
+ 'active_id': ref("purchase_requisition_ext_restricted1"),
+ 'active_ids': [ref("purchase_requisition_ext_restricted1")],
+ })
+-
+ Create RFQ2. I run the 'Request a quotation' wizard. I fill the supplier.
+-
+ !record {model: purchase.requisition.partner, id: purchase_requisition_ext_restricted1_partner2_create}:
+ partner_id: base.res_partner_3
+-
+ Create RFQ2. I confirm the wizard.
+-
+ !python {model: purchase.requisition.partner}: |
+ self.create_order(cr, uid, [ref("purchase_requisition_ext_restricted1_partner2_create")],{
+ 'active_model': 'purchase.requisition',
+ 'active_id': ref("purchase_requisition_ext_restricted1"),
+ 'active_ids': [ref("purchase_requisition_ext_restricted1")],
+ })
+-
+ Check the RFQs. Type must be 'rfq', state 'draft', all prices 0 and the total untaxed amount 0.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ assert len(purchase_req.purchase_ids) == 2, "There must be 2 RFQs linked to this Call for bids"
+ for rfq in purchase_req.purchase_ids:
+ assert rfq.type == 'rfq', "The type must be rfq"
+ for line in rfq.order_line:
+ assert line.price_subtotal == 0, "The price must be 0 in the RFQ"
+ assert rfq.state == 'draft', "The state must be draft"
+ assert rfq.amount_untaxed == 0
+-
+ I send the RFQs. For this, I print the RFQ.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ for rfq in purchase_req.purchase_ids:
+ self.pool.get('purchase.order').print_quotation(cr, uid, [rfq.id])
+-
+ I check the RFQs are in "RFQ sent" state.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ assert len(purchase_req.purchase_ids) == 2, "There must be 2 RFQs linked to this Call for bids"
+ for rfq in purchase_req.purchase_ids:
+ assert rfq.type == 'rfq', "The type must be rfq"
+ for line in rfq.order_line:
+ assert line.price_subtotal == 0, "The price must be 0 in the RFQ line"
+ assert rfq.state == 'sent', "The state must be sent"
+ assert rfq.amount_untaxed == 0, "The total must be 0 in the RFQ"
+-
+ I encode the bids. I set a price on the lines.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ assert len(purchase_req.purchase_ids) == 2, "There must be 2 RFQs linked to this Call for bids"
+ price = 30
+ for rfq in purchase_req.purchase_ids:
+ for line in rfq.order_line:
+ self.pool.get('purchase.order.line').write(cr, uid, [line.id], {'price_unit': price})
+ price += 5
+-
+ I run the 'Bid encoded' wizard of bid1. I fill the date.
+-
+ !record {model: purchase.action_modal_datetime, id: purchase_requisition_ext_restricted1_bid1_bidencoded}:
+ datetime: '2013-08-13 00:00:00'
+-
+ I run the 'Bid encoded' wizard of bid1. I confirm the wizard.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ assert len(purchase_req.purchase_ids) == 2, "There must be 2 RFQs linked to this Call for bids"
+ for rfq in purchase_req.purchase_ids:
+ self.pool.get('purchase.order').bid_received_ok(cr, uid,
+ [rfq.id],
+ {'active_id': ref("purchase_requisition_ext_restricted1_bid1_bidencoded")
+ })
+-
+ I run the 'Bid encoded' wizard of bid2. I fill the date.
+-
+ !record {model: purchase.action_modal_datetime, id: purchase_requisition_ext_restricted1_bid2_bidencoded}:
+ datetime: '2013-08-13 00:10:00'
+-
+ I run the 'Bid encoded' wizard of bid2. I confirm the wizard.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ assert len(purchase_req.purchase_ids) == 2, "There must be 2 RFQs linked to this Call for bids"
+ for rfq in purchase_req.purchase_ids:
+ self.pool.get('purchase.order').bid_received_ok(cr, uid,
+ [rfq.id],
+ {'active_id': ref("purchase_requisition_ext_restricted1_bid2_bidencoded")
+ })
+-
+ I check the "Bid Encoded" status.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ assert len(purchase_req.purchase_ids) == 2, "There must be 2 bids linked to this Call for bids"
+ for rfq in purchase_req.purchase_ids:
+ assert rfq.type == 'bid', "The type must be bid"
+ for line in rfq.order_line:
+ assert line.price_subtotal != 0, "The price must not be 0 in the bid line"
+ assert rfq.state == 'bid', "The state must be bid"
+ assert rfq.amount_untaxed != 0, "The total amount must not be 0 in the bid"
+-
+ I close the Call for bids and move to bids selection
+-
+ !python {model: purchase.requisition}: |
+ import netsvc
+ wf_service = netsvc.LocalService("workflow")
+ wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_ext_restricted1"), 'open_bid', cr)
+-
+ In the bids selection, I confirm line 1 of bid 1 and line 2 of bid 2. For line 3, I select quantity 10 of bid 1 and confirm. I confirm line 3 of bid 2 for remaining quantity.
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ assert len(purchase_req.purchase_ids) == 2, "There must be 2 bids linked to this Call for bids"
+ self.pool.get('purchase.order.line').action_confirm(cr, uid, [purchase_req.purchase_ids[0].order_line[0].id])
+ self.pool.get('purchase.order.line').action_confirm(cr, uid, [purchase_req.purchase_ids[1].order_line[1].id])
+ qtywiz = self.pool.get('bid.line.qty').create(cr, uid, {'qty': 5})
+ self.pool.get('bid.line.qty').change_qty(cr, uid, [qtywiz], {
+ 'active_mode': 'purchase.order.line',
+ 'active_id': purchase_req.purchase_ids[0].order_line[2].id,
+ 'active_ids': [purchase_req.purchase_ids[0].order_line[2].id],
+ })
+ self.pool.get('purchase.order.line').action_confirm(cr, uid, [purchase_req.purchase_ids[1].order_line[2].id])
+-
+ For each call for bid lines, I check that the confirmed selection match the requested qty
+-
+ !python {model: purchase.requisition}: |
+ from openerp.tools.float_utils import float_is_zero
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ precision = self.pool.get('decimal.precision').precision_get(cr, 1, 'Product Unit of Measure')
+ for line in purchase_req.line_ids:
+ qty = line.product_qty
+ for pol in line.purchase_line_ids:
+ if pol.state == 'confirmed':
+ qty -= pol.quantity_bid
+ assert float_is_zero(qty,precision), "The confirmed amount is different from the requested amount"
+-
+ I close the call for bids
+-
+ !python {model: purchase.requisition}: |
+ self.close_callforbids(cr, uid, [ref("purchase_requisition_ext_restricted1")])
+-
+ I generate the POs
+-
+ !python {model: purchase.requisition}: |
+ self.generate_po(cr, uid, [ref("purchase_requisition_ext_restricted1")])
+-
+ I check the POs
+-
+ !python {model: purchase.requisition}: |
+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_ext_restricted1"))
+ assert len(purchase_req.purchase_ids) == 2, "There must be 2 bids linked to this Call for bids"
+
+
+
+
=== added directory 'purchase_requisition_extended/view'
=== added file 'purchase_requisition_extended/view/purchase_order.xml'
--- purchase_requisition_extended/view/purchase_order.xml 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/view/purchase_order.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<openerp>
+<data>
+ <record model="ir.ui.view" id="view_purchase_order_form">
+ <field name="name">purchase.order.inherit</field>
+ <field name="model">purchase.order</field>
+ <field name="inherit_id" ref="purchase_extended.view_purchase_order_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//button[@name='draft_po'][1]" position="attributes">
+ <attribute name="attrs">{'invisible': ['|', ('requisition_id','!=',False)]}</attribute>
+ </xpath>
+ <xpath expr="//button[@name='draft_po'][2]" position="attributes">
+ <attribute name="attrs">{'invisible': ['|', ('requisition_id','!=',False)]}</attribute>
+ </xpath>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="view_purchase_order_form2">
+ <field name="name">purchase.order.inherit</field>
+ <field name="model">purchase.order</field>
+ <field name="inherit_id" ref="purchase_requisition.purchase_order_form_inherit"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='bid_validity']" position="after">
+ <field name="bid_partial"/>
+ </xpath>
+ <xpath expr="//field[@name='requisition_id']" position="after">
+ <field name="tender_bid_receipt_mode"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_purchase_order_line_search">
+ <field name="name">purchase.order.line.inherit</field>
+ <field name="model">purchase.order.line</field>
+ <field name="inherit_id" ref="purchase.purchase_order_line_search"/>
+ <field name="arch" type="xml">
+ <xpath expr="//filter[@name='hide_cancelled']" position="before">
+ <filter name="showbids" string="Bids encoded" domain="[('order_id.state', '=', 'bid')]"/>
+ </xpath>
+ <xpath expr="//filter[@name='groupby_product']" position="after">
+ <filter name="groupby_requisitionline" string="Call for Bids line" domain="[]" context="{'group_by' : 'requisition_line_id'}" />
+ </xpath>
+ </field>
+ </record>
+ <record model="ir.ui.view" id="view_purchase_order_line_tree">
+ <field name="name">purchase.order.line.inherit</field>
+ <field name="model">purchase.order.line</field>
+ <field name="inherit_id" ref="purchase_requisition.purchase_order_line_tree_tender"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='name']" position="before">
+ <field name="requisition_line_id" invisible="1"/>
+ </xpath>
+ <xpath expr="//field[@name='lead_time']" position="before">
+ <field name="date_planned"/>
+ </xpath>
+ <xpath expr="//field[@name='product_id']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ </field>
+ </record>
+</data>
+</openerp>
=== added file 'purchase_requisition_extended/view/purchase_requisition.xml'
--- purchase_requisition_extended/view/purchase_requisition.xml 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/view/purchase_requisition.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record model="ir.ui.view" id="view_purchase_requisition_form">
+ <field name="name">purchase.requisition.form.inherit</field>
+ <field name="model">purchase.requisition</field>
+ <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//header/field[@name='state']" position="attributes">
+ <attribute name="statusbar_visible">draft,in_progress,open,closed,done</attribute>
+ </xpath>
+ <xpath expr="//header/button[@name='generate_po']" position="attributes">
+ <attribute name="states">closed</attribute>
+ <attribute name="string">Generate PO</attribute>
+ </xpath>
+ <xpath expr="//sheet//button[@name='open_product_line']" position="attributes">
+ <attribute name="attrs">{'invisible': ['|', ('state', 'not in', ('open','closed','done')), ('exclusive', '=', 'exclusive')]}</attribute>
+ </xpath>
+ <xpath expr="//sheet//button[@name='open_product_line']" position="after">
+ <button name="open_po" type="object" string="View Generated PO" states="open,closed,done"/>
+ </xpath>
+ <xpath expr="//field[@name='user_id']" position="after">
+ <field name="bid_tendering_mode" attrs="{'readonly': [('state','not in',('draft'))]}" required="1"/>
+ <field name="pricelist_id" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ </xpath>
+ <separator string="Requests for Quotation" position="attributes">
+ <attribute name="string">Requests for Quotation / Bids</attribute>
+ </separator>
+ <xpath expr="//page[@string='Products']//button[@name='%(purchase_requisition.action_purchase_requisition_partner)d']" position="attributes">
+ <attribute name="attrs">{'invisible': ['|','|',('bid_tendering_mode','=','open'),('line_ids','=',[]),('state','!=','in_progress')]}</attribute>
+ </xpath>
+ <xpath expr="//page[@string='Products']//button[@name='open_rfq']" position="attributes">
+ <attribute name="attrs">{'invisible': ['|',('purchase_ids','=',[]),('state', 'in', ('draft'))]}</attribute>
+ </xpath>
+ <xpath expr="//page[@string='Products']//button[@name='%(purchase_requisition.action_purchase_requisition_partner)d']" position="after">
+ <button name="%(action_purchase_requisition_partner_draftbid)d" type="action"
+ string="Encode a Bid" icon="gtk-execute"
+ attrs="{'invisible': ['|','|',('bid_tendering_mode','=','restricted'),('line_ids','=',[]),('state','!=','in_progress')]}"/>
+ </xpath>
+ <xpath expr="//page[@string='Products']" position="after">
+ <page string="Options">
+ <group>
+ <group>
+ <field name="req_validity" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ <field name="req_incoterm_id" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ <field name="req_incoterm_address" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ <field name="req_payment_term_id" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ </group>
+ <group>
+ <field name="bid_receipt_mode" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ </group>
+ </group>
+ </page>
+ </xpath>
+ <button name="open_bid" position="after">
+ <!--<button name="close_bid" states="open" string="Close Bids Selection" class="oe_highlight" type="object"/>-->
+ <button name="reopen_bid" states="closed" string="Re-Open Bids Selection"/>
+ </button>
+ <xpath expr="//field[@name='purchase_ids']//button[@name='purchase_cancel']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ <xpath expr="//field[@name='purchase_ids']//button[@name='purchase_confirm']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ <xpath expr="//field[@name='purchase_ids']//button[@name='purchase_approve']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ <xpath expr="//field[@name='purchase_ids']//button[@name='wkf_send_rfq']" position="attributes">
+ <attribute name="icon">gtk-apply</attribute>
+ </xpath>
+ <xpath expr="//field[@name='exclusive']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
+ <field name="warehouse_id" position="before">
+ <field name="consignee_id" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ <field name="dest_address_id" on_change="onchange_dest_address_id(dest_address_id, warehouse_id)" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ </field>
+ <field name="warehouse_id" position="replace">
+ <field name="warehouse_id" on_change="onchange_warehouse_id(warehouse_id)" attrs="{'readonly': [('state','not in',('draft'))]}"/>
+ </field>
+ </field>
+ </record>
+ </data>
+</openerp>
=== added directory 'purchase_requisition_extended/wizard'
=== added directory 'purchase_requisition_extended/wizard.moved'
=== added file 'purchase_requisition_extended/wizard/__init__.py'
--- purchase_requisition_extended/wizard/__init__.py 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/wizard/__init__.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+import purchase_requisition_partner
=== added file 'purchase_requisition_extended/wizard/modal.xml'
--- purchase_requisition_extended/wizard/modal.xml 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/wizard/modal.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <record id="action_modal_close_callforbids" model="ir.ui.view">
+ <field name="name">action.modal</field>
+ <field name="model">purchase.action_modal</field>
+ <field name="arch" type="xml">
+ <form string="Warning" version="7.0">
+ <p>
+ For some lines, the selected quantity does not match the requested quantity.
+ Please confirm the operation.
+ </p>
+ <footer>
+ <button string="Confirm" class="oe_highlight" type="object" name="action"/>
+ or
+ <button special="cancel" string="Cancel" class="oe_link"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+ </data>
+</openerp>
=== added file 'purchase_requisition_extended/wizard/purchase_requisition_partner.py'
--- purchase_requisition_extended/wizard/purchase_requisition_partner.py 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/wizard/purchase_requisition_partner.py 2015-01-19 14:42:10 +0000
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from openerp.osv import fields, orm
+from openerp.tools.translate import _
+
+
+class purchase_requisition_partner(orm.TransientModel):
+ _inherit = "purchase.requisition.partner"
+
+ def create_order(self, cr, uid, ids, context=None):
+ if context is None:
+ context = {}
+ active_id = context and context.get('active_id', [])
+ data = self.browse(cr, uid, ids, context=context)[0]
+ po_id = self.pool.get('purchase.requisition').make_purchase_order(cr,
+ uid, [active_id], data.partner_id.id,
+ context=context)[active_id]
+ if not context.get('draft_bid', False):
+ return {'type': 'ir.actions.act_window_close'}
+ res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid,
+ 'purchase', 'purchase_rfq', context=context)
+ res.update({'res_id': po_id,
+ 'views': [(False, 'form')],
+ })
+ return res
=== added file 'purchase_requisition_extended/wizard/purchase_requisition_partner_view.xml'
--- purchase_requisition_extended/wizard/purchase_requisition_partner_view.xml 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/wizard/purchase_requisition_partner_view.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,31 @@
+<openerp>
+<data>
+ <record id="view_purchase_requisition_partner_draftbid" model="ir.ui.view">
+ <field name="name">Choose Supplier</field>
+ <field name="model">purchase.requisition.partner</field>
+ <field name="priority">50</field>
+ <field name="arch" type="xml">
+ <form string="Choose Supplier" version="7.0">
+ <group>
+ <field name="partner_id" context="{'default_supplier': 1, 'default_customer': 0}"/>
+ </group>
+ <footer>
+ <button name="create_order" string="Create Bid" type="object" class="oe_highlight"/>
+ or
+ <button string="Cancel" class="oe_link" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+ <record id="action_purchase_requisition_partner_draftbid" model="ir.actions.act_window">
+ <field name="name">Choose Supplier</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">purchase.requisition.partner</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="view_purchase_requisition_partner_draftbid"/>
+ <field name="context">{'record_id' : active_id, 'draft_bid': 1}</field>
+ <field name="target">new</field>
+ </record>
+</data>
+</openerp>
=== added directory 'purchase_requisition_extended/workflow'
=== added file 'purchase_requisition_extended/workflow/purchase_requisition.xml'
--- purchase_requisition_extended/workflow/purchase_requisition.xml 1970-01-01 00:00:00 +0000
+++ purchase_requisition_extended/workflow/purchase_requisition.xml 2015-01-19 14:42:10 +0000
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+ <!-- Add intermediate activity 'closed' between 'open' and 'done' -->
+ <record id="act_closed" model="workflow.activity">
+ <field name="wkf_id" ref="purchase_requisition.purchase_requisition_workflow"/>
+ <field name="name">Bids selected</field>
+ <field name="kind">function</field>
+ <field name="action">tender_close()</field>
+ </record>
+ <record id="purchase_requisition.trans_open_done" model="workflow.transition">
+ <field name="act_from" ref="act_closed"/>
+ </record>
+ <record id="trans_open_closed" model="workflow.transition">
+ <field name="act_from" ref="purchase_requisition.act_open"/>
+ <field name="act_to" ref="act_closed"/>
+ <field name="signal">close_bid</field>
+ </record>
+ <record id="trans_closed_open" model="workflow.transition">
+ <field name="act_from" ref="act_closed"/>
+ <field name="act_to" ref="purchase_requisition.act_open"/>
+ <field name="signal">reopen_bid</field>
+ </record>
+
+ <!-- Check there are product lines when confirming the Call for Bids -->
+ <record id="purchase_requisition.trans_draft_sent" model="workflow.transition">
+ <field name="condition">_has_product_lines()</field>
+ </record>
+ </data>
+</openerp>