diff --git a/addons/purchase/edi/purchase_order_action_data.xml b/addons/purchase/edi/purchase_order_action_data.xml index b9c3c3ab95d..311cb4603ae 100644 --- a/addons/purchase/edi/purchase_order_action_data.xml +++ b/addons/purchase/edi/purchase_order_action_data.xml @@ -18,7 +18,7 @@ - Purchase Order - Send by mail + RFQ - Send by Email ${object.validator.email or ''} ${object.company_id.name} Order (Ref ${object.name or 'n/a' }) ${object.partner_id.id} @@ -34,6 +34,80 @@

Here is a ${object.state in ('draft', 'sent') and 'request for quotation' or 'purchase order confirmation'} from ${object.company_id.name}:

+

+   REFERENCES
+   RFQ number: ${object.name}
+   RFQ date: ${object.date_order}
+ % if object.origin: +   RFQ reference: ${object.origin}
+ % endif + % if object.partner_ref: +   Your reference: ${object.partner_ref}
+ % endif + % if object.validator: +   Your contact: ${object.validator.name} + % endif +

+ +
+

If you have any question, do not hesitate to contact us.

+

Thank you!

+
+
+
+

+ ${object.company_id.name}

+
+
+ + % if object.company_id.street: + ${object.company_id.street}
+ % endif + % if object.company_id.street2: + ${object.company_id.street2}
+ % endif + % if object.company_id.city or object.company_id.zip: + ${object.company_id.zip} ${object.company_id.city}
+ % endif + % if object.company_id.country_id: + ${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}
+ % endif +
+ % if object.company_id.phone: +
+ Phone:  ${object.company_id.phone} +
+ % endif + % if object.company_id.website: +
+ Web : ${object.company_id.website} +
+ %endif +

+
+ + ]]> +
+ + + + + Purchase Order - Send by Email + ${object.validator.email or ''} + ${object.company_id.name} Order (Ref ${object.name or 'n/a' }) + ${object.partner_id.id} + + + + PO_${(object.name or '').replace('/','_')} + ${object.partner_id.lang} + + +

Hello ${object.partner_id.name},

+ +

Here is a ${object.state in ('draft', 'sent') and 'request for quotation' or 'purchase order confirmation'} from ${object.company_id.name}:

+

  REFERENCES
  Order number: ${object.name}
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 01643ee44c6..d13b2cbf21a 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -153,8 +153,9 @@ class purchase_order(osv.osv): STATE_SELECTION = [ ('draft', 'Draft PO'), ('sent', 'RFQ Sent'), + ('bid', 'Bid Received'), ('confirmed', 'Waiting Approval'), - ('approved', 'Purchase Order'), + ('approved', 'Purchase Confirmed'), ('except_picking', 'Shipping Exception'), ('except_invoice', 'Invoice Exception'), ('done', 'Done'), @@ -173,7 +174,7 @@ class purchase_order(osv.osv): help="Reference of the document that generated this purchase order request; a sales order or an internal procurement request." ), 'partner_ref': fields.char('Supplier Reference', states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, size=64, - help="Reference of the sales order or quotation sent by your supplier. It's mainly used to do the matching when you receive the products as this reference is usually written on the delivery order sent by your supplier."), + help="Reference of the sales order or bid sent by your supplier. It's mainly used to do the matching when you receive the products as this reference is usually written on the delivery order sent by your supplier."), 'date_order':fields.date('Order Date', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}, select=True, help="Date on which this document has been created."), 'date_approve':fields.date('Date Approved', readonly=1, select=True, help="Date on which purchase order has been approved"), 'partner_id':fields.many2one('res.partner', 'Supplier', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, @@ -187,7 +188,7 @@ class purchase_order(osv.osv): 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]} ), 'pricelist_id':fields.many2one('product.pricelist', 'Pricelist', required=True, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)],'done':[('readonly',True)]}, help="The pricelist sets the currency used for this purchase order. It also computes the supplier price for the selected products/quantities."), 'currency_id': fields.many2one('res.currency','Currency', readonly=True, required=True,states={'draft': [('readonly', False)],'sent': [('readonly', False)]}), - 'state': fields.selection(STATE_SELECTION, 'Status', readonly=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.", select=True), + 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="The status of the purchase order or the quotation request. A request for 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.", select=True), 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', states={'approved':[('readonly',True)],'done':[('readonly',True)]}), 'validator' : fields.many2one('res.users', 'Validated by', readonly=True), 'notes': fields.text('Terms and Conditions'), @@ -222,10 +223,13 @@ class purchase_order(osv.osv): }, multi="sums",help="The total amount"), 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'), 'payment_term_id': fields.many2one('account.payment.term', 'Payment Term'), + 'incoterm_id': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."), 'product_id': fields.related('order_line','product_id', type='many2one', relation='product.product', string='Product'), 'create_uid': fields.many2one('res.users', 'Responsible'), 'company_id': fields.many2one('res.company','Company',required=True,select=1, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}), 'journal_id': fields.many2one('account.journal', 'Journal'), + 'bid_date': fields.date('Bid Received On', readonly=True, help="Date on which the bid was received"), + 'bid_validity': fields.date('Bid Valid Until', help="Date on which the bid expired"), } _defaults = { 'date_order': fields.date.context_today, @@ -271,6 +275,13 @@ class purchase_order(osv.osv): return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context) + def set_order_line_status(self, cr, uid, ids, status, context=None): + line = self.pool.get('purchase.order.line') + for order in self.browse(cr, uid, ids, context=context): + order_line_ids = [order_line.id for order_line in order.order_line] + line.write(cr, uid, order_line_ids, {'state': status}, context=context) + return True + def button_dummy(self, cr, uid, ids, context=None): return True @@ -398,6 +409,9 @@ class purchase_order(osv.osv): self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)}) return True + def wkf_bid_received(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state':'bid', 'bid_date': fields.date.context_today(self,cr,uid,context=context)}) + def print_confirm(self,cr,uid,ids,context=None): print "Confirmed" @@ -411,9 +425,14 @@ class purchase_order(osv.osv): ''' This function opens a window to compose an email, with the edi purchase template message loaded by default ''' + if not context: + context= {} ir_model_data = self.pool.get('ir.model.data') try: - template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1] + if context.get('send_rfq', False): + template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1] + else: + template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase_done')[1] except ValueError: template_id = False try: @@ -506,12 +525,17 @@ class purchase_order(osv.osv): if not len(ids): return False self.write(cr, uid, ids, {'state':'draft','shipped':0}) + self.set_order_line_status(cr, uid, ids, 'draft', context=context) for p_id in ids: # Deleting the existing instance of workflow for PO self.delete_workflow(cr, uid, [p_id]) # TODO is it necessary to interleave the calls? self.create_workflow(cr, uid, [p_id]) return True + def wkf_po_done(self, cr, uid, ids, context=None): + self.write(cr, uid, ids, {'state': 'done'}, context=context) + self.set_order_line_status(cr, uid, ids, 'done', context=context) + def action_invoice_create(self, cr, uid, ids, context=None): """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order. :param ids: list of ids of purchase orders. @@ -577,6 +601,10 @@ class purchase_order(osv.osv): return True return False + def wkf_action_cancel(self, cr, uid, ids, context=None): + self.write(cr,uid,ids,{'state':'cancel'}) + self.set_order_line_status(cr, uid, ids, 'cancel', context=context) + def action_cancel(self, cr, uid, ids, context=None): for purchase in self.browse(cr, uid, ids, context=context): for pick in purchase.picking_ids: @@ -594,7 +622,7 @@ class purchase_order(osv.osv): self.pool.get('account.invoice') \ .signal_invoice_cancel(cr, uid, map(attrgetter('id'), purchase.invoice_ids)) self.write(cr,uid,ids,{'state':'cancel'}) - + self.set_order_line_status(cr, uid, ids, 'cancel', context=context) self.signal_purchase_cancel(cr, uid, ids) return True @@ -755,6 +783,7 @@ class purchase_order(osv.osv): 'invoiced':False, 'invoice_ids': [], 'picking_ids': [], + 'origin' : '', 'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'), }) return super(purchase_order, self).copy(cr, uid, id, default, context) @@ -909,7 +938,6 @@ class purchase_order_line(osv.osv): 'invoiced': fields.boolean('Invoiced', readonly=True), 'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True), 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date"), - } _defaults = { 'product_qty': lambda *a: 1.0, @@ -928,7 +956,7 @@ class purchase_order_line(osv.osv): def onchange_product_uom(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): + name=False, price_unit=False, state='draft', context=None): """ onchange handler of product_uom. """ @@ -936,7 +964,7 @@ class purchase_order_line(osv.osv): return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}} return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id, partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned, - name=name, price_unit=price_unit, context=context) + name=name, price_unit=price_unit, state=state, context=context) def _get_date_planned(self, cr, uid, supplier_info, date_order_str, context=None): """Return the datetime value to use as Schedule Date (``date_planned``) for @@ -961,7 +989,7 @@ class purchase_order_line(osv.osv): 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): + name=False, price_unit=False, state='draft', context=None): """ onchange handler of product_id. """ @@ -1035,12 +1063,14 @@ class purchase_order_line(osv.osv): if qty: res['value'].update({'product_qty': qty}) - # - determine price_unit and taxes_id - if pricelist_id: - price = product_pricelist.price_get(cr, uid, [pricelist_id], - product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order})[pricelist_id] - else: - price = product.standard_price + price = price_unit + if state not in ('sent','bid'): + # - determine price_unit and taxes_id + if pricelist_id: + price = product_pricelist.price_get(cr, uid, [pricelist_id], + product.id, qty or 1.0, partner_id or False, {'uom': uom_id, 'date': date_order})[pricelist_id] + else: + price = product.standard_price taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id)) fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False @@ -1050,7 +1080,7 @@ class purchase_order_line(osv.osv): return res product_id_change = onchange_product_id - product_uom_change = onchange_product_uom + product_uom_change = onchange_product_uom def action_confirm(self, cr, uid, ids, context=None): self.write(cr, uid, ids, {'state': 'confirmed'}, context=context) @@ -1128,6 +1158,7 @@ class procurement_order(osv.osv): # res = res.values() # return len(res) and res[0] or 0 #TO CHECK: why workflow is generated error if return not integer value + def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None): """Create the purchase order from the procurement, using the provided field values, after adding the given purchase @@ -1188,8 +1219,8 @@ class procurement_order(osv.osv): warehouse_obj = self.pool.get('stock.warehouse') for procurement in self.browse(cr, uid, ids, context=context): res_id = procurement.move_id.id - #TODO: so if the seller does not exist, it will just crash... partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement. + assert partner, ('There is no supplier associated to product %s', (procurements.product_id.name)) seller_qty = procurement.product_id.seller_qty partner_id = partner.id address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery'] @@ -1258,7 +1289,9 @@ class mail_mail(osv.Model): def _postprocess_sent_message(self, cr, uid, mail, context=None): if mail.model == 'purchase.order': - self.pool.get('purchase.order').signal_send_rfq(cr, uid, [mail.res_id]) + obj = self.pool.get('purchase.order').browse(cr, uid, mail.res_id, context=context) + if obj.state == 'draft': + self.pool.get('purchase.order').signal_send_rfq(cr, uid, [mail.res_id]) return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context) diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index 6fc61724e2a..24317b9fa9c 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -159,28 +159,29 @@

-

-

@@ -200,16 +201,16 @@ - + - + - - + + @@ -228,7 +229,15 @@
- + + + + + + + + + @@ -329,11 +338,11 @@ - Quotations + Requests for Quotation ir.actions.act_window purchase.order {} - [('state','in',('draft','sent','cancel', 'confirmed'))] + [('state','in',('draft','sent','bid','cancel', 'confirmed'))] tree,form,graph,calendar @@ -359,7 +368,7 @@ purchase.order tree,form,graph,calendar {} - [('state','not in',('draft','sent','confirmed'))] + [('state','not in',('draft','sent','bid', 'confirmed'))]

@@ -382,11 +391,11 @@ - +

- - + +
@@ -429,6 +438,7 @@
+ purchase.order.line.form2 purchase.order.line @@ -480,9 +490,10 @@ + - - + + @@ -507,6 +518,7 @@

+ tree diff --git a/addons/purchase/purchase_workflow.xml b/addons/purchase/purchase_workflow.xml index f2459ad12a1..294dad28d92 100644 --- a/addons/purchase/purchase_workflow.xml +++ b/addons/purchase/purchase_workflow.xml @@ -19,6 +19,12 @@ function write({'state':'sent'}) + + + bid + function + wkf_bid_received() + confirmed @@ -31,7 +37,7 @@ cancel function True - write({'state':'cancel'}) + wkf_action_cancel() @@ -88,7 +94,7 @@ done - write({'state':'done'}) + wkf_po_done() function True AND @@ -104,16 +110,26 @@ send_rfq - - + + purchase_confirm + + + + bid_received + purchase_cancel + + + + purchase_cancel + @@ -221,7 +237,7 @@ - check_buy() and check_supplier_info() + check_buy() and check_supplier_info() and not check_product_requisition() diff --git a/addons/purchase/res_config.py b/addons/purchase/res_config.py index ffbbbfd974e..95f2454f3f0 100644 --- a/addons/purchase/res_config.py +++ b/addons/purchase/res_config.py @@ -49,10 +49,14 @@ Example: Product: this product is deprecated, do not purchase more than 5. 'module_purchase_double_validation': fields.boolean("Force two levels of approvals", help="""Provide a double validation mechanism for purchases exceeding minimum amount. This installs the module purchase_double_validation."""), - 'module_purchase_requisition': fields.boolean("Manage purchase requisitions", - help="""Purchase Requisitions are used when you want to request quotations from several suppliers for a given set of products. + 'module_purchase_requisition': fields.boolean("Manage calls for bids", + help="""Calls for bids are used when you want to generate requests for quotations to several suppliers for a given set of products. You can configure per product if you directly do a Request for Quotation - to one supplier or if you want a purchase requisition to negotiate with several suppliers."""), + to one supplier or if you want a Call for Bids to compare offers from several suppliers."""), + 'group_advance_purchase_requisition': fields.boolean("Choose from several bids in a call for bids", + implied_group='purchase.group_advance_bidding', + help="""In the process of a public bidding, you can compare the bid lines and choose for each requested product from which bid you + buy which quantity"""), 'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on purchase orders', help ="""Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans. This installs the module purchase_analytic_plans."""), diff --git a/addons/purchase/res_config_view.xml b/addons/purchase/res_config_view.xml index 276dc892392..6983acb854a 100644 --- a/addons/purchase/res_config_view.xml +++ b/addons/purchase/res_config_view.xml @@ -66,6 +66,10 @@
+
+ +